Skip to content

Latest commit

 

History

History
262 lines (181 loc) · 10.9 KB

File metadata and controls

262 lines (181 loc) · 10.9 KB

Courier Implementer Guide

Dispatch is defined by three layers:

  • Agentfile as 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.

Courier Contract

The reference contract lives in CourierBackend.

Required responsibilities:

  • capabilities() reports what the courier can do
  • validate_parcel() checks whether this courier can execute the parcel's declared courier reference
  • inspect() returns non-mutating courier metadata for the parcel
  • open_session() creates a fresh courier session bound to the parcel digest
  • run() executes one operation against one session and returns ordered courier events

Compatibility Rules

validate_parcel() is the compatibility gate.

Minimum expectations:

  • it must reject parcels whose courier.reference is incompatible with the courier implementation
  • it must reject parcels whose $schema URL or format_version it 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_version values they support
  • list the courier.reference families 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.

Session Rules

open_session() creates a courier-owned session record.

Minimum expectations:

  • session.parcel_digest must match the loaded parcel digest
  • session.turn_count starts at 0
  • session.history starts empty unless the courier explicitly restores persisted state outside this API
  • session.entrypoint should reflect the parcel entrypoint when one is declared

run() must reject requests where the provided session is not bound to the loaded parcel.

Operation Rules

Current operations:

  • ResolvePrompt
  • ListLocalTools
  • InvokeTool
  • Chat
  • Job
  • Heartbeat

Minimum expectations:

  • ResolvePrompt and ListLocalTools are parcel-inspection helpers, not turn-execution operations
  • ResolvePrompt returns the resolved prompt stack from packaged instruction files
  • ListLocalTools returns the declared local tool list from the parcel manifest
  • InvokeTool executes one declared local tool or rejects the request if unsupported
  • Chat, Job, and Heartbeat must 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.

Event Rules

Each run() call returns ordered courier events.

Minimum expectations:

  • successful turns should end with CourierEvent::Done
  • tool execution should emit ToolCallStarted before ToolCallFinished
  • 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.

Inspection Rules

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.

Tool Execution Rules

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.

Conformance Tests

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 output
  • dispatch 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 command
  • courier_id - the courier implementation id returned by inspection
  • kind - the courier kind enum, such as native, docker, wasm, or plugin
  • checks[].name - stable check identifier for one contract assertion
  • checks[].passed - whether the check passed
  • checks[].skipped - whether the check was intentionally skipped because the courier kind does not support that fixture
  • checks[].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:

  1. Build or install the courier exactly as operators will invoke it.
  2. Run dispatch courier inspect <name> and fail if inspection itself errors.
  3. Run dispatch courier conformance <name> --json.
  4. Treat any non-zero exit code as a release blocker.
  5. 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_version values
  • 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

Practical Guidance

  • keep the parcel format portable and courier-agnostic
  • treat courier.reference as 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

Compatibility Claims

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 conformance against Dispatch v0.x.y"
  • "supports dispatch/native parcels for chat, job, and heartbeat, but not dispatch/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

WASM Host Model Calls

For dispatch/wasm, hosted-model access remains a host responsibility even when the guest initiates the request through the WIT ABI.

Minimum expectations:

  • model-complete should use the parcel's declared MODEL and FALLBACK policy 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