Dispatch is defined by three layers:
Agentfileas the authored build language- the built parcel manifest and packaged context as the portable artifact
- the courier contract as the execution boundary
If you are implementing a Dispatch-compatible courier, the courier contract matters more than the CLI. Compatibility means your courier can load a Dispatch parcel, validate whether it can execute it, and honor the expected session and operation semantics.
This guide is the contract-facing companion to schema-compatibility.md. External couriers should treat both documents as release inputs: one defines which parcel manifests are valid to load, the other defines what it means to execute them compatibly.
The reference contract lives in CourierBackend.
Required responsibilities:
capabilities()reports what the courier can dovalidate_parcel()checks whether this courier can execute the parcel's declared courier referenceinspect()returns non-mutating courier metadata for the parcelopen_session()creates a fresh courier session bound to the parcel digestrun()executes one operation against one session and returns ordered courier events
validate_parcel() is the compatibility gate.
Minimum expectations:
- it must reject parcels whose
courier.referenceis incompatible with the courier implementation - it must reject parcels whose
$schemaURL orformat_versionit does not support - it must not mutate the parcel or session state
- it should fail before execution, not halfway through a turn
The Dispatch CLI calls validate_parcel() before opening a session.
Couriers should also make their compatibility scope explicit:
- list the schema URLs and
format_versionvalues they support - list the
courier.referencefamilies they support - reject parcels outside that supported matrix before execution starts
If your courier only supports a subset of Dispatch parcels, that is acceptable as long as the rejection is explicit and deterministic.
open_session() creates a courier-owned session record.
Minimum expectations:
session.parcel_digestmust match the loaded parcel digestsession.turn_countstarts at0session.historystarts empty unless the courier explicitly restores persisted state outside this APIsession.entrypointshould reflect the parcel entrypoint when one is declared
run() must reject requests where the provided session is not bound to the loaded parcel.
Current operations:
ResolvePromptListLocalToolsInvokeToolChatJobHeartbeat
Minimum expectations:
ResolvePromptandListLocalToolsare parcel-inspection helpers, not turn-execution operationsResolvePromptreturns the resolved prompt stack from packaged instruction filesListLocalToolsreturns the declared local tool list from the parcel manifestInvokeToolexecutes one declared local tool or rejects the request if unsupportedChat,Job, andHeartbeatmust reject mismatched parcel entrypoints
Couriers are allowed to reject operations they do not support, but they should do so explicitly with an unsupported-operation error.
Each run() call returns ordered courier events.
Minimum expectations:
- successful turns should end with
CourierEvent::Done - tool execution should emit
ToolCallStartedbeforeToolCallFinished - prompt resolution should emit
PromptResolved - local tool enumeration should emit
LocalToolsListed - couriers that fall back from one execution path to another should surface that as an explicit event when possible
Dispatch currently preserves event ordering and, for external courier plugins,
streams courier events incrementally as JSON-RPC 2.0 notifications framed as
line-delimited JSON on stdio. The terminal done result for a run() call is
returned as the final JSON-RPC success response for that request.
For external courier plugins, Dispatch keeps one long-lived subprocess per open
session and sends subsequent run requests over the same stdio stream.
inspect() should be safe and non-mutating.
Minimum expectations:
- report courier id and kind
- report parcel entrypoint
- report required secrets
- report declared mounts
- report declared local tools
Inspection should not require opening a courier session.
For couriers that execute packaged local tools:
- only declared parcel tools may be exposed to the model or to operator-facing inspection commands
- only declared local tools may be invoked
- required secrets must be enforced before execution
- couriers should avoid forwarding their full ambient environment to tools
- tool execution results should preserve stdout, stderr, exit code, command, and args
The reference native courier clears the child environment and only forwards a minimal system environment plus declared ENV and declared SECRET values.
The public conformance skeleton lives in:
The current suite checks:
- courier/parcel compatibility validation
- session binding to parcel digests
- built-in mount resolution on
open_session() - prompt resolution behavior
- local tool listing behavior
- conditional chat execution
- conditional job execution
- conditional heartbeat execution
- conditional direct local tool invocation
- conditional A2A tool invocation through card discovery and expected agent identity
- explicit rejection of unsupported execution in stub couriers
If you are building a new courier, these tests are the minimum target. Add courier-specific tests in your own crate, but keep the shared public contract passing.
The Dispatch CLI also exposes a generated harness:
dispatch courier conformance <name>
That command builds temporary fixture parcels, runs the shared contract checks against the selected built-in or installed courier, and reports pass/fail per check. Use it as the quickest operator-facing validation pass before you wire courier-specific tests into your own CI.
Useful forms:
dispatch courier conformance <name>- human-readable pass/fail outputdispatch courier conformance <name> --json- machine-readable report for CI artifacts
Current JSON report shape:
{
"courier": "native",
"courier_id": "dispatch-native",
"kind": "native",
"checks": [
{
"name": "validate-compatible",
"passed": true,
"skipped": false,
"detail": "dispatch/native:latest"
}
]
}Field meanings:
courier- the operator-facing courier name passed to the commandcourier_id- the courier implementation id returned by inspectionkind- the courier kind enum, such asnative,docker,wasm, orpluginchecks[].name- stable check identifier for one contract assertionchecks[].passed- whether the check passedchecks[].skipped- whether the check was intentionally skipped because the courier kind does not support that fixturechecks[].detail- short human-readable context for the check result
Stability guidance:
- top-level field names and check object field names are intended to stay stable across patch releases within the same Dispatch minor release line
- new checks may be added in later Dispatch releases, so CI consumers should tolerate additional
checks[]entries - compare reports by
checks[].name, not by array position
Recommended CI pattern for external couriers:
- Build or install the courier exactly as operators will invoke it.
- Run
dispatch courier inspect <name>and fail if inspection itself errors. - Run
dispatch courier conformance <name> --json. - Treat any non-zero exit code as a release blocker.
- Archive the JSON report so regressions can be compared across releases.
Suggested release checklist for a courier claiming Dispatch compatibility:
- pass the shared conformance suite for every supported backend mode
- document the supported schema URLs and
format_versionvalues - document any unsupported operations or mount types
- document any extra runtime requirements such as Docker, WASM host support, or network access
- add courier-specific tests for isolation, persistence, auth, and performance characteristics not covered by the shared suite
The shared conformance suite is intentionally a floor, not a ceiling.
What the shared suite does not prove:
- sandbox strength or isolation guarantees
- performance, timeout, or scaling behavior under load
- provider-specific hosted-model correctness
- organization-specific trust policy or secret handling requirements
- every courier-specific feature you may add beyond the Dispatch core contract
- keep the parcel format portable and courier-agnostic
- treat
courier.referenceas a compatibility declaration, not just a label - avoid depending on CLI-only behavior for correctness
- prefer explicit unsupported-operation errors over silent no-ops
- keep inspection and validation cheap and deterministic
- pin the Dispatch release range and schema versions you have validated instead of assuming forward compatibility
Use precise language when you document or market an external courier.
Good compatibility claims look like:
- "supports Dispatch parcel schema
https://serenorg.github.io/dispatch/schemas/parcel.v1.json" - "passes
dispatch courier conformanceagainst Dispatch v0.x.y" - "supports
dispatch/nativeparcels forchat,job, andheartbeat, but notdispatch/wasm"
Avoid vague claims like "supports Dispatch" unless you also say:
- which schema versions you load
- which courier families you execute
- which operations you support
- which conformance suite version or Dispatch release you tested against
Dispatch does not require Docker, WASM, or any other execution engine. Those are implementation choices behind the courier boundary.
For the built-in Docker courier specifically, the current boundary is:
- Dispatch keeps session history, mounts, and hosted-model orchestration on the host
- declared local tools run inside Docker as the isolated execution surface
- the parcel is not currently executed as a full in-container agent runtime
For dispatch/wasm, hosted-model access remains a host responsibility even when the guest initiates the request through the WIT ABI.
Minimum expectations:
model-completeshould use the parcel's declaredMODELandFALLBACKpolicy unless the guest explicitly requests a model id- guest-supplied model ids should be treated as model selection within the host's configured provider policy, not as authority to switch to arbitrary providers
- if the primary hosted-model request fails before producing a reply, the courier should try declared fallback models in order
- prompt resolution, declared tool exposure, and memory access remain host-owned even when the guest orchestrates the turn