0.4: client Transport port, JSON-RPC interop, typed errors/versioning + workspace cleanups#23
Merged
Conversation
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
Transportport + JSON-RPC 2.0 client + card-driven negotiation (a2a-rs): hexagonal outbound transport mirroring the server side;JsonRpcClientis wire-compatible with the canonical SDKs. New runnableexamples/jsonrpc_client.rs(auto-connect → full task lifecycle).a2a-mcpedition 2024.This batch (finish 0.4 + cleanups)
refactor(a2a-agents)!: drop the stalews_portconfig field (WebSocket transport was removed in 0.3.0).build: consolidate common deps into[workspace.dependencies]; unifythiserrorto2; remove thea2a-agents-common → a2a-agentsdev-dep cycle.feat(a2a-rs): runnablejsonrpc_clientexample.test: SSE tool-call-metadata broadcast wiring + a proto vendor/specsync guard.ci:release-plz.toml(per-crate tags, correct compare-links, noise-commit filtering) + switch release-plz to the master-pushrelease-pr/releaseflow; bumpactions/checkout@v5and force Node 24 on lagging JS actions.docs: doc-comment audit (loneignoredoctest made compile-checked), newROADMAP.md, retireTODO.md+OFFICIAL_SDK_COMPARISON.md.a2a-agents:ServerConfig.ws_portremoved.AsyncA2AClient→Transportport; 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.0and publishes. No publish happens on this merge itself (crates are still at the published0.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; thejsonrpc_clientexample was confirmed end-to-end againstjsonrpc_server.