fix(adk): export a turn's spans before its terminal event leaves the process (#2786)
A streamed A2A turn on Agent Substrate leaves no trace spans, while the same turn sent as a unary `SendMessage` does. The UI's chat page streams its turns, so from the UI every conversation is missing from the trace backend, while `buf curl` unary turns and the Go e2e trace cases pass. **Why** `KAGENT_PRE_RESPONSE_TRACE_FLUSH` (#2176) flushes spans in the HTTP wrapper in `go/adk/pkg/a2a/server/server.go`, after the gRPC handler returns. For a unary request that is before the response is written, so it is early enough. For `SendStreamingMessage` the terminal `TaskStatusUpdateEvent` is already on the wire by then, the gateway closes its stream to the runtime as soon as that event arrives (`go/core/internal/a2agateway/task_run.go`), and Substrate checkpoints the actor right after. On a kind cluster the atelet log shows the task completing at 06:02:45.837 and the checkpoint at 06:02:46.013, so the flush is frozen mid-export and its deadline has passed by the next thaw. The spans of every streamed turn stay in the snapshot. **What changes** - `KAgentExecutor.Execute` ends the invocation span and force-flushes right before yielding the event that ends the turn (a terminal, input-required or auth-required state, or an error). That is the only window a streamed turn has. - The flush stays behind `KAGENT_PRE_RESPONSE_TRACE_FLUSH`, now read through `telemetry.PreResponseFlushEnabled()` in both the executor and the server wrapper. Runtimes that are not frozen after a response keep relying on the batch exporter's timer. - The server wrapper's flush stays, since it is the only point the otelhttp server span can be exported from. Its comment now explains how the two flushes relate. **Verification** - With the runtime image built from this change on a kind cluster, a `SendStreamingMessage` turn on a fresh conversation landed its three `gcp.vertex.agent` spans within five seconds. Before the change the same call produced none, while a unary `SendMessage` on the same instance produced three. - Two turns typed into the UI's chat page were traced, and "View traces" on the answer opened the trace. - `TestKAgentExecutor_ExportsSpansBeforeYieldingTheTerminalEvent` asserts the export has happened by the time the terminal event reaches the consumer, with the invocation span in it. It fails on the current code. `TestKAgentExecutor_LeavesSpansToTheBatcherWithoutTheOptIn` pins that nothing is flushed per turn without the variable. - `go build`, `go vet`, `gofmt` and `go test` over `./adk/...`, and `make -C go lint`, are clean. Signed-off-by: David Haifley <david.haifley@solo.io>
D
David Haifley committed
6a6b54ffd1fa0617a6bfc2822bc19c4ed8c5f9da
Parent: f378edf
Committed by GitHub <noreply@github.com>
on 9/10/2026, 10:36:00 PM