DaloyJS A2A agent endpoint
Agent2Agent (A2A) is the Linux Foundation protocol for one agent calling another agent it does not share memory, tools, or code with. DaloyJS can expose an existing service as an A2A 1.0 peer: other agents discover it through a public Agent Card and send it work over the JSON-RPC binding.
The split is deliberate. DaloyJS owns the protocol: the card, the envelope, version and extension negotiation, validation, error codes, task bookkeeping, and the auth guardrails. You own the meaning: one onMessage function reads the message and decides what to do. DaloyJS is not an agent framework and runs no LLM or skill router. If you want a model to interpret requests, call it from your handler.
- Peer agentany A2A 1.0 client
- Agent CardGET /.well-known/agent-card.json
- JSON-RPC endpointPOST /a2a, auth + validation
- Your onMessagereply or task result
- Existing systemsdatabase, REST handlers, queues
A2A, MCP, and OpenAPI
These are three faces of the same service, not competitors. Pick by who is calling.
| Surface | Caller | Unit of work |
|---|---|---|
| OpenAPI + typed client | Humans and generated SDKs | HTTP route |
| MCP | An AI client using your service as a tool | Tool call, resource read |
| A2A | Another agent treating you as an opaque peer | Message, task with a lifecycle, artifacts |
If the question is "can Cursor call my inventory API?", that is MCP. If it is "can a partner's procurement agent hand my service a job and get a result back?", that is A2A.
Install
A2A ships in core at @daloyjs/core/a2a (also re-exported from @daloyjs/core). No extra SDK is installed.
Create an agent
createA2aHandler() builds the protocol layer and a2aRoutes() mounts it. The card route is GET /.well-known/agent-card.json. The JSON-RPC route is POST at the path you choose, plus a GET hint (405) and OPTIONS preflight. The hooks option applies auth to the POST route only, so the card stays reachable without an except().
The route handlers forward the Daloy ctx.state to onMessage, so the principal your auth middleware stored is available as ctx.state inside the handler.
The Agent Card
You write the identity and skills. DaloyJS derives supportedInterfaces and capabilities from what the handler actually implements, so the card cannot advertise a feature that would fail when called. The card is validated at startup: it needs at least one skill, every skill needs an id, name, description, and a tag, URLs must be absolute http(s) without embedded credentials, and every security requirement must name a declared scheme.
The card response carries Cache-Control: public, max-age=300 (tune with cardMaxAgeSeconds) and a strong ETag, and answers If-None-Match with 304. Skills are descriptive. A2A has no skill id on messages, which is exactly why your handler decides what to run.
Wire format
DaloyJS implements the A2A 1.0 JSON-RPC binding: PascalCase methods, ProtoJSON field names, SCREAMING_SNAKE enums, and parts whose member name is the type (text, raw, url, or data). The 0.3 kind field and message/send style names are not accepted.
What onMessage returns
Return a string or { reply } for a direct answer that creates no task. Return { status } for a task. The spec allows both, and a direct message is the right choice for simple, stateless answers.
The context also carries text (all text parts joined), contextId, the taskId a task would get, acceptedOutputModes, request metadata, the extensions the client activated, and signal, which aborts when the request times out.
Multi-turn tasks
Without a task store the agent is stateless, which fits serverless and edge deployments. Add a taskStore to enable GetTask, CancelTask, ListTasks, and the input-required / auth-required states a client continues by sending another message with the same taskId.
- 01requestPeer agentDaloyJS A2ASendMessage"Check stock"
- 02noteDaloyJS A2AonMessagectx.task is undefined
- 03responseDaloyJS A2APeer agenttask: TASK_STATE_INPUT_REQUIRED"Which SKU?" (stored under the caller)
- 04requestPeer agentDaloyJS A2ASendMessage with taskId"ABC-1"
- 05noteDaloyJS A2AonMessagectx.task is a copy of the stored task
- 06responseDaloyJS A2APeer agenttask: TASK_STATE_COMPLETEDartifacts + history
Task ownership fails closed. A task store requires taskOwner. Every store call is scoped by the value it returns, so one caller can never read, continue, cancel, or list another caller's tasks: those requests get -32001 TaskNotFound, which does not reveal that the task exists. When taskOwner returns nothing, the request is refused with 401 instead of falling into a shared bucket. With tenancy, include the tenant in the owner, for example JSON.stringify([state.tenant, user.sub]).
memoryTaskStore() is bounded, deep-copies on read and write, and expires entries. On serverless or several instances, a follow-up call can land somewhere else, so bring a shared store:
Methods and capabilities
Version 1 is honest about what it does not do. The card says streaming, pushNotifications, and extendedAgentCard are false, and those methods return the errors the spec requires instead of hanging.
| Method | Behavior |
|---|---|
SendMessage | Runs onMessage. Returns { message } or { task }. |
GetTask | Needs a task store. Otherwise -32001 TaskNotFound. |
CancelTask | Needs a task store. Finished tasks answer -32002 TaskNotCancelable. |
ListTasks | Needs a store with list(). Otherwise -32004 UnsupportedOperation. |
SendStreamingMessage, SubscribeToTask | -32004 UnsupportedOperation (streaming: false). |
*TaskPushNotificationConfig | -32003 PushNotificationNotSupported (pushNotifications: false). |
GetExtendedAgentCard | -32004 UnsupportedOperation (extendedAgentCard: false). |
Anything else, including 0.3 names like message/send | -32601 Method not found. |
Versioning. Clients send A2A-Version: 1.0. An empty header means 0.3 per the spec, and 0.3 is not implemented, so it is refused with -32009 VersionNotSupported. Extensions. A card extension marked required: true must be activated in the A2A-Extensions header, or the request gets -32008.
Calling your agent
Any A2A 1.0 client can call a DaloyJS agent. This example uses the official JS SDK, which DaloyJS is tested against for discovery, direct replies, multi-turn tasks, GetTask, ListTasks, and error mapping.
Error handling
Validation failures return -32602 with a google.rpc.BadRequest list of field violations. A2A errors carry a google.rpc.ErrorInfo with the spec's reason, for example TASK_NOT_FOUND. Throw A2aError to choose the code yourself. Unexpected throws become a redacted -32603. Raw error text is shown only when NODE_ENV is development or test, and never on an App running in production unless you set exposeInternalErrors explicitly.
What stays out of core
- Streaming (
SendStreamingMessage, SSE) and push notifications. Planned only when real users need work that outlives one request. - The gRPC and HTTP+JSON bindings. The card lists JSON-RPC only.
- Signed and extended Agent Cards.
- Agent orchestration, skill routing, memory, and LLM calls. Those belong in your handler or an agent framework.
- Fetching
urlparts. DaloyJS validates that they arehttp(s)without credentials, but never fetches them.
Security checklist
- Authenticate the JSON-RPC route with the
hooksoption or app middleware. In production asecureDefaultsApp refuses to boot without it. For a genuinely public, read-only agent, opt out witha2aRoutes(path, handler, { public: true }). - Keep the card truthful and free of internal details. It is public, and other agents act on it. Do not list operator-only skills.
- Use an
https:card URL. In production, registration throws onhttp:for any non-loopback host. - Resolve
taskOwnerfrom the verified identity only, never from a header or message field the caller controls. - Route any fetch of a
urlpart throughfetchGuard()so a peer cannot aim your service at loopback, private, or cloud-metadata addresses. - Treat message text and data as untrusted input, especially if your handler passes it to a model. A peer agent is just another client.
- Rate-limit the endpoint like any other API route, and keep the default
Origincheck. Add trusted browser apps toallowedOriginsinstead of a wildcard CORS layer.