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).