Skip to content

Add A2A protocol bridge and client primitives#6

Merged
Beckers26 merged 4 commits into
mainfrom
codex/issue-4-a2a
Jul 4, 2026
Merged

Add A2A protocol bridge and client primitives#6
Beckers26 merged 4 commits into
mainfrom
codex/issue-4-a2a

Conversation

@Beckers26

Copy link
Copy Markdown
Contributor

Summary

  • Add localized A2A bridge package that exposes any agentadaptor.Runner through the official a2a-go/v2 server handler without importing concrete adapters or changing core SDK APIs.
  • Add thin A2A client package for Agent Card discovery/cache plus Send, SendStream, Subscribe, GetTask, and CancelTask with structured task/status/artifact/error preservation.
  • Document A2A host integration, dependency rationale, and non-goals.

Verification

  • go test ./pkg/clients/a2a ./pkg/bridges/a2a
  • go test -count=20 ./pkg/clients/a2a ./pkg/bridges/a2a
  • go test ./...
  • go vet ./...
  • git diff --check --cached

Known verification gap

  • go test -race ./pkg/clients/a2a ./pkg/bridges/a2a could not run on this Windows host because the race detector requires cgo and no gcc, clang, or zig compiler is installed in PATH.
  • Live third-party A2A endpoint interop was not available in this workspace.

Closes #4

A2A support is implemented as localized bridge/client packages. The server bridge wraps a Runner through the official a2a-go request handler and maps A2A tasks onto Runner.Start, StreamEvents, Wait, and Cancel. The thin client wraps official discovery and transport behavior behind stable agent-adaptor DTOs so remote protocol traffic is not flattened into local CLI semantics.

Constraint: Issue #4 requires the official A2A Go SDK and no core SDK A2A API.
Constraint: Existing workspace had unrelated dirty changes, so this work was isolated in a clean worktree.
Rejected: Hand-rolled JSON-RPC/SSE bridge | duplicates official SDK wire behavior and violates dependency requirement.
Rejected: Remote A2A AgentBinding | conflates remote protocol tasks with local CLI workspace/profile/stdout semantics and is out of scope.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep A2A imports confined to pkg/bridges/a2a and pkg/clients/a2a; do not add A2A-specific core SDK APIs without a new design.
Tested: go test ./pkg/clients/a2a ./pkg/bridges/a2a
Tested: go test -count=20 ./pkg/clients/a2a ./pkg/bridges/a2a
Tested: go test ./...
Tested: go vet ./...
Not-tested: go test -race ./pkg/clients/a2a ./pkg/bridges/a2a | Windows race detector requires cgo and no gcc/clang/zig C compiler is installed in PATH.
Not-tested: live third-party A2A endpoint interop.
Related: #4
@Beckers26

Copy link
Copy Markdown
Contributor Author

Owner Review: Request Changes

This PR is directionally aligned with the repository boundary: A2A stays localized under pkg/bridges/a2a and pkg/clients/a2a, and core SDK execution semantics are not expanded. I still cannot approve it as production-ready because there are protocol-state and merge-gate issues that need to be fixed first.

Blocking / High

  1. A2A stream final-state detection is incomplete

    Files: pkg/clients/a2a/convert.go, pkg/clients/a2a/types.go, pkg/clients/a2a/client.go

    eventFromUpstream currently marks only TaskState.Terminal() task/status events as EventTerminal, and *a2a.Message is returned as EventMessage. The upstream A2A SDK treats a valid execution sequence as final when it receives:

    • any Message
    • a Task / TaskStatusUpdateEvent whose state is terminal
    • a Task / TaskStatusUpdateEvent in TASK_STATE_INPUT_REQUIRED

    Because TASK_STATE_INPUT_REQUIRED is not Terminal(), a legal stream that pauses for user input is surfaced as a non-terminal event followed by EOF. If the stream disconnects after that state, tryRecover also refuses the recovered task because it checks only task.Status.State.Terminal(), producing a StreamRecoveryError for a valid final execution state.

    Required fix: add a package-local execution-final predicate matching upstream semantics, and use it consistently in eventFromUpstream, stream terminal tracking, and recovery. Add tests for Message final events and TASK_STATE_INPUT_REQUIRED task/status events.

  2. Required PR check is red

    Workflow failure: build in GitHub Actions.

    The failure is currently:

    go: errors parsing go.mod:
    go.mod:3: invalid go version '1.26.0': must match format 1.23
    

    The workflow still sets up Go 1.20, while the repository declares go 1.26.0. This appears to be an existing CI/toolchain mismatch rather than an A2A design issue, but the PR cannot be merged while required checks are failing. Fix this in this PR or land a prerequisite CI fix and rerun the checks.

Medium

  1. Capabilities.Streaming=false cannot be expressed

    File: pkg/bridges/a2a/card.go

    buildAgentCard uses trueOrDefault(in.Capabilities.Streaming, true), so callers cannot explicitly disable streaming even though Capabilities.Streaming is exposed in public ServerOptions and then passed into WithCapabilityChecks. That makes the public option misleading and can advertise streaming support against host intent.

    Required fix: either make streaming explicitly always-on and remove/ignore the option with documentation, or make the option tri-state/pointer-backed so false can be represented.

  2. Missing race coverage for returnImmediately followed by immediate cancel

    File: pkg/bridges/a2a/server.go

    The bridge yields Submitted / Working before calling Runner.Start and before storing the handle in active. A client using returnImmediately can receive a task ID and call CancelTask while the handle is not yet in the active map. Upstream A2A has cancellation machinery that likely covers part of this path by sending cancellation through the execution queue/context, but this bridge-specific gap is not covered by tests.

    Required fix: add a test where SendMessage uses returnImmediately, CancelTask happens before or during delayed Runner.Start, and verify the underlying run is either not started or receives a cancelled context and cannot continue mutating session/workspace state.

Verification Performed

Local verification on a detached PR worktree:

  • go test ./... passed
  • go build ./... passed
  • go vet ./... passed
  • go test -count=20 ./pkg/clients/a2a ./pkg/bridges/a2a passed
  • git diff --check origin/main...origin/pr/6 passed
  • go test -race ./pkg/clients/a2a ./pkg/bridges/a2a was not runnable in this Windows environment because cgo requires a C compiler and gcc is not installed in %PATH%

Recommendation: Request changes until the A2A final-state semantics and CI failure are fixed.

@Beckers26

Copy link
Copy Markdown
Contributor Author

Owner Review: Request Changes

This PR is directionally aligned with issue #4 on the package boundary: A2A stays localized under pkg/bridges/a2a and pkg/clients/a2a, uses github.com/a2aproject/a2a-go/v2, does not add A2A API to core, and remains driver-blind. I still cannot approve it as production-ready. Several issue #4 must requirements are only partially implemented, and a few public API surfaces over-promise behavior the bridge/client does not actually provide.

Blocking / High

  1. Client stream final-state detection does not match upstream A2A semantics

    Files: pkg/clients/a2a/convert.go:232, pkg/clients/a2a/client.go:347, pkg/clients/a2a/client.go:387, pkg/clients/a2a/types.go:137

    eventFromUpstream marks only TaskState.Terminal() task/status events as EventTerminal; *a2a.Message remains EventMessage, and TASK_STATE_INPUT_REQUIRED is treated as non-final. tryRecover has the same terminal-only predicate.

    The official SDK treats a valid execution sequence as final when it sees a Message, a task/status event whose state is terminal, or a task/status event in TASK_STATE_INPUT_REQUIRED. This PR will therefore surface legal Message and input-required completions as non-terminal streams followed by EOF or StreamRecoveryError.

    Required fix: add a package-local execution-final predicate matching upstream semantics and use it consistently in event conversion, terminal tracking, and recovery. Add tests for Message final events and TASK_STATE_INPUT_REQUIRED task/status events.

  2. returnImmediately cancellation can report canceled while the SDK run still starts

    Files: pkg/bridges/a2a/server.go:107, pkg/bridges/a2a/server.go:116, pkg/bridges/a2a/server.go:122, pkg/bridges/a2a/server.go:177

    The bridge yields Submitted / Working before Runner.Start and before storing the handle in active. A CancelTask that arrives in that window sees no handle and returns a canceled A2A status, but the SDK run can still start afterward and mutate session/workspace state.

    Required fix: establish a cancelable pending task before exposing the task ID, or route cancel through the startup context. Add a delayed-Start / immediate-CancelTask race test that proves the underlying run is not allowed to continue after the client sees cancellation.

  3. Agent Card capabilities can lie about bridge support

    Files: pkg/bridges/a2a/types.go:37, pkg/bridges/a2a/card.go:28, pkg/bridges/a2a/card.go:75, pkg/bridges/a2a/server.go:49

    Capabilities.Streaming=false cannot be expressed because trueOrDefault(false, true) always publishes streaming=true. Separately, PushNotifications and ExtendedAgentCard are exposed and forwarded into the served card, but NewServer does not wire the upstream push config store/sender or extended-card producer. Discovery can therefore advertise capabilities that fail at runtime.

    Required fix: make capability fields truthful. Use tri-state/defaultable configuration for streaming, and either remove unsupported push/extended-card knobs or add the corresponding upstream collaborators and validation in ServerOptions.

  4. Task lifecycle is hardcoded to hidden, unbounded in-memory state

    Files: pkg/bridges/a2a/types.go:90, pkg/bridges/a2a/server.go:49, docs/a2a.md:56

    Issue Add A2A protocol bridge and client primitives #4 explicitly proposed host-owned TaskStore / lifecycle injection and says hosts own durability, retention, tenancy, and observability. This PR hardcodes taskstore.NewInMemory(nil) and exposes no way for the host to supply durable/bounded storage or cluster/event-queue collaborators.

    This makes returnImmediately, GetTask, CancelTask, SubscribeToTask, artifact retention, and restart/multi-instance behavior silently process-local. It is also an unbounded memory-retention risk under high-volume or malicious input.

    Required fix: expose at least a host-supplied task store and bounded defaults; for async/subscribe semantics, either expose the relevant upstream lifecycle collaborators or explicitly narrow the bridge to ephemeral single-process behavior in API and docs.

  5. Bearer credentials are attached to arbitrary Agent Card-declared endpoints

    Files: pkg/clients/a2a/auth.go:24, pkg/clients/a2a/client.go:163, pkg/clients/a2a/client.go:193, pkg/clients/a2a/convert.go:81

    BearerToken wraps the HTTP transport and adds Authorization to every outbound request. The client then trusts supportedInterfaces[*].url from the fetched Agent Card with only a non-empty URL check. A malicious or compromised card can redirect protocol traffic to another host or internal URL, and the wrapper will attach the bearer token to that request. The wrapper also re-adds auth on redirected requests.

    Required fix: pin allowed origins by default, require explicit opt-in for cross-origin interface URLs, block or validate cross-origin redirects, and add tests proving tokens are not sent to untrusted endpoints.

  6. Bridge exposes raw internal execution data to remote A2A callers by default

    Files: pkg/bridges/a2a/mapping.go:43, pkg/bridges/a2a/mapping.go:91, pkg/bridges/a2a/mapping.go:97, pkg/bridges/a2a/mapping.go:103, pkg/bridges/a2a/mapping.go:106, pkg/bridges/a2a/convert.go:85

    The bridge publishes RunResult.Result, Transcript, RawStreams, metadata, failure metadata, stream raw payloads, and HITL request/response objects into A2A artifacts/messages by default. Issue Add A2A protocol bridge and client primitives #4 requires result layering, but production exposure across a remote protocol boundary needs an explicit exposure/redaction policy.

    Required fix: keep assistant-facing output separate, but make diagnostic/raw exposure opt-in and redact common secret-bearing fields. Add tests for auth header/token redaction and for RawStreams/HITL payload handling.

  7. Required PR check is red

    Workflow: build in GitHub Actions.

    Current failure:

    go: errors parsing go.mod:
    go.mod:3: invalid go version '1.26.0': must match format 1.23
    

    The workflow still uses Go 1.20, while the repository declares go 1.26.0. This predates the A2A diff, but the PR cannot merge with required checks failing. Fix CI/toolchain alignment in this PR or land/rerun a prerequisite CI fix first.

Medium

  1. SubscribeRequest.Since is a public no-op

    Files: pkg/clients/a2a/types.go:187, pkg/clients/a2a/client.go:85, docs/a2a.md:90

    The API exposes a recovery cursor, but the implementation never sends or emulates it. If A2A 1.0 cannot express this, the public API should not imply cursor replay. Either remove it, or document and implement a real recovery strategy with explicit guarantees.

  2. Client stream lifecycle is pass-through, not deterministic under duplicate/late terminal events

    Files: pkg/clients/a2a/client.go:347, pkg/clients/a2a/client.go:372, pkg/clients/a2a/convert.go:232

    Issue Add A2A protocol bridge and client primitives #4 requires deterministic handling for duplicate terminal events and terminal-before-start/status. The current client only sets a boolean after emitting EventTerminal; it does not dedupe or reject/normalize later events. Add a minimal stream state machine and fixture tests.

  3. Server.AgentCard() is lossy compared with AgentCardHandler()

Files: pkg/bridges/a2a/server.go:71, pkg/bridges/a2a/server.go:262

The served card includes security schemes/requirements, capability extensions, and rich skill fields, but Server.AgentCard() drops them. Either make this introspection API round-trip complete or remove it to avoid two sources of truth.

  1. Test and CI coverage are below issue Add A2A protocol bridge and client primitives #4's acceptance matrix

Files: pkg/bridges/a2a/server_test.go, pkg/clients/a2a/client_test.go, .github/workflows/go.yml

Current tests cover happy paths, basic cancel, one protocol error, and import boundaries. Missing high-value coverage includes start failure, partial stream then wait failure, RunResult.Failure, invalid request not starting the SDK, stream recovery success/failure, malformed Agent Cards, unsupported protocol versions, duplicate terminal events, large artifact/resource caps, and token redaction. CI also does not run go vet, repeated A2A tests, or go test -race on a Linux runner.

Verification Performed

On a clean detached PR worktree at 3b5d5f42:

  • go test ./... passed
  • go vet ./... passed
  • go test -count=20 ./pkg/clients/a2a ./pkg/bridges/a2a passed
  • git diff --check origin/main...HEAD passed
  • go test -race ./pkg/clients/a2a ./pkg/bridges/a2a was not runnable on this Windows host because cgo requires a C compiler and gcc is not installed in PATH
  • gh pr checks 6 is failing on the required build check

Recommendation: REQUEST CHANGES until the protocol final-state semantics, cancellation race, capability truthfulness, lifecycle ownership, credential boundary, raw-data exposure, and CI failure are addressed.

@Beckers26

Copy link
Copy Markdown
Contributor Author

Additional Owner Review Finding

The rerun code-reviewer subagent found one additional issue that was not explicitly called out in my consolidated owner review, and I agree it should be tracked before merge.

Medium: streamed A2A artifacts never mark their final chunk

Files: pkg/bridges/a2a/mapping.go:63, pkg/bridges/a2a/mapping.go:89, pkg/bridges/a2a/mapping.go:109

streamTranslator.artifact only manages Append; it never marks the final update for assistant-output, reasoning, tool argument/result artifacts, or the terminal agent-adaptor-result artifact. The client DTO preserves LastChunk, so the bridge should emit it truthfully. As written, a conforming A2A client that assembles artifact streams by lastChunk cannot know when an artifact is complete; it must infer completion from the task terminal status, which breaks per-artifact lifecycle semantics.

Required fix: set LastChunk=true for single-shot terminal/result artifacts, and either buffer one chunk ahead or emit an explicit final update for streamed artifacts. Add tests that prove streamed assistant output and terminal result artifacts close with the expected lastChunk signal.

Owner review found that the first A2A slice had protocol-final, lifecycle, capability, credential, and remote exposure gaps. This change keeps A2A localized while making the public bridge/client contracts truthful under review pressure.

Constraint: PR #6 review required production-grade fixes without adding A2A concepts to core SDK execution semantics
Constraint: A2A 1.0 SubscribeToTask has no cursor replay field
Rejected: Trust Agent Card cross-origin interface URLs by default | bearer credentials could be leaked to attacker-controlled endpoints
Rejected: Continue using upstream default in-memory task store | it is process-local and unbounded for bridge defaults
Rejected: Advertise push or extended-card support from booleans alone | runtime collaborators are required for truthful capability discovery
Confidence: high
Scope-risk: moderate
Directive: Keep A2A lifecycle, auth-origin, and raw-exposure policy inside pkg/bridges/a2a and pkg/clients/a2a; do not move these concerns into core SDK
Tested: go test -count=20 ./pkg/clients/a2a ./pkg/bridges/a2a
Tested: go test ./...
Tested: go vet ./...
Tested: git diff --check
Not-tested: Local Windows race detector because cgo needs gcc and no C compiler is installed in PATH; CI now runs go test -race for A2A packages on Ubuntu
@Beckers26

Copy link
Copy Markdown
Contributor Author

Owner follow-up after review fixes:

I reviewed the comments and found the substantive findings reasonable. I did not find a comment that should be rejected as incorrect.

Addressed in 79b78bc:

  • Client execution-final semantics now match the official A2A server behavior: Message, terminal task/status, and TASK_STATE_INPUT_REQUIRED all end a stream. Recovery and duplicate/late final handling use the same predicate.
  • SubscribeRequest.Since is no longer a silent no-op; it returns ErrUnsupported because A2A 1.0 SubscribeToTask has no cursor replay field.
  • Bearer auth is origin-pinned by default. Cross-origin Agent Card interface URLs require TrustedAuthOrigins, and redirects to untrusted origins strip Authorization.
  • Bridge capability discovery is now truthful: streaming is tri-state, and push/extended-card require both card capability and runtime collaborators.
  • Task lifecycle is host-injectable through taskstore.Store. The default bridge store is bounded ephemeral storage instead of the upstream unbounded default.
  • returnImmediately startup cancellation now has a pending cancel path before the task ID is exposed, with a delayed Start cancellation regression test.
  • Raw/diagnostic bridge exposure is opt-in through ExposurePolicy and sanitized; default remote output is assistant-facing output plus safe summary only.
  • A2A streamed artifacts now close with lastChunk, including assistant-output and agent-adaptor-result.
  • Server.AgentCard() now preserves configured security requirements, capability extensions, and rich skill fields.
  • CI now reads go.mod for Go 1.26.0 and runs build, test, vet, repeated A2A tests, and A2A race tests on Ubuntu.

Verification:

  • go test -count=20 ./pkg/clients/a2a ./pkg/bridges/a2a
  • go test ./...
  • go vet ./...
  • git diff --check

Local note: go test -race could not run on this Windows host because cgo requires a C compiler and gcc/clang/zig are not installed in PATH. The workflow now runs the A2A race test on Ubuntu.

Issue #5 needs the A2A client to carry tenant-aware lookup and cancellation metadata without another public API break after issue #4 merges. This changes task lookup and cancellation to request DTOs before the A2A package is merged, and promotes bridge-owned artifact names to exported constants so the visual subagent mux can consume stable wire conventions instead of string literals.

Constraint: Issue #4 must remain localized to A2A protocol primitives and not implement the issue #5 delegation bus or MCP tool surface

Constraint: A2A GetTask and CancelTask both carry tenant fields upstream; GetTask also carries historyLength and CancelTask carries metadata

Rejected: Leave GetTask/CancelTask string-only until issue #5 | would force a public API break or lossy tenant/cancel semantics in the delegation workstream

Rejected: Let issue #5 match assistant-output by magic string | brittle and undocumented bridge contract

Confidence: high

Scope-risk: narrow

Directive: Keep visual delegation implementation in issue #5 packages; issue #4 should only expose stable A2A primitives and bridge wire conventions

Tested: go test ./pkg/clients/a2a ./pkg/bridges/a2a

Tested: go test -count=20 ./pkg/clients/a2a ./pkg/bridges/a2a

Tested: go test ./...

Tested: go vet ./...

Tested: git diff --check

Not-tested: Local Windows race detector; this host still lacks a C compiler for cgo
@Beckers26

Copy link
Copy Markdown
Contributor Author

Follow-up substrate optimization for issue #5 in 15feb31:

  • pkg/clients/a2a.Client.GetTask now accepts GetTaskRequest so callers can pass Tenant and HistoryLength through to upstream A2A.
  • pkg/clients/a2a.Client.CancelTask now accepts CancelTaskRequest so parent-run cancellation can carry Tenant and structured cancellation metadata.
  • Added a non-streaming fallback regression proving Send(ReturnImmediately) plus GetTask polling works for remote agents that advertise streaming=false.
  • Exported bridge-owned artifact names as a2a.ArtifactAssistantOutput and a2a.ArtifactAgentAdaptorResult, and documented them as the stable wire convention for higher-level stream overlays.

This keeps PR #6 inside the issue #4 boundary while removing avoidable API churn for issue #5.

Verification after the change:

  • go test ./pkg/clients/a2a ./pkg/bridges/a2a
  • go test -count=20 ./pkg/clients/a2a ./pkg/bridges/a2a
  • go test ./...
  • go vet ./...
  • git diff --check

The A2A bridge and client now have a runnable local example that exercises Agent Card discovery, streaming task execution, bridge-owned artifacts, and GetTask polling against a real local runner. The demo creates an isolated workspace and cloned provider profile so it does not write state into a host's active agent profile, while still seeding native settings and linking auth files so custom API key and base URL setups work in the isolated run.

Constraint: A2A bridge execution always requests SDK streaming, so the example must exercise provider streaming paths rather than a fake runner or non-streaming CLI shortcut.

Constraint: Local provider credentials may live in settings files instead of recognized auth files, especially custom API key/proxy setups.

Rejected: Use a mock runner for the demo | it would not prove the bridge/client/SDK/provider integration that issue #4 is meant to expose.

Rejected: Use native profiles directly | that could write demo state into the user's active provider profile.

Rejected: Clone auth files only | custom API key/base URL settings were lost in isolated Claude/Codex runs.

Confidence: high

Scope-risk: narrow

Directive: Keep this example isolated by default; do not switch it to WithNativeProfile or a shared workspace for convenience.

Tested: go test ./examples/a2a-local ./pkg/clients/a2a ./pkg/bridges/a2a

Tested: go test -count=20 ./pkg/clients/a2a ./pkg/bridges/a2a ./examples/a2a-local

Tested: go test ./...

Tested: go vet ./...

Tested: git diff --check

Tested: go run ./examples/a2a-local -agent=claude -timeout=2m

Tested: go run ./examples/a2a-local -agent=codex -timeout=2m

Tested: go run ./examples/a2a-local -agent=codex -serve-only -timeout=1s
@Beckers26

Copy link
Copy Markdown
Contributor Author

Added an isolated runnable A2A example in commit c9aad7f.\n\nWhat changed:\n- New �xamples/a2a-local starts a local A2A JSON-RPC server around a real SDK runner, then calls it through pkg/clients/a2a.\n- The demo verifies Agent Card discovery, streaming task events/artifacts, terminal result artifact exposure, and GetTask polling.\n- The demo uses a temporary workspace and cloned provider profile by default. It copies native settings into the isolated profile for custom API key/base URL setups, links auth files via CloneProfileAuthLink, and does not write to the host's active profile.\n- README/docs and the example smoke runner now include the A2A demo.\n\nVerification:\n- go test ./examples/a2a-local ./pkg/clients/a2a ./pkg/bridges/a2a\n- go test -count=20 ./pkg/clients/a2a ./pkg/bridges/a2a ./examples/a2a-local\n- go test ./...\n- go vet ./...\n- git diff --check\n- go run ./examples/a2a-local -agent=claude -timeout=2m\n- go run ./examples/a2a-local -agent=codex -timeout=2m\n- go run ./examples/a2a-local -agent=codex -serve-only -timeout=1s\n\nCI: build passed on c9aad7f.

@cbuthim-art cbuthim-art left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Beckers26 Beckers26 merged commit cdceab3 into main Jul 4, 2026
1 check 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.

Add A2A protocol bridge and client primitives

2 participants