Dispatch packages agents into verifiable parcels and runs them through pluggable couriers.
The core idea: an agent should be a self-describing, verifiable artifact - separate from the infrastructure that runs it. Build it once. Run it anywhere a courier exists.
dispatch.toml -> dispatch parcel build -> parcel (artifact) -> dispatch run -> courier
dispatch.toml defines a Dispatch project. It either defines an agent under [agent] or references a built parcel. Its other fields and tables configure deployment. A parcel is the built artifact. A courier executes a parcel. A session is an active run.
Available couriers today include native, Docker, WASM, and external plugins that speak JSON-RPC 2.0 over line-delimited JSON on stdio. The WASM path runs a guest component compiled against the Dispatch WIT ABI in any host that implements the interface - local machine, cloud worker, edge node, or multi-tenant platform - with no container daemon required and with WebAssembly isolation by default.
Most agent "frameworks" solve the programming problem. Dispatch solves the packaging problem.
Without a standard artifact format:
- an agent's prompt, tools, model policy, and security constraints live in ad-hoc code
- the author and the executor must share runtime assumptions
- deploying to a new environment means rewriting configuration
- verifying that what runs matches what was authored is manual or impossible
With Dispatch:
- the agent definition in
dispatch.tomlis the canonical authored source - human-editable, diff-friendly, reviewable dispatch parcel buildproduces a content-addressed parcel with a verifiable manifestdispatch parcel verifyre-hashes every file and checks detached signaturesdispatch runselects a courier backend and executes - the parcel carries its contract with it- couriers can be native, Docker, WASM, or custom; the parcel format is independent of which one runs it
The practical applications: deploying untrusted third-party agents in a sandboxed WASM host, running agents at the edge without a container runtime, distributing agents through a depot network with integrity guarantees, and letting authors declare model policy and tool permissions explicitly rather than via ambient prompt text.
- Dispatch compiles an agent definition into a parcel
- a parcel is described by
manifest.json parcel.lockrecords parcel integrity metadata- a courier executes a parcel
- a depot stores parcels
- a running parcel execution is a session
- the resolved prompt stack is the parcel's brief to the model
An agent is defined under [agent] in dispatch.toml. This agent definition plays the same role as a Dockerfile in Docker. It is declarative, diff-friendly, and strictly typed.
An agent project has:
- a
dispatch.tomlwith an[agent]table - optional instruction files loaded into the agent's prompt stack:
| File | Purpose |
|---|---|
IDENTITY.md |
Name, role, and display metadata |
SOUL.md |
Persona, tone, writing style, and behavioral invariants |
SKILL.md |
What the agent does and how to approach tasks |
AGENTS.md |
Operating procedures: tool discipline, memory discipline, scope rules |
USER.md |
Operator context: timezone, preferences, access boundaries |
TOOLS.md |
When and how to use each declared tool |
MEMORY.md |
Memory policy: what to store, when, and in what format |
HEARTBEAT.md |
Procedures to execute on each scheduled run |
- optional Agent Skills bundles referenced in
agent.skills - an explicit
agent.componentfordispatch/wasmparcels - local tools, reference assets, evals, and code
Example (examples/parcels/basic/dispatch.toml):
[agent]
courier_reference = "dispatch/native:latest"
name = "basic-assistant"
version = "0.1.0"
entrypoint = "chat"
visibility = "open"
evals = ["evals/smoke.eval"]
[agent.instructions]
identity = "IDENTITY.md"
soul = "SOUL.md"
skill = "SKILL.md"
agents = "AGENTS.md"
user = "USER.md"
tools = "TOOLS.md"
memory = "MEMORY.md"
[agent.model]
id = "gpt-5.6-luna"
provider = "openai"
[[agent.model.fallbacks]]
id = "claude-sonnet-4-6"
provider = "anthropic"
[agent.env]
TZ = "UTC"
[[agent.secrets]]
name = "PLANNER_TOKEN"
[[agent.tools]]
kind = "builtin"
name = "web_search"
[[agent.tools]]
kind = "builtin"
name = "memory_put"
[[agent.tools]]
kind = "a2a"
alias = "planner"
url = "https://planner.example.com"
discovery = "card"
expect_agent_name = "planner-agent"
expect_card_sha256 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
description = "Delegate planning to a remote agent."
[agent.tools.auth]
scheme = "bearer"
secret_name = "PLANNER_TOKEN"
[[agent.mounts]]
kind = "session"
driver = "sqlite"
[[agent.mounts]]
kind = "memory"
driver = "sqlite"
[[agent.mounts]]
kind = "artifacts"
driver = "local"
[agent.limits]
iterations = 20
tool_calls = 12
tool_rounds = 8
tool_output = 10000
context_tokens = 16000
[agent.timeouts]
run = "300s"
tool = "60s"
llm = "120s"
[agent.compaction]
interval = "200"
overlap = 32A2A tool endpoints are declared in the parcel, and discovered agent cards are allowed to refine the RPC path but not pivot execution onto a different origin than the declared URL. Dispatch requires https:// for non-loopback A2A endpoints and rejects URLs with embedded credentials; plain http:// is only accepted for loopback development targets such as localhost or 127.0.0.1. Operators can still constrain outbound calls at runtime with DISPATCH_A2A_ALLOWED_ORIGINS, using a comma-separated list of allowed origins or hostnames, or with DISPATCH_A2A_TRUST_POLICY, a TOML policy file that can match by origin/hostname and require discovered agent-card identity fields such as expected_agent_name and expected_card_sha256. Command-scoped CLI A2A policy flags override inherited environment values for that one invocation without mutating the process environment. The current A2A tool contract is synchronous: Dispatch will poll tasks/get for unfinished remote tasks until completion or the configured tool timeout. For the full declaration and operator model, see docs/a2a.md.
The run timeout is enforced as a persisted pre-turn session budget using accumulated elapsed runtime across successful runs and resumes. It does not currently preempt a turn that has already started.
The tool timeout is currently enforced for host-executed local tools and host-executed A2A tool calls.
Declared secret values resolve from the environment first and then fall back to a repo-local encrypted store under .dispatch/secrets. Use dispatch secret init . once per project, dispatch secret set NAME --value ... or --value-stdin to store values locally, dispatch secret ls to inspect stored names, and dispatch secret rm NAME to remove one without printing plaintext back to the terminal.
CLI-scoped A2A operator policy overrides are available on:
dispatch rundispatch parcel evaldispatch courier conformance
Use --a2a-allowed-origins ... and --a2a-trust-policy ... when you want command-scoped A2A policy without exporting environment variables.
Hosted model backends also receive agent.timeouts.llm as an HTTP request timeout when the parcel declares it.
Timeout durations must be positive integers ending in ms, s, m, or h.
Dispatch supports the Agent Skills specification as a first-class skill packaging layout.
Use agent.instructions.skill for one standalone markdown instruction document. Use the agent.skills array for Agent Skills bundle directories. In each bundle directory, Dispatch expects:
SKILL.mdfor the skill instructions- an optional
skill.tomlsidecar for Dispatch-executable tool metadata - the rest of the Agent Skills bundle layout such as
scripts/,references/, andassets/
SKILL.md stays Agent Skills compliant. Dispatch-specific execution metadata lives in skill.toml, or in a sidecar path referenced by metadata.dispatch-manifest in the skill frontmatter.
skill.toml is a reserved filename inside skill directories: if it exists, Dispatch will try to load it as the sidecar unless frontmatter points at a different file.
If you only want to work with a skill locally, dispatch skill validate <path> checks that Dispatch can synthesize a parcel from a SKILL.md file or skill bundle directory, and dispatch skill run <path> executes that synthesized parcel without requiring an authored [agent] table.
Example skill bundle:
skills/file-analyst/
|-- SKILL.md
|-- skill.toml
|-- scripts/
| |-- read_file.sh
| \-- find_files.sh
|-- schemas/
| \-- find_files.json
\-- references/
\-- REFERENCE.md
Example dispatch.toml:
[agent]
courier_reference = "dispatch/native:latest"
name = "file-analyst-agent"
entrypoint = "chat"
skills = ["skills/file-analyst"]
[agent.instructions]
soul = "SOUL.md"
[agent.model]
id = "claude-sonnet-4-6"
provider = "anthropic"Example skill.toml sidecar:
entrypoint = "chat"
[[tools]]
name = "read_file"
script = "scripts/read_file.sh"
risk = "low"
description = "Read the full contents of a file."
[[tools]]
name = "find_files"
script = "scripts/find_files.sh"
schema = "schemas/find_files.json"
risk = "low"
description = "Find files matching a pattern."Dispatch packages the whole skill directory, strips SKILL.md frontmatter out of the prompt text seen by the model for directory-based skill bundles, and synthesizes the sidecar tool declarations into the parcel manifest as normal local tools. A instructions.skill file path is left unchanged even if they happen to contain YAML frontmatter. The built parcel preserves skill annotations such as allowed-tools as structured lists, and skill-generated tools retain skill_source provenance using the skill's canonical name. skill.toml may also provide a default entrypoint, but an explicit entrypoint in the [agent] table still wins. Explicit [[agent.tools]] declarations override skill-generated tool aliases, but duplicate explicit aliases and conflicting aliases across skills fail the build.
allowed-tools is currently preserved as informational metadata for interoperability and downstream policy engines. The reference courier does not enforce it yet, but dispatch parcel lint and dispatch parcel build warn when a skill's allowed-tools entries do not line up with synthesized or declared tool aliases.
Dispatch includes a WASM courier for parcels that package a guest component targeting the Dispatch WIT ABI.
A dispatch/wasm parcel contains a WASM component compiled against the Dispatch WIT ABI:
// dispatch:courier@0.0.1 - full definition in crates/dispatch-wasm-abi/wit/
interface host {
model-complete: func(request: model-request) -> result<model-response, string>;
invoke-tool: func(invocation: tool-invocation) -> result<tool-result, string>;
memory-get: func(namespace: string, key: string) -> result<option<memory-entry>, string>;
memory-put: func(namespace: string, key: string, value: string) -> result<bool, string>;
memory-delete: func(namespace: string, key: string) -> result<bool, string>;
memory-list: func(namespace: string, prefix: option<string>) -> result<list<memory-entry>, string>;
}The guest component implements open-session and handle-operation. The host owns:
- model routing - the guest can request a model ID, but provider and API key selection come from the parcel manifest and host environment
- tool execution - the host invokes declared local tools; the guest cannot access tools outside the parcel manifest
- memory - the host provides durable parcel-scoped sqlite storage; the guest sees it as a namespace/key/value API
- sandboxing - WASM memory isolation applies by default; the guest cannot access host resources unless the host imports them
This separation enables:
- running untrusted third-party agent components with bounded resource access
- edge and serverless deployment with no container daemon
- multi-tenant agent execution on a shared host
- auditing what a guest can actually do based on the parcel manifest and WIT imports, not inferred from prompt text
The dispatch-wasm-guest-reference crate shows how to build a guest component with multi-round tool calling, previous_response_id chain management, and session state. Any language that compiles to WASM with WIT component support can implement a guest.
The reference WASM courier keeps a bounded in-process component cache keyed by component SHA256. Override the cache size with DISPATCH_WASM_COMPONENT_CACHE_SIZE if you need a smaller or larger warm set.
The commands below assume dispatch is installed and on your PATH. For local development from this repository, install the CLI with cargo install --path crates/dispatch-cli --locked.
Build and run the reference examples:
# Lint an agent config
dispatch parcel lint examples/parcels/basic
dispatch parcel lint examples/parcels/wasm-reference
dispatch parcel lint examples/skills/file-analyst
# Build a parcel
dispatch parcel build examples/parcels/basic
dispatch parcel build examples/parcels/wasm-reference
dispatch parcel build examples/skills/file-analyst
# Run packaged evals
dispatch parcel eval examples/parcels/basic
dispatch parcel eval examples/parcels/basic --courier native
dispatch parcel eval examples/skills/file-analyst --courier native
# Inspect a built parcel
dispatch parcel inspect examples/parcels/basic/.dispatch/parcels/<digest>
dispatch parcel inspect examples/parcels/wasm-reference/.dispatch/parcels/<digest> --courier wasm
# Verify parcel integrity
dispatch parcel verify examples/parcels/basic/.dispatch/parcels/<digest>
# Sign a parcel
dispatch parcel keygen --key-id release --output-dir .dispatch/keys
dispatch parcel sign examples/parcels/basic/.dispatch/parcels/<digest> --secret-key .dispatch/keys/release.dispatch-secret.json
dispatch parcel verify examples/parcels/basic/.dispatch/parcels/<digest> --public-key .dispatch/keys/release.dispatch-public.json
# Run a parcel (native courier, requires LLM_API_KEY or provider env vars)
dispatch run examples/parcels/basic/.dispatch/parcels/<digest> --chat "hello"
dispatch run examples/parcels/basic/.dispatch/parcels/<digest> --interactive
# Run a skill bundle directly without authoring an agent config
dispatch skill validate examples/skills/file-analyst/skills/file-analyst
dispatch skill run examples/skills/file-analyst/skills/file-analyst --list-tools
dispatch skill run examples/skills/file-analyst/skills/file-analyst --model gpt-5.6-luna --provider openai --chat "Summarize this repository."
# Run a WASM parcel
dispatch run examples/parcels/wasm-reference/.dispatch/parcels/<digest> --courier wasm --chat "hello"
# Run a heartbeat
dispatch run examples/parcels/heartbeat-monitor/.dispatch/parcels/<digest> --heartbeat
# Test the Codex backend (requires `codex app-server` access)
dispatch parcel lint examples/parcels/codex
dispatch parcel build examples/parcels/codex
dispatch run examples/parcels/codex/.dispatch/parcels/<digest> --chat "Say hello in one sentence."
dispatch run examples/parcels/codex/.dispatch/parcels/<digest> --interactive
# List and invoke tools
dispatch run examples/parcels/heartbeat-monitor/.dispatch/parcels/<digest> --list-tools
dispatch run examples/parcels/heartbeat-monitor/.dispatch/parcels/<digest> --tool poll_mentions
# Push/pull to a depot
dispatch depot push examples/parcels/basic/.dispatch/parcels/<digest> file:///tmp/dispatch-depot::acme/basic:0.1.0
dispatch depot pull file:///tmp/dispatch-depot::acme/basic:0.1.0
dispatch depot push examples/parcels/basic/.dispatch/parcels/<digest> file:///tmp/dispatch-depot::acme/basic:0.1.0 --json
dispatch depot pull file:///tmp/dispatch-depot::acme/basic:0.1.0 --json
dispatch depot push examples/parcels/basic/.dispatch/parcels/<digest> https://depot.example.com::acme/basic:0.1.0
dispatch depot pull https://depot.example.com::acme/basic:0.1.0
dispatch depot pull https://depot.example.com::acme/basic:0.1.0 --public-key .dispatch/keys/release.dispatch-public.json
dispatch depot pull https://depot.example.com::acme/basic:0.1.0 --trust-policy trust-policy.toml
# Docker-style aliases for parcel operations
dispatch build examples/parcels/basic
# `dispatch images` is shorthand for `dispatch image ls`
dispatch images examples/parcels/basic
dispatch image ls examples/parcels/basic
dispatch image build examples/parcels/basic
dispatch image inspect examples/parcels/basic/.dispatch/parcels/<digest>
dispatch image push examples/parcels/basic/.dispatch/parcels/<digest> file:///tmp/dispatch-depot::acme/basic:0.1.0
dispatch image pull file:///tmp/dispatch-depot::acme/basic:0.1.0Long-lived runtime examples:
# Run directly from a source directory and let Dispatch build/resolve the parcel
dispatch run examples/parcels/codex --chat "Say hello in one sentence."
# List locally built parcels
dispatch parcel list examples/parcels/basic
dispatch images examples/parcels/basic
# Start a detached heartbeat/job run
dispatch run examples/parcels/heartbeat-monitor --heartbeat --detach
# Start a long-lived service run for a heartbeat parcel
dispatch serve examples/parcels/heartbeat-monitor --detach
dispatch serve examples/parcels/heartbeat-monitor --schedule "*/5 * * * * * *" --detach
# Inspect and manage long-lived runs
dispatch ps examples/parcels/heartbeat-monitor
dispatch inspect-run <run-id> examples/parcels/heartbeat-monitor --json
dispatch logs <run-id> examples/parcels/heartbeat-monitor --follow
dispatch wait <run-id> examples/parcels/heartbeat-monitor
dispatch stop <run-id> examples/parcels/heartbeat-monitor
dispatch restart <run-id> examples/parcels/heartbeat-monitor
dispatch rm <run-id> examples/parcels/heartbeat-monitor
# Docker-style aliases for the same run-management surface
dispatch container ls examples/parcels/heartbeat-monitor
dispatch container logs <run-id> examples/parcels/heartbeat-monitorPrint the parsed AST:
dispatch parcel lint examples/parcels/basic --jsonA built parcel contains:
manifest.json- typed parcel manifest with$schemapointerparcel.lock- file and digest integrity metadatacontext/- packaged build content referenced by the[agent]tablesignatures/<key_id>.json- detached Ed25519 signatures (optional)
The manifest is described by schemas/parcel.v2.json, published at https://serenorg.github.io/dispatch/schemas/parcel.v2.json.
Schema publication and compatibility policy live in docs/schema-compatibility.md.
Packaged eval files live under context/ with the other authored inputs. A minimal eval file looks like:
name = "smoke"
input = "What time is it?"
expects_tool = "system_time"
expects_text_contains = "plugin reply"Eval files can also group multiple cases:
[[cases]]
name = "smoke"
input = "What time is it?"
expects_tool = "system_time"
[[cases]]
name = "exact"
input = "What time is it?"
expects_tool_count = 1
expects_tool_stdout_contains = { tool = "system_time", contains = "2026-04-03" }
expects_text_exact = "plugin reply"dispatch parcel eval runs packaged agent.evals cases and agent.tests tool smoke checks against a live courier and reports pass/fail per case.
Tool result assertions can be either a plain value or a tool-scoped object, so multi-tool evals can target one tool explicitly.
expects_no_tool = true can be used for cases that should complete without invoking any tool.
expects_tool_stdout_matches_schema validates JSON stdout from a tool against a packaged JSON schema file, and expects_a2a_endpoint asserts that an A2A tool alias resolved to the expected declared endpoint.
For larger regression suites, keep the assertions inside packaged eval files and fan them out with a repo-local dataset:
version = 1
[[cases]]
name = "utc-smoke"
source = "evals/smoke.eval"
case = "smoke"
input = "What time is it in UTC?"Run it with dispatch parcel eval . --dataset evals/regression.dataset.toml. Dataset cases keep the packaged eval assertions and only override the input plus an optional entrypoint.
Add --trace-dir .dispatch/traces to persist one structured JSON trace per eval/test case under .dispatch/traces/evals/<parcel-digest>/.
Parcel format compatibility:
load_parcelvalidatesmanifest.jsonagainst the bundled Dispatch JSON Schema before parsing- the reference implementation supports exactly
format_version: 2 - couriers must reject parcels whose
$schemaorformat_versionthey do not support - published schema URLs are immutable; new manifest-shape changes require a new schema URL and
format_version
verify behavior:
- recomputes the parcel manifest digest from normalized manifest content
- validates
parcel.lockdigest, layout metadata, and file list - re-hashes every packaged file under
context/ - optionally verifies detached Ed25519 signatures with
--public-key <path> - fails if packaged files are missing or modified
The native courier runs the parcel directly on the local machine as a host process with a model-backed chat loop.
Model backend selection:
- if
agent.modeldeclaresidandprovider, that provider is used - if no parcel-level provider,
LLM_BACKENDselects the backend:openai,anthropic,claude,gemini,openai_compatible,codex agent.model.fallbacksentries are tried in order when the primary backend fails before producing a reply
Supported backends:
| Backend | API | Environment |
|---|---|---|
openai |
OpenAI Responses API | OPENAI_API_KEY |
anthropic |
Anthropic Messages API | ANTHROPIC_API_KEY |
claude |
claude CLI |
local claude auth; optional CLAUDE_BINARY, DISPATCH_PERSIST_THREAD, DISPATCH_REASONING_EFFORT (low, medium, high, max) |
gemini |
Gemini generateContent | GEMINI_API_KEY or GOOGLE_API_KEY |
openai_compatible |
Chat Completions | LLM_API_KEY + LLM_BASE_URL |
codex |
Local codex app-server JSON-RPC transport |
optional CODEX_BINARY, CODEX_HOME, DISPATCH_PERSIST_THREAD, DISPATCH_REASONING_EFFORT (value passed through to Codex; typically low, medium, or high) |
LLM_API_KEY and LLM_BASE_URL take precedence over provider-specific vars. Provider-specific vars (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) are checked as fallbacks when the LLM_* vars are not set.
claude uses the local claude CLI instead of a hosted HTTP API. Authentication is handled entirely by the local claude binary using whatever login, config, or environment-based credentials it already supports; Dispatch does not preflight or inject API keys for this backend. Dispatch does not load CLAUDE.md or Claude settings files from the working directory, which keeps the execution context explicit. By default Dispatch persists Claude session state and resumes that session on later turns. Parcel authors can set agent.model.provider = "claude" and agent.model.options.persist-thread = "false" to request ephemeral sessions, and can set agent.model.options.reasoning-effort = "high" at the parcel level. Dispatch validates Claude effort overrides against the documented CLI values low, medium, high, and max before invoking the local binary. DISPATCH_PERSIST_THREAD remains the operator override and takes precedence over the parcel setting. Set CLAUDE_BINARY to override the path to the claude executable (default: claude on PATH). To preserve Dispatch's capability boundary, ambient Claude tool actions are denied unless Dispatch grows an explicit tool bridge for them.
codex uses the local codex app-server process instead of a hosted HTTP API. Dispatch starts a fresh app-server process per model call, but by default it persists Codex thread state and resumes that thread on later turns when --session-file or interactive session state is present. Parcel authors can set agent.model.provider = "codex" and agent.model.options.persist-thread = "false" to request ephemeral Codex threads, and can set agent.model.options.reasoning-effort = "high" at the parcel level. DISPATCH_PERSIST_THREAD remains the operator override and takes precedence over the parcel setting. If neither the parcel nor the env sets a reasoning effort, Dispatch omits the override and lets Codex use the selected model's default effort. Dispatch leaves Codex using its normal home/config/auth location unless the environment already overrides CODEX_HOME. When persistence is disabled, no Codex rollout files are saved and follow-up context comes from Dispatch session history instead. To preserve Dispatch's capability boundary, app-server permission requests are denied by default in this backend, so ambient Codex command/file/MCP actions are not available unless Dispatch grows an explicit tool bridge for them. This backend intentionally preserves the user's Codex auth/config for both modes. On Unix the reference implementation uses a PTY-backed transport for Codex; other targets currently fall back to plain process pipes.
run flags:
--interactive- multi-turn chat session in the terminal--session-file <path>- persist and resume session state across invocations--chat <text>- single chat turn--job <payload>- execute the parceljobentrypoint--heartbeat [payload]- execute the parcelheartbeatentrypoint--print-prompt- resolve and print the parcel's brief to the model--list-tools- list declared tools--json- when combined with--list-tools, print full tool metadata as JSON--tool <name>- execute one declared local tool--tool-approval <ask|always|never>- control how tools withapproval = "confirm"are handled at the CLI/prompt,/tools,/help- handled locally during interactive sessions
Dispatch also has a local long-lived runtime for detached execution and always-on heartbeat services.
dispatch run --detachstarts a detachedjoborheartbeatrun and writes a run record under.dispatch/runs/- detached helpers publish authoritative terminal snapshots so
wait,ps, andinspect-runcan reconcile exited runs without guessing from dead pids dispatch servestarts a long-livedservicerun that can wake from heartbeat intervals, persisted cron schedules, and local HTTP ingressdispatch ps,dispatch logs,dispatch wait,dispatch stop,dispatch restart,dispatch prune,dispatch rm, anddispatch inspect-runmanage those runsdispatch build,dispatch inspect,dispatch pull,dispatch push, anddispatch imagesexpose Docker-style top-level aliases for common parcel operationsdispatch imagesis shorthand fordispatch image lsdispatch image ...exposes Docker-style aliases for parcel artifact management commandsdispatch container ...exposes Docker-style aliases for the same run management commands
dispatch serve currently requires agent.entrypoint = "heartbeat".
Service scheduling and ingress can be authored into the parcel:
agent.schedules = ["<cron>"]agent.listeners = ["127.0.0.1:0"]agent.ingress.path = "/hook"agent.ingress.methods = ["POST"]agent.ingress.secret_env = "DISPATCH_WEBHOOK_SECRET"agent.ingress.max_body_bytes = 8192agent.ingress.max_header_bytes = 4096
CLI dispatch serve flags can also provide or override schedules, listeners, and ingress policy at runtime. For the full runtime contract, see docs/runtime-and-serve.md.
dispatch wait prints the run exit code. Detached one-shot runs that complete normally return 0; explicitly stopped runs and one-shot runs whose helper dies before recording terminal state return non-zero.
dispatch skill validate and dispatch skill run are convenience wrappers over the same build path. They copy the referenced SKILL.md file or skill bundle into a temporary workspace, synthesize a minimal [agent] table, and run the same synthesis and parcel build that an authored one would use. dispatch skill validate stops after that build-time validation, while dispatch skill run then delegates to dispatch run. This means validate surfaces sidecar, frontmatter, packaging, and build errors directly and is suitable for CI, but it is intentionally heavier than a schema-only lint. The current shortcuts support built-in native and docker couriers and accept --model, --provider, and --entrypoint overrides for the synthesized parcel.
Dispatch defines the parcel format and courier contract. Multiple couriers can implement that contract:
- native - executes the parcel as a host process with model-backed chat; reference implementation in
crates/dispatch-core - docker - keeps session state, mounts, and model orchestration on the host, while running declared local tools inside Docker as an execution sandbox
- wasm - typed component-model courier using the Dispatch WIT ABI; see WASM Courier
- plugins - external courier plugins launched as subprocesses; protocol is JSON-RPC 2.0 messages framed as one JSON value per line on stdio in
docs/courier-plugin-protocol.md
The courier/plugin boundary lives in crates/dispatch-core/src/courier.rs.
Core traits and types:
CourierBackend- trait every courier backend implementsCourierSession- dispatch-owned session identity and turn stateCourierRequest/CourierResponse- courier operation envelopeCourierEvent- ordered event stream emitted per turnCourierCapabilities/CourierInspection- backend introspectionMountProvider,MountRequest,ResolvedMount- mount abstraction
For courier implementers:
docs/schema-compatibility.mddocs/courier-implementers.mddocs/courier-plugin-protocol.mdcrates/dispatch-core/tests/courier_conformance.rs
Courier registry:
The courier registry is host inventory, not parcel source. The agent definition remains the canonical parcel source. dispatch courier install records the runtime backends that are available on a machine.
For project-scoped runtime wiring, use dispatch up. The two halves of dispatch.toml stay separate: [agent] defines the parcel, while the deployment tables bind parcels to installed channels/couriers, declares managed deployment bindings, and reconciles extension manifests into project-local registries under .dispatch/registries/. Reply delivery through channel bindings requires a parcel; deliver_replies does not work on a channel-only runtime binding.
dispatch parcel lint|build|inspect|verify|keygen|sign- manage parcel sources, signatures, and built artifactsdispatch depot push|pull- move parcels to and from depotsdispatch courier ls- list built-in backends and installed pluginsdispatch courier inspect <name>- show courier metadatadispatch courier install <manifest>- install a plugin manifestdispatch courier conformance <name>- run the public courier contract checks against one backenddispatch courier conformance <name> --json- emit the same conformance report as machine-readable JSONdispatch run --courier <name>- select a backend by namedispatch run --registry <path>- use a non-default courier registrydispatch up [dispatch.toml]- reconcile project-local deployment/runtime bindings and start configured channel listeners/pollers- plugin installation records the executable SHA256; Dispatch checks that digest before each launch
See examples/runtime/telegram-bot/dispatch.toml for a concrete project-level runtime config example.
Third-party couriers and channels live in their own repositories. Dispatch discovers them through catalogs - JSON index documents at stable URLs, analogous to Homebrew taps. Each repo publishes its own catalog at catalog/extensions.json, users register the catalog URL once, and the entries become searchable locally.
# Register a catalog (one-time)
dispatch extension catalog add \
https://raw.githubusercontent.com/serenorg/dispatch-plugins/master/catalog/extensions.json
# Populate the local cache
dispatch extension catalog refresh
# Search and inspect
dispatch extension search telegram
dispatch extension show channel-telegramCatalogs are stored in ~/.config/dispatch/catalogs.toml and fetched JSON is cached under ~/.config/dispatch/catalog-cache/.
If a catalog entry publishes machine-installable source metadata, Dispatch can also install it directly by name:
dispatch extension install <name>The shipped install-by-name path is intentionally narrow: it supports direct GitHub release binaries and still rewrites the catalog's manifest into the existing dispatch courier install / dispatch channel install flow. Capability-based trust remains on the roadmap.
See docs/plugin-ecosystem.md for the full roadmap, including the canonical list of known third-party catalogs and guidance for publishing a new one.
State is not baked into the parcel. Sessions, memory, and artifacts are mounts declared in [[agent.mounts]].
kind = "session",driver = "sqlite"- session-scoped sqlite; persistsCourierSessionstate per turnkind = "memory",driver = "sqlite"- parcel-scoped sqlite; exposesmemory_get,memory_put,memory_delete,memory_listto model-backed turnskind = "artifacts",driver = "local"- parcel-scoped artifact storage
State layout:
- parcels opened from a normal build tree:
.dispatch/state/<digest>/ - parcels at custom locations:
<parcel-parent>/.dispatch-state/<digest>/ DISPATCH_STATE_ROOToverrides the state root completely
State management:
dispatch state ls- list digest-scoped state directoriesdispatch state gc- remove orphaned state for parcels no longer presentdispatch state migrate <old> <new>- copy state when a rebuilt parcel gets a new digest
dispatch depot push <parcel> <reference>- publish a parcel into a depotdispatch depot pull <reference>- resolve a tagged reference into.dispatch/parcels/dispatch depot push ... --json/dispatch depot pull ... --json- emit machine-readable depot resultsdispatch depot pull <reference> --public-key <path>- require detached signature verification during fetchdispatch depot pull <reference> --trust-policy <path>- apply pull-time trust rules during fetch- trust, provenance, and depot operator guidance live in
docs/trust-and-depots.md - v1 depot references include:
file:///absolute/path/to/depot::org/parcel:v1https://depot.example.com::org/parcel:v1- file depots store parcels by digest under
blobs/parcels/<digest>/ - file depots store tags under
refs/<org>/<parcel>/tags/<tag>.json - HTTP depots expose parcel blobs at
/v1/parcels/<digest>.tarand tag lookup at/v1/tags?repository=<repo>&tag=<tag> - set
DISPATCH_DEPOT_TOKENto sendAuthorization: Bearer <token>on HTTP depot requests - set
DISPATCH_TRUST_POLICYto apply a default pull-time trust policy without passing--trust-policy - trust policy files are TOML documents with
rules, optionalreference_prefix, optionalrepository_prefix,public_keys, and optionalrequire_signatures - each trust-policy rule must set at least one matcher:
reference_prefix,repository_prefix, or both - if a rule sets both prefixes, both must match for the rule to apply
- matching rules compose:
require_signaturesis enabled if any matching rule requires itpublic_keysfrom matching rules are merged and deduplicated
--public-keycomposes with--trust-policy; explicit keys are added to any matching policy keys- trust-policy verification happens before a pulled parcel is committed into the local parcel store
agent.frameworkmetadata is informational provenance, not a trust root; use signatures and trust policy for publisher authorization
- Agent definitions are declarative, typed, and human-editable.
- Dispatch owns the courier and parcel contract. The agent definition remains the authored source.
- State is not baked into the parcel. Sessions, memory, and artifacts are mounts.
- Tools are declared capabilities, not implicit filesystem accidents.
- A courier must not advertise or execute undeclared tools based on ambient prompt text.
- A built parcel should have a digest and be runnable by reference.
- Couriers must reject parcels whose format version or schema they do not support.
- Replacing Docker or OCI
- Hiding execution or security policy behind prompt text
- Treating agent memory as part of the immutable build artifact
- Requiring any specific agent framework or language runtime
Licensed under the MIT License. See LICENSE.