You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
AfterA2ARequestCallbacks registered on a remoteagent.NewA2A agent (agent/remoteagent/v2) are invoked on each incoming A2A event, but not on the non-partial event the run processor synthesizes when it reassembles a partial-chunked artifact. As a result, a callback that inspects or mutates response content cannot observe or transform a chunked/reassembled artifact — only an artifact delivered as a single non-partial event passes through the callback.
In agent/remoteagent/v2/a2a_agent.go, processEvent runs runAfterA2ARequestCallbacks on the converted incoming event, then calls aggregatePartial and yields its output directly:
When aggregatePartial (a2a_agent_run_processor.go) accumulates partial artifact chunks and, on the last chunk, builds a brand-new aggregated event via buildNonPartialAggregation, that event is returned and yielded to the caller without ever passing back through runAfterA2ARequestCallbacks:
// a2a_agent_run_processor.go (v1.4.0)// emit partial last chunk and follow by the non-partial aggregated eventp.removeAggregation(update.Artifact.ID)
return []*session.Event{event, p.buildNonPartialAggregation(ctx, aggregation)} // L116 ← aggregated event bypasses callbacks
buildNonPartialAggregation is reached from two sites, and both bypass the callback: the last-chunk path above (a2a_agent_run_processor.go:116) and the terminal-status flush path (a2a_agent_run_processor.go:71-78, the Status.State.Terminal() branch). A fix should cover both.
Because an artifact streamed as chunks arrives entirely as Partial events, a callback that (as is typical) only acts on non-partial events skips every chunk, and the one non-partial event that carries the reassembled artifact — the aggregated event — is never handed to the callback at all. The callback silently no-ops for any chunked artifact.
Steps to Reproduce:
Build an A2A AgentExecutor that, for a single task, emits one artifact as multiple TaskArtifactUpdateEvents sharing one ArtifactID — a sequence of chunks where continuation chunks set Append=true and the final chunk sets Append=true, LastChunk=true — each Part carrying a slice of the artifact bytes (the standard way to stream an artifact larger than the SSE line limit). (Append=true on the last chunk is what routes the stream into the aggregation path: a non-append last chunk takes the early return at a2a_agent_run_processor.go:94-98, is marked non-partial, and never reaches buildNonPartialAggregation.)
Wire that server into a consumer with remoteagent.NewA2A(remoteagent.A2AConfig{ /* ... */ AfterRequestCallbacks: []remoteagent.AfterA2ARequestCallback{cb} }), where cb records (or mutates) the events it receives.
Run the remote agent and drive the task to completion.
Observe that cb is invoked for the partial chunk events but is never invoked for the final non-partial aggregated event that carries the concatenated artifact.
Expected Behavior:
AfterA2ARequestCallbacks should be invoked on every event the remote agent emits to the caller, including the non-partial event synthesized from partial artifact chunks. A callback registered to inspect or transform response content should be able to act on the reassembled artifact, consistently with how it acts on a single-event (non-chunked) artifact.
Observed Behavior:
The aggregated event produced by aggregatePartial / buildNonPartialAggregation is yielded to the caller without passing through runAfterA2ARequestCallbacks. Callbacks run only on the raw incoming A2A events, so:
For a single-event (non-chunked) artifact, the callback runs and any transformation takes effect.
For a chunked artifact, the callback never sees the reassembled event, so its transformation is silently dropped.
Where this bites in practice (framed generically): consider an after-request callback that moves large InlineData parts out of events into the artifact service so persisted events stay small. For files small enough to arrive as a single event this works; for larger files the remote agent streams them as chunks, the reassembled event bypasses the callback and retains the full bytes inline, and persisting that event to a size-limited session backend then fails (e.g. the Vertex AI managed session service rejects events over ~10 MiB).
Environment Details:
ADK Library Version: v1.4.0 (latest). The bug is present in v1.3.0 and v1.4.0 — i.e., since the agent/remoteagent/v2 package was introduced in v1.3.0 — and remains on main.
OS: macOS (platform-independent — this is a control-flow bug)
Go Version: go1.25
Model Information:
N/A — the bug is in remote-agent event aggregation / callback dispatch and is independent of the model.
🟡 Optional Information
Regression:
No. The synthesized aggregated event appears never to have been routed through AfterA2ARequestCallbacks. The agent/remoteagent/v2 package was introduced in v1.3.0 already carrying this behavior, and the identical aggregation/callback logic (same function names and line numbers) existed in the predecessor agent/remoteagent package back to at least v1.1.0 — that package's run-processor was later removed in favor of v2. We don't believe it ever worked.
Logs:
The adk-go layer surfaces no error — the callback simply isn't called. The downstream symptom when the un-stripped aggregated event is persisted to a size-limited session backend:
Relevant code (v1.4.0 / main): agent/remoteagent/v2/a2a_agent.goprocessEvent (runAfterA2ARequestCallbacks at L270 on the incoming event; aggregatePartial(...) result yielded at L283-284); agent/remoteagent/v2/a2a_agent_run_processor.goaggregatePartial (returns the synthesized aggregated event from buildNonPartialAggregation at L116) and buildNonPartialAggregation (L175-184), which constructs a fresh event that never re-enters the callback pipeline.
buildNonPartialAggregation is invoked from two sites and both bypass the callback: the last-chunk path (a2a_agent_run_processor.go:116) and the terminal-status flush path (a2a_agent_run_processor.go:71-78).
Minimal Reproduction Code:
Sketch isolating the issue (server setup elided for brevity):
// A callback registered on the remote-agent config that records every event it is handed.varseen []*session.Eventcb:=func(
ctx agent.CallbackContext,
req*a2a.SendMessageRequest,
resp*session.Event,
errerror,
) (*session.Event, error) {
seen=append(seen, resp)
returnnil, nil// leave the event unchanged
}
remoteAgent, _:=remoteagent.NewA2A(remoteagent.A2AConfig{
Name: "remote",
AgentCardProvider: remoteagent.NewAgentCardProvider(serverURL), // server streams ONE chunked artifactClientProvider: remoteagent.NewA2AClientProvider(clientFactory),
AfterRequestCallbacks: []remoteagent.AfterA2ARequestCallback{cb},
})
// Drive remoteAgent to run a task whose server emits ONE artifact as N TaskArtifactUpdateEvents// sharing one ArtifactID: continuation chunks Append=true, final chunk Append=true + LastChunk=true.// ... run ...// BUG: `seen` contains the partial chunk events but NOT the final non-partial// aggregated event (the one carrying the concatenated artifact).// Expected: the aggregated event is also handed to `cb`.
How often has this issue occurred?:
Always (100%) — for any artifact the remote agent delivers as partial chunks.
🔴 Required Information
Describe the Bug:
AfterA2ARequestCallbacks registered on aremoteagent.NewA2Aagent (agent/remoteagent/v2) are invoked on each incoming A2A event, but not on the non-partial event the run processor synthesizes when it reassembles a partial-chunked artifact. As a result, a callback that inspects or mutates response content cannot observe or transform a chunked/reassembled artifact — only an artifact delivered as a single non-partial event passes through the callback.In
agent/remoteagent/v2/a2a_agent.go,processEventrunsrunAfterA2ARequestCallbackson the converted incoming event, then callsaggregatePartialand yields its output directly:When
aggregatePartial(a2a_agent_run_processor.go) accumulates partial artifact chunks and, on the last chunk, builds a brand-new aggregated event viabuildNonPartialAggregation, that event is returned and yielded to the caller without ever passing back throughrunAfterA2ARequestCallbacks:buildNonPartialAggregationis reached from two sites, and both bypass the callback: the last-chunk path above (a2a_agent_run_processor.go:116) and the terminal-status flush path (a2a_agent_run_processor.go:71-78, theStatus.State.Terminal()branch). A fix should cover both.Because an artifact streamed as chunks arrives entirely as
Partialevents, a callback that (as is typical) only acts on non-partial events skips every chunk, and the one non-partial event that carries the reassembled artifact — the aggregated event — is never handed to the callback at all. The callback silently no-ops for any chunked artifact.Steps to Reproduce:
AgentExecutorthat, for a single task, emits one artifact as multipleTaskArtifactUpdateEvents sharing oneArtifactID— a sequence of chunks where continuation chunks setAppend=trueand the final chunk setsAppend=true, LastChunk=true— eachPartcarrying a slice of the artifact bytes (the standard way to stream an artifact larger than the SSE line limit). (Append=trueon the last chunk is what routes the stream into the aggregation path: a non-append last chunk takes the early return ata2a_agent_run_processor.go:94-98, is marked non-partial, and never reachesbuildNonPartialAggregation.)remoteagent.NewA2A(remoteagent.A2AConfig{ /* ... */ AfterRequestCallbacks: []remoteagent.AfterA2ARequestCallback{cb} }), wherecbrecords (or mutates) the events it receives.cbis invoked for the partial chunk events but is never invoked for the final non-partial aggregated event that carries the concatenated artifact.Expected Behavior:
AfterA2ARequestCallbacks should be invoked on every event the remote agent emits to the caller, including the non-partial event synthesized from partial artifact chunks. A callback registered to inspect or transform response content should be able to act on the reassembled artifact, consistently with how it acts on a single-event (non-chunked) artifact.Observed Behavior:
The aggregated event produced by
aggregatePartial/buildNonPartialAggregationis yielded to the caller without passing throughrunAfterA2ARequestCallbacks. Callbacks run only on the raw incoming A2A events, so:Where this bites in practice (framed generically): consider an after-request callback that moves large
InlineDataparts out of events into the artifact service so persisted events stay small. For files small enough to arrive as a single event this works; for larger files the remote agent streams them as chunks, the reassembled event bypasses the callback and retains the full bytes inline, and persisting that event to a size-limited session backend then fails (e.g. the Vertex AI managed session service rejects events over ~10 MiB).Environment Details:
agent/remoteagent/v2package was introduced in v1.3.0 — and remains onmain.Model Information:
🟡 Optional Information
Regression:
No. The synthesized aggregated event appears never to have been routed through
AfterA2ARequestCallbacks. Theagent/remoteagent/v2package was introduced in v1.3.0 already carrying this behavior, and the identical aggregation/callback logic (same function names and line numbers) existed in the predecessoragent/remoteagentpackage back to at least v1.1.0 — that package's run-processor was later removed in favor ofv2. We don't believe it ever worked.Logs:
The adk-go layer surfaces no error — the callback simply isn't called. The downstream symptom when the un-stripped aggregated event is persisted to a size-limited session backend:
Additional Context:
main):agent/remoteagent/v2/a2a_agent.goprocessEvent(runAfterA2ARequestCallbacksat L270 on the incoming event;aggregatePartial(...)result yielded at L283-284);agent/remoteagent/v2/a2a_agent_run_processor.goaggregatePartial(returns the synthesized aggregated event frombuildNonPartialAggregationat L116) andbuildNonPartialAggregation(L175-184), which constructs a fresh event that never re-enters the callback pipeline.buildNonPartialAggregationis invoked from two sites and both bypass the callback: the last-chunk path (a2a_agent_run_processor.go:116) and the terminal-status flush path (a2a_agent_run_processor.go:71-78).Actions.ArtifactDeltaduringAppendEvent/ListEventsround-trip #902 (open — Vertex AI session service dropsActions.ArtifactDeltaon round-trip; adjacent artifact-persistence area, different mechanism). Neither addresses this callback-dispatch gap.Minimal Reproduction Code:
Sketch isolating the issue (server setup elided for brevity):
How often has this issue occurred?: