Skip to content

0.4: client Transport port, JSON-RPC interop, typed errors/versioning + workspace cleanups#23

Merged
EmilLindfors merged 17 commits into
masterfrom
0.4
Jun 5, 2026
Merged

0.4: client Transport port, JSON-RPC interop, typed errors/versioning + workspace cleanups#23
EmilLindfors merged 17 commits into
masterfrom
0.4

Conversation

@EmilLindfors

Copy link
Copy Markdown
Owner

Lands the 0.4 line into master. Pre-1.0 with in-workspace consumers, so this breaks cleanly and fixes all call sites in-tree (no deprecation shims).

Highlights (0.4)

  • Client Transport port + JSON-RPC 2.0 client + card-driven negotiation (a2a-rs): hexagonal outbound transport mirroring the server side; JsonRpcClient is wire-compatible with the canonical SDKs. New runnable examples/jsonrpc_client.rs (auto-connect → full task lifecycle).
  • Wire-compatible JSON-RPC 2.0 + HTTP+JSON server adapter and MCP server over Streamable HTTP.
  • Typed error details, task versioning, call interceptors, streaming wiring; streaming & push split out of storage adapters.
  • mcp-client framework integration + a2a-mcp edition 2024.

This batch (finish 0.4 + cleanups)

  • refactor(a2a-agents)!: drop the stale ws_port config field (WebSocket transport was removed in 0.3.0).
  • build: consolidate common deps into [workspace.dependencies]; unify thiserror to 2; remove the a2a-agents-common → a2a-agents dev-dep cycle.
  • feat(a2a-rs): runnable jsonrpc_client example.
  • test: SSE tool-call-metadata broadcast wiring + a proto vendor/spec sync guard.
  • ci: release-plz.toml (per-crate tags, correct compare-links, noise-commit filtering) + switch release-plz to the master-push release-pr/release flow; bump actions/checkout@v5 and force Node 24 on lagging JS actions.
  • docs: doc-comment audit (lone ignore doctest made compile-checked), new ROADMAP.md, retire TODO.md + OFFICIAL_SDK_COMPARISON.md.

⚠️ Breaking changes

  • a2a-agents: ServerConfig.ws_port removed.
  • Earlier in the line: client AsyncA2AClientTransport port; synchronous port traits removed; streaming/push split out of storage adapters.

Release note

After merge, the new master-push release-plz flow opens a per-crate version-bump PR; merging that bumps the crates to 0.4.0 and publishes. No publish happens on this merge itself (crates are still at the published 0.3.0).

Verification

cargo check --workspace --all-features, cargo clippy --workspace --all-features -- -D warnings, workspace doctests, and the touched unit/integration tests all pass locally; the jsonrpc_client example was confirmed end-to-end against jsonrpc_server.

EmilLindfors and others added 16 commits May 30, 2026 05:27
Captures the work-in-progress migration toward the 0.4 line:
- new transport adapters (jsonrpc, connectrpc replacing request_processor)
- application task_service + task_status_broadcast
- domain ids module
- planning docs (REFACTORING_PLAN, JSONRPC_ADAPTER_PLAN, OFFICIAL_SDK_COMPARISON)
- ignore local reference clone of upstream SDK (a2aproject/)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The sync capability traits (TaskManager, MessageHandler,
NotificationManager, StreamingHandler) had no production users: every
call site, adapter, example, and test used the async variants. Their
only implementations were stubs that returned UnsupportedOperation
("Use async version"), and unlike the async traits they were not even
feature-gated, so they were the sole dead code visible with no features.

Removes the four trait definitions, their re-exports from port/mod.rs
and lib.rs, and the stub impls in the test and example handlers. The
async traits, their *Ext convenience traits, and the shared
validate_push_notification_url helper are unchanged.

Pre-1.0, in-workspace consumers only: broken cleanly in one commit, no
deprecation shims.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cargo check -p a2a-rs --no-default-features previously failed: port/mod.rs
re-exported the server-gated async traits unconditionally, and the
always-on Authenticator trait used async-trait/futures while those crates
were optional (client/server only). The port layer therefore could not
satisfy the rule that domain + port compile with zero features.

Per hexagonal rule #5 (do not feature-gate trait definitions; ports must
compile with zero features), make the port layer always compile:
- async-trait and futures become non-optional deps (dropped from the
  client/server feature lists; tokio stays optional, adapter-only)
- un-gate the async trait definitions (AsyncTaskLifecycle/Query/Ext,
  AsyncMessageHandler, AsyncNotificationManager/Ext, AsyncStreamingHandler,
  Subscriber); their adapter impls remain gated under server
- fix an unused-variable warning in Task::validate surfaced once the
  no-features build compiles (index is only read under the tracing feature)

Verified: --no-default-features and the client/server/auth feature configs
all compile; --all-features clippy and the full test suite still pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…otiation

Add a hexagonal client-side transport abstraction mirroring the server,
plus a wire-compatible JSON-RPC 2.0 client so the client can talk to any
standard A2A agent (recommendation #3 in OFFICIAL_SDK_COMPARISON.md).

- port::client::Transport (a2a_rs::Transport): the renamed/relocated
  AsyncA2AClient with a new protocol() discriminator; StreamItem moves
  alongside it. Deletes services::client. HttpClient reports "CONNECTRPC".
- adapter::transport::jsonrpc_wire: shared method names, error codes,
  envelopes, and a2a_to_jsonrpc/jsonrpc_to_a2a, extracted from the server
  adapter so client and server agree on the wire byte-for-byte.
- adapter::transport::codec::stream_response_to_item: shared wire->StreamItem.
- JsonRpcClient (jsonrpc-client feature): impl Transport over JSON-RPC 2.0
  (single POST, SSE for streaming) reusing the generated ProtoJSON types.
- negotiation: TransportFactory, TransportNegotiator (client-preference
  ranking + major-version filter), default_registry (CONNECTRPC then JSON-RPC),
  and connect(base_url, &negotiator) that fetches the card and negotiates.
- a2a-web-client WebA2AClient now holds Box<dyn Transport> (field transport,
  was http); auto_connect negotiates from the card with a ConnectRPC fallback.

Breaking: AsyncA2AClient -> Transport, moved services::client -> port::client.
All in-workspace call sites (a2a-mcp, a2a-agents, a2a-client, examples) updated.

Tests: tests/jsonrpc_client_interop_test.rs (in-process client<->server
round-trip over a real socket) and tests/transport_negotiation_test.rs.
Verified: workspace builds no-default-features, test --all-features,
clippy --all-features --all-targets -D warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ase 4 final)

Sheds the storage adapters' two non-persistence jobs. InMemoryTaskStorage and
SqlxTaskStorage previously fused persistence, in-memory streaming fan-out, and
push-webhook delivery into one struct. Each is now its own adapter behind its
own port, wired at the composition edge.

- Add adapter::streaming::InMemoryStreamingHandler (subscriber registry +
  fan-out), extracted out of the storage structs; storage no longer impls
  AsyncStreamingHandler nor holds a subscribers map.
- Add the AsyncPushNotifier port for out-of-band webhook delivery, kept
  separate from config CRUD (AsyncNotificationManager) and streaming.
  PushNotificationRegistry implements it; PushNotificationSender stays the
  swappable backend seam. Add NoopPushNotifier + an Arc<T> forwarding impl.
  Storage hands out its notifier via push_notifier() (shares the config
  registry).
- TaskStatusBroadcast mixin gains a HasPushNotifier ingredient and fires push
  (best-effort) alongside the streaming broadcast; add broadcast_artifact.
- Thread the notifier through TaskService, ConnectRpcAdapter/JsonRpcAdapter
  (default NoopPushNotifier + with_push_notifier), ResponderMessageHandler, and
  ReimbursementHandler -- streaming + push are now separate constructor args.
- Behavior change (spec-compliant): subscribing no longer replays current task
  state; the initial snapshot is delivered by the service/transport.

Originally scoped for 0.5 (REFACTORING_PLAN.md 4.3); pulled into 0.4. All crates
compile with/without features, clippy -D warnings clean, full test suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The port-layer refactor (capability decomposition, Arc<dyn> dispatch, newtype
IDs, the cross-port mixin, the service/transport split, and the
storage/streaming/push struct-split) is fully implemented, so the planning doc
is no longer needed.

- Delete REFACTORING_PLAN.md.
- TODO.md: rewrite the status header to record the refactor as complete and make
  it self-contained (no dangling plan reference); keep the lone remaining
  idiomatic item -- the `Result<T>` alias (§3.2) -- as 0.4 item 4, described
  inline; drop "(by the plan)" from the 0.5 heading; note that the prior
  --no-default-features warnings in task_storage.rs are gone with the streaming
  code removed.

Historical "see REFACTORING_PLAN.md §X.Y" citations remain in the CHANGELOG and
a few code comments as design-rationale record.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…treaming wiring + doc audit

Land the remaining 0.4 work and clean up its docs.

- Typed error details: domain::error_details (ErrorInfo/FieldViolation/
  BadRequest), surfaced in the JSON-RPC error.data array and reconstructed
  client-side.
- AsyncTaskVersioning: u64 optimistic-concurrency versions + VersionConflict,
  in both in-memory and sqlx storage (migration 003_task_version.sql).
- CallInterceptor before/after chain on the JSON-RPC client and server, plus a
  built-in LoggingInterceptor.
- JSON-RPC 2.0 client/wire round-tripping.
- Builder-level streaming: AgentBuilder/AgentRuntime::with_streaming inject a
  shared backend into the transport (via ConnectRpcAdapter::with_streaming_
  handler), backed by a forwarding blanket impl AsyncStreamingHandler for
  Arc<dyn AsyncStreamingHandler>. Fixes the builder path defaulting to a no-op
  streaming handler that silently dropped handler broadcasts before SSE clients.
- Kitchen-sink complex_agent example (a2a-agents, --features mcp-server).
- Doc audit: rewrote doc comments that referenced internal planning docs
  (REFACTORING_PLAN.md), refactor narrative ("Phase 4 struct-split", "before
  the split"), and roadmap history so each describes the actual architecture
  and behavior on its own terms.
Serve a TOML-configured agent over MCP's Streamable HTTP transport
(rmcp StreamableHttpService on an axum router) in addition to stdio.

- McpServerConfig gains a [features.mcp_server.http] section (McpHttpConfig:
  enabled, host, port, path) plus allowed_hosts / allowed_origins
  DNS-rebinding knobs; http.enabled takes precedence over stdio.
- run_mcp_server branches to run_streamable_http; enables the
  transport-streamable-http-server rmcp feature.
- mcp_http_agent example (.rs + .toml) and tests/mcp_http_test.rs covering
  the initialize handshake and Host-allow-list reject/allow behavior, plus
  config-parse unit tests.
- README + CHANGELOG + TODO updated.

Also includes in-flight 0.4 transport work present in the tree (JSON-RPC
client, retry transport, streaming wiring).
…2024

mcp-client: close the dead-wiring loop so a config-built MCP client actually
reaches the handler. Previously build_with_auto_storage connected to the
[features.mcp_client] servers and stashed the McpClientManager in AgentRuntime,
but nothing read it — the handler (the tool consumer via McpToolsExt) never saw
it.

- McpClientManager::connect(&McpClientConfig): one-call connect + tool
  discovery; lenient per-server, errors only on total failure. Typed
  McpClientError replaces Box<dyn Error>.
- Hold the RunningService alive per server (dropping it tore down the
  child-process transport — "Transport closed" on every call).
- Handler owns the connected manager and impls McpToolsExt; the trait now
  returns the typed error.
- Remove the dead auto-init from the builder and the unused
  mcp_client/with_mcp_client/mcp_client() on AgentRuntime; gate the
  McpClientManager/McpClientError re-exports on the mcp-client feature.
- bin/mcp_echo_server.rs fixture + examples/mcp_client_agent.{rs,toml} +
  tests/mcp_client_test.rs (spawns the fixture, asserts discovery, echo/add
  calls, NotConnected). README §5 + Cargo.toml comment.

a2a-mcp: bump to edition 2024. Drop the two redundant `ref` bindings in the
reference-matched OAuth2 if-let in bridge/agent_to_mcp.rs (rejected under
edition 2024 match ergonomics).
WebSocket transport was removed in 0.3.0, but both ServerConfig structs
(core/config.rs and agents/reimbursement/config.rs) still defined ws_port
with a default. Removed the field, its default fns, the from_env/Default
wiring, and the now-redundant validation branch (port check collapses to
http_port == 0). Dropped ws_port from the demo bin, example/JSON configs,
and BUILDER_API.md. Two MCP parse-tests that only passed because ws_port
defaulted non-zero now set [features.mcp_server] enabled = true.

BREAKING CHANGE: ServerConfig.ws_port is removed (a2a-agents).
Move the shared set (tokio, serde, serde_json, thiserror, anyhow, chrono,
uuid, tracing, tracing-subscriber, async-trait, futures, reqwest, bon) to
the workspace root; members now reference them with dep.workspace = true,
adding features/optional locally. Unifies thiserror to 2 (was 1.0 in every
crate except a2a-mcp). Also removes the a2a-agents-common -> a2a-agents
dev-dependency cycle (no test used the crate) and registers the new
jsonrpc_client example.
Mirrors examples/jsonrpc_server.rs: auto-connects via connect() +
default_registry() (card-driven negotiation with a direct JsonRpcClient
fallback), then drives a full task lifecycle over the negotiated Transport
(send -> get -> subscribe SSE -> cancel). Verified end-to-end against the
jsonrpc_server example.
Two gaps that were only compile-checked before:
- tool_call_metadata_reaches_stream_subscribers (a2a-agents): a fake
  LlmProvider streams a tool call; the test asserts the tool-call metadata
  (partial chunk + finalized call) reaches a combined_update_stream
  subscriber -- the stream the SSE transport serializes.
- proto_vendor_sync_test (a2a-rs): fails if any file under a2a-rs/proto/
  diverges from its spec/ counterpart; skips when the spec/ mirror is absent
  (e.g. a packaged crate).
release-plz.toml standardizes on per-crate tags ({package}-v{version}),
uses release-plz's release_link for correct changelog compare-links, and
filters noise commits (fmt/clippy/ci/build/style/chore) via commit_parsers.
release-plz.yml switches to the standard master-push release-pr + release
flow (the umbrella v* tag is retired). All three workflows bump
actions/checkout v4 -> v5 and set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 to
silence the Node 20 deprecation from the remaining JS actions.
Convert the lone Rust 'ignore' doctest (AsyncTaskVersioning read-modify-write
loop) into a compile-checked example. Move deferred themes (incl. the blocked
aws-lc-sys/cross investigation) into a new ROADMAP.md and delete the completed
root TODO.md and the stale OFFICIAL_SDK_COMPARISON.md. Repoint the three crate
READMEs' dead TODO.md links to ROADMAP.md and drop a2a-agents' stale Phase 2-6
list (that work has shipped).
@EmilLindfors EmilLindfors merged commit ac9e6f4 into master Jun 5, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant