Dispatch

https://dispatch.foo/a2a/ext/work/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.6 work/v1

https://dispatch.foo/a2a/ext/work/v1 is a profile plus a sub-state of TASK_STATE_WORKING.

A client sends a request on Message.metadata; it selects the skill, and its absence means ask:

type WorkRequestV1 =
  | {
      skill: 'handoff';
      title: string; // required; one line, not blank, at most 200 bytes
      acceptance?: string[]; // at most 20 entries, each one line of at most 500 bytes
      writes?: string[]; // at most 50 repo-relative paths or globs, each at most 512 bytes
      priority?: 'urgent' | 'high' | 'medium' | 'low' | 'none'; // capped at 'medium'
      labels?: string[]; // at most 10, each at most 50 bytes
    }
  | { skill: 'status'; task?: string }; // an A2A task id; absent means all of the caller's

A host MUST refuse a request that breaks these limits as invalid on the field or its slot, under work. (work.title, work.writes[1]). Lengths are UTF-8 bytes. A write is relative to the repository: an absolute path, a drive letter, a backslash or a .. segment is refused. A request that is not an object is refused on work, and a skill other than handoff or status on work.skill. Vectors: a2a.work-ext.accepts-a-handoff-with-every-field, a2a.work-ext.accepts-a-status-request, a2a.work-ext.refuses-an-unknown-skill, a2a.work-ext.refuses-a-handoff-without-a-title, a2a.work-ext.refuses-a-title-over-200-bytes, a2a.work-ext.refuses-a-multi-line-title, a2a.work-ext.refuses-more-than-20-acceptance-criteria, a2a.work-ext.refuses-a-criterion-over-500-bytes, a2a.work-ext.refuses-a-multi-line-criterion, a2a.work-ext.refuses-more-than-50-writes, a2a.work-ext.refuses-a-write-outside-the-repository, a2a.work-ext.refuses-a-write-over-512-bytes, a2a.work-ext.refuses-an-unknown-priority, a2a.work-ext.refuses-more-than-10-labels, a2a.work-ext.refuses-a-request-that-is-not-an-object.

Handoffs. A handoff becomes a draft work item that a deciding principal approves before anything runs. The host raises a task-proposal gate for it (§11.6): only the system address raises one, its choices are approve and decline, and its data names the drafted work item, the client that proposed it and the handoff message. Approving it promotes the draft; declining it drops the draft. A handoff starts at the host's highest risk, so none of its own gates decides itself; a client chooses no risk, its priority is capped at medium, and its labels are stored under an a2a/ prefix. Vectors: core.gates.task-proposal-is-raised-only-by-the-system, core.gates.task-proposal-needs-a-deciding-answer, core.gates.task-proposal-is-answered-with-approve-or-decline.

The host tells the client its handoff's state on Task.metadata and on status.message.metadata:

interface WorkStateV1 {
  task: string; // the DMP work item id
  title: string;
  status: string; // draft, ready, working, review, landing, landed, dropped, or a host's own
  stage?: 'review' | 'landing'; // the TASK_STATE_WORKING sub-state
}

Results go on Artifact.metadata, and an artifact's artifactId equals its kind:

type WorkArtifactV1 =
  | { kind: 'answer'; messageId: string; choice?: string }
  | { kind: 'pr'; url: string; number: number; state?: 'open' | 'merged' }
  | {
      kind: 'diffstat';
      files: number;
      insertions: number;
      deletions: number;
      perFile: { path: string; insertions: number; deletions: number }[]; // at most 200
    }
  | {
      kind: 'evidence';
      items: {
        command: string;
        exitCode: number;
        durationMs: number;
        summary: string;
      }[]; // at most 50
    };

Each artifact is at most 64 KiB as JSON; past that, perFile and items are cut to fit and metadata.truncated is true. There is no branch artifact: branch names and commit ids do not leave the host (§8.8).