missive treats A2A as the canonical protocol layer. The current implemented
protocol behavior covers public Agent Card discovery for registered agents,
official Rust protocol type integration through missive-a2a, local interface
negotiation, non-streaming SendMessage calls for missive send and
missive bcast, SSE SendStreamingMessage calls for missive stream, remote
task GetTask/ListTasks/CancelTask, barrier GetTask polling for group
member tasks, local gather output/artifact collection from persisted A2A task
state, reduce prompts sent through A2A SendMessage when --reducer-agent is
used, and local A2A contextId continuity management through missive context.
When an A2A workflow fails, start with the non-mutating discovery path before retrying mutating sends or task changes:
missive doctor --json
missive agent inspect echo --refresh --json
missive agent inspect echo --binding http+json --json
missive agent capabilities echo --refresh --jsonUseful signals:
agent inspect --jsonshows the selected binding, interface URL, advertised protocol versions, capabilities, skills, and cache validators.- Exit code
76is reserved for protocol failures such as unsupported A2A version responses or malformed protocol payloads. - Exit code
77indicates auth material could not be resolved before the A2A request was sent. - Exit code
69usually indicates transport reachability or local gateway status failure rather than an A2A payload failure. missive events list --limit 50 --jsonshows recent redacted request/update history that reached local persistence; add an exact--typesuch asa2a.task.updatedora2a.push.status_updatewhen the event type is known.
For incident handling and recovery steps, see the runbook.
missive agent inspect <alias> and missive agent refresh <alias> resolve the
registered agent base_url to:
/.well-known/agent-card.json
The fetched JSON is parsed as an A2A Agent Card and cached on the agent registry row with:
- raw public Agent Card JSON
- fetch timestamp
- HTTP
ETag, when returned - HTTP
Last-Modified, when returned
inspect uses the cached card when present. inspect --refresh and refresh
perform a remote request. If cache validators are available, missive sends
If-None-Match and/or If-Modified-Since; 304 Not Modified keeps the cached
card and updates the cache validation timestamp.
The current parser deserializes into the official a2a-lf AgentCard type and
extracts the fields needed for human and machine inspection: provider, agent
version, protocol versions, capabilities, supported interfaces, default
input/output modes, and skills. The same public fields feed capability-aware
selection: missive agent capabilities, missive group capabilities, and
missive route explain --policy capability-match derive labels from skill ids,
skill names, skill tags, local metadata, input/output modes, streaming support,
and push notification support. Route explain reads cached cards by default and
updates the cache only when --refresh-capabilities is passed. A small
compatibility layer normalizes known snake_case fixture aliases and inserts an
empty supportedInterfaces array for older/pre-release cards that omit the
field, preserving the negotiation fallback introduced earlier. The raw public
card JSON remains cached and rendered for inspection.
missive-a2a now centralizes A2A service-parameter handling in a
ServiceParameters value. HTTP-based requests apply these parameters as headers:
A2A-Versionis sent on every implemented outbound A2A request. The default is the official SDKVERSION(1.0).A2A-Extensionsis sent when the selected config or CLI requests extensions; repeated values are rendered as a comma-separated header value.- Additional non-auth service parameters are accepted as validated HTTP header names and values.
The effective protocol defaults come from [protocol] or
[profiles.<name>.protocol] in missive.config.v1. For a single invocation,
--protocol-version <VERSION> overrides the configured version,
--a2a-extension <EXTENSION> appends an extension, and the
--service-param NAME=VALUE flag adds or overrides an extra service parameter.
Current implemented uses are Agent Card discovery/refresh, non-streaming
missive send, missive bcast, streaming missive stream, remote missive task
get/list/wait/cancel calls, barrier remote polling, reduce reducer-agent calls,
and push-config calls so request metadata remains consistent.
When an HTTP response body reports the official A2A
VERSION_NOT_SUPPORTED error code/reason, missive maps it to a protocol error
with deterministic protocol exit code 76 instead of a generic transport
failure.
ServiceParameters::to_metadata() records the version under
a2a.protocol_version, requested extensions under a2a.extensions, and extra
parameters under a2a.service_parameters; task/event persistence code should
copy that metadata when recording outbound request effects.
missive-a2a exposes an AuthHeaders request helper for resolved HTTP auth
headers. Raw values are not serializable, Debug renders redacted values, and
reqwest header values are marked sensitive before sending. The CLI currently
builds AuthHeaders from agent config auth refs, --bearer-token-env, and
repeatable --header Name:Value inputs for Agent Card fetch/refresh requests.
Auth resolution is deliberately outside protocol type parsing: config and CLI
code locate secrets in environment variables or platform keyrings, while the A2A
client only validates and applies already-resolved headers. missive send,
missive stream, missive bcast, missive barrier remote polling,
missive reduce --reducer-agent, remote missive task, and push operations use
the same auth headers for the optional Agent Card fetch and the A2A request.
missive agent inspect <alias> now computes a selected A2A interface from the
parsed Agent Card and the agent registry preference. The default local preference
order is:
http+json, json-rpc
Agent Card binding names are canonicalized for comparison, so HTTP+JSON,
JSONRPC, and lowercase config values all map to stable missive binding names.
gRPC is recognized as a future extension point but is not locally supported
yet. If a remote card only advertises unsupported bindings, negotiation fails as
a transport error and includes the local support list in the diagnostic.
The selected interface appears in agent inspect --json under
data.selected_interface with the canonical binding, original
protocol_binding, selected url, optional tenant, protocol_version, and a
source value.
Advanced users and tests can pass missive agent inspect <alias> --binding <BINDING> to require a specific local binding such as json-rpc. The override
must still be locally supported and advertised by the Agent Card unless the card
omits supportedInterfaces entirely.
For compatibility with older/pre-release Agent Cards that omit
supportedInterfaces, missive falls back to explicit registry/config interface
URLs by binding. If no explicit URL is available and the chosen binding is
http+json, it uses the registered agent base_url as a legacy HTTP+JSON
fallback and reports protocol_version = "unknown" with
source = "base_url_fallback".
missive send builds an official a2a-lf SendMessageRequest containing a
ROLE_USER Message, rich A2A parts, optional contextId/taskId, optional
configuration.acceptedOutputModes, and non-secret request metadata. missive bcast reuses the same request preparation for each group member, assigns one
shared contextId, and generates a fresh A2A message id per member so local
message rows remain unique. Supported outbound parts are official text parts,
url file-reference parts for
canonicalized local file:// URLs, raw byte parts for --file-bytes, and
structured data parts for --json-part. MIME values are stored on official
mediaType fields and JSON data parts default to application/json. The
outbound message id is generated by the official A2A type helper and is also the
local request-message row id.
Transport mapping:
http+jsonappendsmessage:sendto the selected interface URL and sendsPOST <interface>/message:sendwithContent-Type: application/a2a+json.json-rpcposts a JSON-RPC 2.0 request with methodSendMessageto the selected JSON-RPC interface URL.
Both transports send A2A-Version and any configured extensions/service
parameters, apply resolved auth headers, enforce the selected profile's
qos.max_request_bytes on local inputs and the serialized request, and parse the
official SendMessageResponse oneof. Responses may be either a direct Message
or a Task. Direct messages are stored as response message rows. Task responses
are stored in tasks with the raw remote task JSON, mapped task state, protocol
version metadata, and a linked response message row using the task status message
when one is present or a local synthetic response row when the task has no status
message. Broadcasts additionally append redacted missive.bcast.started,
missive.bcast.member.*, and missive.bcast.completed events around the normal
per-member a2a.send.* events.
missive barrier <group> --context <id> is a collective synchronization command
that consumes locally persisted tasks for group members in one context. When
--local is not supplied, known task ids are refreshed with the A2A GetTask
operation before each barrier decision. Task ids can come from the latest local
task row per member/context or from a previous missive bcast --json document
passed with --from-bcast PATH_OR_-.
Transport mapping is the same as missive task get:
http+jsoncallsGET <interface>/tasks/{id}.json-rpcposts methodGetTaskto the selected JSON-RPC interface URL.
Barrier polling sends A2A service-parameter headers and resolved auth headers,
uses cached Agent Cards/interfaces where available, and persists changed remote
Task payloads back into the same tasks and artifacts tables used by task
commands. Changed remote tasks append redacted a2a.task.updated events with
observed_by: "barrier". The collective itself appends redacted
missive.barrier.started, missive.barrier.member.*, and
missive.barrier.completed events.
Default barrier semantics wait for terminal task states while only completed
counts toward successful quorum. Explicit --state values become the satisfying
states. Non-requested failed/cancelled states map to deterministic task exit
codes unless quorum has already been reached under --failure-policy continue.
missive gather <group> --context <id> does not make A2A network calls. It reads
the selected profile's local tasks, messages, and artifacts rows and
collects the latest local task for each group member in deterministic rank order.
If the selected context row is absent, gather creates a local context row so the
journal can link missive.gather.* events to that context.
The text selected for each member comes from the latest persisted non-request
message linked to the gathered task, then the task status message in stored A2A
Task JSON, then the first text artifact. Persisted artifacts are rendered with
the same summaries as missive task artifact commands. --output-dir exports
those already persisted artifacts with safe rank-prefixed filenames; it never
dereferences URL/file-reference artifacts and instead writes their JSON manifest
shape.
missive reduce <group> --context <id> is local by default and does not call
A2A endpoints. It reads the same persisted local task/message/artifact rows as
gather, builds a source-attributed provenance block, and records the final
reduced result as a local messages row with direction local.
When --reducer-agent ALIAS is supplied, missive builds an official A2A
SendMessageRequest whose user message contains the generated reduce prompt.
The prompt is either the selected --template after placeholder expansion or a
default strategy-specific prompt containing {{inputs}}-style provenance plus a
deterministic local baseline. The request uses the selected reduce context id,
adds non-secret reduce metadata, enforces the selected profile's request-size
limit, applies service parameters/auth headers, negotiates the reducer agent's
HTTP+JSON or JSON-RPC interface, and persists the request/response through the
same send path as missive send.
Reducer-agent direct Message responses use the response text as the final
reduced_text. Task responses use the task status message text when present and
otherwise report the returned task id/state. The command also writes one local
reduced-output message containing the final text and provenance metadata, and it
appends redacted missive.reduce.* journal events. No proprietary wire protocol
is introduced for reduction; the only remote operation is ordinary A2A
SendMessage to the reducer agent.
missive stream builds the same official a2a-lf SendMessageRequest shape and
rich text/file/byte/JSON part set as missive send, then validates that the
fetched or cached Agent Card advertises capabilities.streaming = true. Passing
--force bypasses that local capability check for interoperability testing;
without --force, missive fails before opening a stream.
Transport mapping:
http+jsonappendsmessage:streamto the selected interface URL and sendsPOST <interface>/message:streamwithContent-Type: application/a2a+jsonandAccept: text/event-stream.json-rpcposts a JSON-RPC 2.0 request with methodSendStreamingMessageto the selected JSON-RPC interface URL and expects an SSE response.
Each SSE record's data field is parsed as either a direct A2A StreamResponse
object or a JSON-RPC response whose result is a StreamResponse. Supported
stream payloads are the official SDK task, message, statusUpdate, and
artifactUpdate variants. JSON-RPC stream errors become protocol errors, and
malformed JSON or unknown stream variants include the event sequence number in
the diagnostic.
The CLI processes stream events incrementally. Human output writes one line per
parsed event, NDJSON writes one stream_event envelope per event followed by a
stream_result envelope, and JSON mode writes one final stream_result document
after the stream closes. Every parsed event is appended to the local event journal
with event types a2a.stream.task, a2a.stream.message,
a2a.stream.status_update, or a2a.stream.artifact_update; a corresponding
messages row with direction stream_event is also written. Task and status
events update the local tasks row state and preserve A2A protocol-version
metadata. Artifacts embedded in task events are persisted as dedicated artifact
rows. artifactUpdate events upsert the referenced artifact row, and appended
chunks merge their parts into the existing A2A artifact JSON while incrementing a
local version. Gateway task subscription/resume is implemented by missive gateway run for cached streaming-capable agents; task push notification config CRUD is implemented by missive push.
missive gateway run monitors local in-flight tasks and uses the official SubscribeToTaskRequest plus StreamResponse types to resume remote updates when a cached Agent Card advertises capabilities.streaming = true.
Transport mapping follows the same negotiated interface as other task operations:
http+jsonmapsSubscribeToTasktoPOST <interface>/tasks/{id}:subscribewithAccept: text/event-streamandContent-Type: application/a2a+json.json-rpcposts a JSON-RPC 2.0 methodSubscribeToTaskto the selected JSON-RPC interface URL and expects an SSE response.
Each parsed task, message, statusUpdate, or artifactUpdate is appended to the event journal as a2a.subscription.task, a2a.subscription.message, a2a.subscription.status_update, or a2a.subscription.artifact_update. Task and status events update the local task row and terminal states clean up the durable task_subscription gateway job. Streams that fail or close while the task remains in-flight leave a retrying gateway job with bounded backoff metadata.
missive task get --remote, missive task list --remote, missive task wait,
and missive task cancel use the official a2a-lf task request/response types
behind missive_a2a::protocol: GetTaskRequest, ListTasksRequest,
ListTasksResponse, CancelTaskRequest, and Task.
Transport mapping follows the negotiated interface:
http+jsonmapsGetTasktoGET <interface>/tasks/{id},ListTaskstoGET <interface>/tasks, andCancelTasktoPOST <interface>/tasks/{id}:cancel. Task ids are percent-encoded as path segments. List filters become query parameters such ascontextId,status,statusTimestampAfter,pageSize,pageToken,historyLength, andincludeArtifacts.json-rpcposts JSON-RPC 2.0 methodsGetTask,ListTasks, andCancelTaskto the selected JSON-RPC interface URL.
Both transports send A2A-Version, optional A2A-Extensions, configured extra
service parameters, and resolved auth headers. A task returned by any of these
operations is persisted to the local tasks table with raw remote task JSON,
context id, mapped state, protocol-version metadata, and status-message id when
present. Any artifacts embedded in the returned task are also persisted to the
local artifacts table with kind, name, MIME type, metadata, raw A2A artifact
JSON, and an incremented version when the same artifact id is observed again.
ListTasks persists each returned task before applying local output filters.
For interoperability with current a2a-rs example agents, missive accepts
ListTasksResponse payloads that omit or null out pagination fields and treats
missing nextPageToken, pageSize, and totalSize as empty-token/observed-task
count defaults before deserializing into the official Rust type.
task wait repeatedly calls GetTask unless --local is supplied. The wait
loop treats completed, failed, cancelled, and input_required as decisive
states and returns deterministic process exit codes documented in docs/cli.md.
Timeout is controlled by global --timeout; polling cadence is controlled by
--interval.
missive push create/get/list/delete uses the official a2a-lf push config
models behind missive_a2a::protocol: TaskPushNotificationConfig,
GetTaskPushNotificationConfigRequest,
ListTaskPushNotificationConfigsRequest,
ListTaskPushNotificationConfigsResponse, and
DeleteTaskPushNotificationConfigRequest. The upstream SDK currently models
create as the TaskPushNotificationConfig payload itself.
Transport mapping follows the negotiated interface:
http+jsonmaps create/list toPOST/GET <interface>/tasks/{taskId}/pushNotificationConfigs, get toGET <interface>/tasks/{taskId}/pushNotificationConfigs/{configId}, and delete toDELETE <interface>/tasks/{taskId}/pushNotificationConfigs/{configId}. Task ids and config ids are percent-encoded as path segments. List pagination usespageSizeandpageTokenquery parameters.json-rpcposts JSON-RPC 2.0 methodsCreateTaskPushNotificationConfig,GetTaskPushNotificationConfig,ListTaskPushNotificationConfigs, andDeleteTaskPushNotificationConfigto the selected JSON-RPC interface URL.
Both transports send A2A-Version, optional A2A-Extensions, configured extra
service parameters, and resolved auth headers for talking to the remote agent.
Callback authentication is separate: push create --auth-scheme ... --auth-credentials-env ... places the environment value into the A2A
authentication.credentials field for the remote agent to use when calling the
callback URL. CLI output, event payloads, and local push_configs.remote_config_json
redact those credentials. A local placeholder task row is created when needed so
push config rows and events can link to the configured task id even before
missive task get/list observes the remote task.
A2A push callbacks use the same official StreamResponse union shape as
streaming updates: exactly one of task, message, statusUpdate, or
artifactUpdate. missive webhook run validates incoming JSON against
missive_a2a::protocol::StreamResponse before acknowledging the callback.
Accepted callbacks are persisted as redacted event rows with event types
a2a.push.task, a2a.push.message, a2a.push.status_update, or
a2a.push.artifact_update; malformed JSON, schema-incompatible payloads, and
auth failures are persisted as redacted a2a.push.rejected events. Valid
callbacks return HTTP 202; invalid callbacks return 400; auth failures return
401.
The receiver records A2A-Version from inbound headers when present, otherwise
it records the effective missive protocol version. It can require a simple
header/token check through --auth-token-env with configurable header and
scheme. It does not implement signature verification, JWT validation, replay
protection, or remote task resubscription yet. The local HTTP listener exposes
/healthz and /readyz; HTTPS must terminate in a trusted tunnel, reverse
proxy, or local ingress before forwarding to the receiver.
Webhook persistence appends events through missive-store from a blocking task.
If a matching local context exists, or can be safely created, the event links to
that context_id. If a matching task row already exists, for example because
missive push create created a placeholder or missive task observed the task,
the event also links to task_id; otherwise the event remains unlinked while the
redacted raw payload still preserves the A2A ids.
A2A contexts are represented by opaque contextId values. missive send and
missive stream place --context CONTEXT_ID on the outbound official A2A
Message and persist the same id on request/response/stream message rows. A2A
Task payloads returned by send, stream, or task operations also persist their
contextId on the local tasks table and ensure a matching local context row
exists.
missive context is a local control-plane layer around those ids. It can create
a local context id before the first send, assign a human-friendly name, list/show
context records, fork a child context by recording parent_context_id and parent
metadata, mark a context closed locally, and export the context with linked
messages, tasks, and events. These commands do not introduce a proprietary wire
protocol and do not call a remote A2A "close context" endpoint; they preserve and
organize the canonical A2A ids used by implemented message and task calls.
Context export recursively redacts secret-like keys and HTTP authorization
headers before printing. Dedicated A2A task resubscription and task-state updates
from webhook callbacks remain for later gateway work. Artifact rows can be
inspected and exported separately with missive task artifact list/show/save/export.
- HTTP status failures such as
404 Not Foundmap tomissive::transport. - Network/TLS/HTTP client failures map to
missive::transport. - Unsupported or non-mutual interface bindings map to
missive::transport. - A2A
VERSION_NOT_SUPPORTEDresponses map tomissive::protocolwith exit code76. - Invalid JSON or schema-incompatible Agent Card payloads map to
missive::protocol.
crates/missive-a2a depends on the official a2a-lf crate from
a2aproject/a2a-rs and re-exports protocol models from
missive_a2a::protocol. Downstream missive crates should use that module rather
than importing a2a-lf directly. Current fixture coverage round-trips these
A2A v1.0 shapes through official serde types:
AgentCard, including a richer multi-interface Agent Card with extensions, security schemes, and signaturesMessage, including text/data and file-byte examplesArtifact, including extension metadataTask, including completed, input-required, and file-artifact task examplesSendMessageRequestandSendMessageResponseStreamResponsetask/message/status/artifact variants, including the push webhook payload shape used bymissive webhook runGetTaskRequest,ListTasksRequest/ListTasksResponse,CancelTaskRequest, andSubscribeToTaskRequestTaskPushNotificationConfigplus get/list/delete push config request and list response shapes- JSON-RPC request/response/error envelopes with embedded official payloads
- HTTP binding error values that preserve A2A
google.rpc.ErrorInfodetails
Fixtures live under tests/fixtures/a2a/1.0/, and normalized CLI golden outputs
for fixture-backed agent inspect --json and send --json live under
tests/fixtures/a2a/1.0/cli/. The directory name is the protocol major/minor
version used by those wire examples. When updating the upstream SDK or when A2A
wire shapes change, follow the update process in
tests/fixtures/a2a/1.0/README.md, then rerun at least:
cargo test -p missive-a2a --test protocol_fixtures --all-features
cargo test -p missive-cli --test a2a_conformance_fixtures --all-featuresAuthentication material is resolved for implemented Agent Card fetch/refresh,
non-streaming send, streaming send, reduce reducer-agent send, task
get/list/wait/cancel, and push config requests. These paths share AuthHeaders plus the CLI/config auth resolver
instead of inventing separate outbound secret handling paths.