a2a-plugin
v0.2.0
Published
CAP plugin that automatically exposes annotated CDS services as A2A (Agent-to-Agent) agents.
Maintainers
Readme
a2a-plugin
A CAP plugin that automatically exposes annotated CDS services as A2A (Agent-to-Agent protocol) agents.
Installation
npm add a2a-pluginCAP activates the plugin automatically via the cds.plugin flag.
Usage
1. Annotate your CDS service
Both @a2a and @a2a.Skill are required. @a2a opts the service into the A2A protocol; without it the plugin ignores the service entirely.
@a2a.Skill: {
id: 'my-skill',
name: 'My Skill',
description: 'Does something useful for an LLM caller.'
}
@a2a
service MyService {}2. Implement the agent handler
import cds from '@sap/cds';
import { AgentRequest, getEventBus } from 'a2a-plugin/lib/types';
export default class MyService extends cds.ApplicationService {
async init() {
this.on('agent', async (req: AgentRequest) => {
// Optional: publish an intermediate status update
getEventBus()?.publish({
kind: 'status-update',
contextId: req.data.contextId,
taskId: req.data.taskId,
final: false,
status: { state: 'working' },
});
return 'Hello from my CAP agent!';
});
return super.init();
}
}Endpoints
The plugin mounts the following routes at /a2a/<service-path>:
| Route | Description |
|---|---|
| GET .well-known/agent.json | Agent card (A2A discovery) |
| POST /jsonrpc | JSON-RPC 2.0 transport |
| * /rest/* | HTTP+JSON REST transport |
Customising execution lifecycle
By default the plugin creates a CAPAgentExecutor internally and dispatches to your agent handler. If you need control over the full execution lifecycle — custom error handling, pre/post logic, different transport behaviour — extend CAPAgentService instead of cds.ApplicationService:
import { ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
import { AgentRequest, CAPAgentService } from 'a2a-plugin/lib/types';
export default class MyService extends CAPAgentService {
async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
// pre-execution logic
await super.execute(requestContext, eventBus);
// post-execution logic
}
async init() {
this.on('agent', async (req: AgentRequest) => {
return 'Hello from my CAP agent!';
});
return super.init();
}
}CAPAgentService extends cds.ApplicationService and implements the A2A AgentExecutor interface. When the plugin detects that the service already satisfies AgentExecutor, it uses it directly as the executor — no separate internal executor is created. Override cancelTask the same way to customise task cancellation.
Services that extend plain cds.ApplicationService continue to work exactly as before.
API
CAPAgentService
import { CAPAgentService } from 'a2a-plugin/lib/types';Base class combining cds.ApplicationService with the A2A AgentExecutor interface. Override execute and/or cancelTask to customise the execution lifecycle. Both methods are regular prototype methods so super calls work normally.
getEventBus(): ExecutionEventBus | undefined
import { getEventBus } from 'a2a-plugin/lib/types';Returns the ExecutionEventBus for the currently running A2A task. Only available inside an agent handler. Use it to publish streaming intermediate status updates back to the client.
AgentRequest
import { AgentRequest } from 'a2a-plugin/lib/types';Typed alias for cds.Request<RequestContext>. Gives typed access to req.data.taskId, req.data.contextId, and req.data.userMessage.
Skill annotation reference
| Annotation | Type | Required | Description |
|---|---|---|---|
| @a2a.Skill.id | String | ✓ | Unique skill identifier |
| @a2a.Skill.name | String | ✓ | Human-readable name |
| @a2a.Skill.description | String | ✓ | Skill description for LLM prompting |
| @a2a.Skill.tags | [String] | | Categorisation tags |
| @a2a.Skill.examples | [String] | | Example input queries |
| @a2a.Skill.inputModes | [String] | | Accepted MIME types (default: ["text/plain"]) |
| @a2a.Skill.outputModes | [String] | | Produced MIME types (default: ["text/plain"]) |
Input mapping
The req.data in the agent handler is the full A2A RequestContext. The plugin also attempts to extract a plain text value from the incoming message:
- If the request has a
DataPartwhose body can be parsed as JSON, it is spread intoreq.data. - Otherwise the first text content of the message is mapped to the first
String-typed action parameter.
Security
The adapter uses the authentication method configured for the CAP service.
License
Apache-2.0
