Dispatch

https://dispatch.foo/a2a/ext/envelope/v1

From §8 of the Dispatch Messaging Protocol 1.0.0-draft.2.

8.3 Extension activation and placement

The three extensions carry what A2A 1.0 has no field for: DMP addressing, why a task is waiting on its owner, and handoffs of work. Each extension URI serves its own subsection together with this one.

An extension is active for a request when its URI appears in the A2A-Extensions header or in message.extensions. The response echoes the active URIs in A2A-Extensions, and a host MUST write extension metadata only for active extensions. A client that activates none gets the same facts as plain text in status.message.

Extension data sits in metadata[<URI>]. On a Message (including status.message) and on an Artifact, the object's extensions list also names the URI. A Task has no extensions field, so task-level data lives only in Task.metadata[<URI>], written when the request activated the extension.

All metadata on one message together MUST be at most 64 KiB as JSON; a host refuses more as invalid on message.metadata. The extensions fit well inside it: work/v1 tops out near 36 KB and envelope/v1 near 15 KB with inbound refs restricted as §8.4 says. Body (64 KiB), data (64 KiB), metadata (64 KiB) and A2A's framing fit a 256 KiB request. Vectors: a2a.envelope-ext.refuses-metadata-over-64-kib.

Each extension is versioned by its URI: a breaking change mints a new URI ending /v2 (§14.4). The registry of extension URIs is §11.10.

8.4 envelope/v1

https://dispatch.foo/a2a/ext/envelope/v1 is a profile on Message.metadata: a DMP envelope (§4.1) riding on an A2A message.

interface EnvelopeExtV1 {
  id?: string; // host to client: the DMP message id
  thread?: string; // host to client: the DMP thread id
  from?: string; // host to client: the sender's address; ignored inbound
  to?: string[]; // client to host only: recipients (default: the owner)
  kind?: 'message' | 'question' | 'answer' | 'handoff' | 'notice';
  replyTo?: string; // a DMP message id the caller takes part in
  blocking?: boolean; // questions only; a client's question always blocks
  choices?: string[]; // at most 20, each one line of at most 200 bytes
  choice?: string; // answers only; one of the question's choices
  refs?: { type: string; id: string }[]; // inbound: at most 50, task and message ids only
  urgent?: boolean; // inbound true is refused
  wake?: 'none' | 'request'; // inbound 'request' is refused
}

Inbound. A host reads a client's envelope before anything else of the message, and MUST apply these rules on top of §4.5:

Vectors: a2a.envelope-ext.accepts-recipients-choices-and-message-refs, a2a.envelope-ext.accepts-no-envelope, a2a.envelope-ext.refuses-urgent, a2a.envelope-ext.refuses-a-wake-request, a2a.envelope-ext.refuses-a-private-use-kind, a2a.envelope-ext.refuses-an-unknown-kind, a2a.envelope-ext.refuses-a-bad-recipient, a2a.envelope-ext.refuses-a-ref-type-other-than-task-or-message, a2a.envelope-ext.refuses-a-ref-anchor, a2a.envelope-ext.refuses-more-than-20-choices, a2a.envelope-ext.refuses-a-choice-over-200-bytes, a2a.envelope-ext.refuses-blocking-that-is-not-a-boolean, a2a.external.the-host-sets-id-thread-and-from, a2a.external.defaults-to-a-blocking-question-to-the-owner, a2a.external.a-client-reaches-only-the-owner, a2a.external.an-a2a-recipient-reads-as-the-owner, a2a.external.an-opening-answer-or-handoff-is-refused, a2a.external.an-answer-from-a-client-is-never-a-close.

Outbound. A host MUST NOT write to to a client. To a peer it writes only the peer's own address, so no other recipient leaves the host. Refs are written as the DMP sender wrote them, as opaque strings.

External data is stored wrapped under this URI, as { "https://dispatch.foo/a2a/ext/envelope/v1": <value> }, so it has no top-level type (§8.8); it is unwrapped when it goes back to the client that sent it.