Conversation
576a03c to
52ace44
Compare
52ace44 to
a4d9542
Compare
a4d9542 to
1812e9f
Compare
…d capture Signed-off-by: David Haifley <[email protected]>
1812e9f to
c49922d
Compare
krisztianfekete
left a comment
There was a problem hiding this comment.
As discussed offline, we are targeting full OTel GenAI semconv compat as the goal for v1.x telemetry design. We merge this as an intermediate step, and work toward the redefined tracing contract right away.
Notes on making that easier:
- The invocation span has
gen_ai.agent.name,gen_ai.conversation.idandgen_ai.task.idbut is still nameda2a.requestand sets nogen_ai.operation.name, so every consumer has to recognise it by scope plus the harness information. - When the runtime telemetry contract declares a native harness kind, could this span be named
invoke_agent {gen_ai.agent.name}withgen_ai.operation.name=invoke_agent, leaving ADK ona2a.requestso it keeps emitting exactly oneinvoke_agentof its own? That is the semconv shape for an agent invocation we are building towards in v1.x.
…entions Signed-off-by: David Haifley <[email protected]>
49f65e4 to
4349194
Compare
|
@krisztianfekete Thanks for the review. When the runtime telemetry contract declares a native harness, the request span is now For full GenAI semconv compatibility, I moved the rest of the contract there. |
…bandoned request completion Signed-off-by: David Haifley <[email protected]>
Description
The Claude Code and Codex harnesses export traces, but a consumer cannot reliably tell which agent, conversation, task or user an exported span belongs to, and the span that anchors a turn does not always describe what actually happened. The
a2a.requestwrapper carries onlya2a.methodand, on some paths,a2a.task.state. It has no runtime marker, so a collector cannot distinguish a native harness turn from an ADK one without parsing a service name. It has no conversation or task identity, so opening the trace for a chat message means scanning a time window rather than looking one up. It ends unary requests even when execution is still running detached from the caller, and it never sets an error status, so a failed turn can read as a successful one.This change makes invocation identity a producer contract, expressed in the OpenTelemetry GenAI semantic conventions, and makes the wrapper's lifecycle describe the real outcome. It also adds the bounded, opt-in prompt and response capture that the existing
KAGENT_OTEL_CAPTURE_SENSITIVE_CONTENTswitch resolves in the controller but never reached the Go wrapper. The contract is written down indocs/architecture/telemetry.md.invoke_agentspan; an ADK wrapper stays a transport span. For Claude Code and Codex nothing beneath the wrapper describes the agent invocation, so each A2A request opensinvoke_agent <gen_ai.agent.name>withgen_ai.operation.name=invoke_agent. The ADK emitsinvoke_agentspans of its own, for the root agent and any sub-agent it transfers to, so its request span keeps the namea2a.requestwith the same identity attributes and no operation, and the wrapper never adds an invocation to what the ADK reports. A consumer counting turns countskagent.invocation.segment, which only the request span carries. Every tracer kagent creates now declares the semantic conventions schema URL. The conventions are pinned at 1.41.0 on purpose: it is the last release of the main conventions to carry the GenAI registry, so it is the last with a Go package of typed keys, and the constants ingo/pkg/tracingare taken from that package rather than spelled out so they cannot drift from the declared version. Only thekagent.*anda2a.*names are kagent's own.RuntimeTelemetrycarrieskagent.runtime(adk-go,claudeorcodex), the compiled agent name, its namespace-qualifiedgen_ai.agent.id, and for the harnesses thegen_ai.provider.nameandgen_ai.request.modelthe agent is compiled against. The harness compilers put it in the runtime configuration JSON they already generate, and the ADK runtime builds it from its own environment, so the same identity reaches every request span and every runtime resource. Both harness config versions move up one, since the runtime rejects a configuration it does not understand. A configuration without the section stays valid and leaves capture off, which keeps standalone harness validation working.gen_ai.request.modelon an agent span when the agent is bound to one model, which a compiled kagent agent is.tracing.NewResourceapplies the same owned identity afterOTEL_RESOURCE_ATTRIBUTES, and each adapter merges it, withservice.namespace, into that variable for its native child process, preserving unrelated user-supplied attributes and replacing only owned keys. A user-supplied runtime marker is no longer required, andOTEL_RESOURCE_ATTRIBUTESremains available for other tuning.tracing.Invocationis started by the transport interceptor with the static identity the runtime knows before execution begins, so a request rejected during validation still reports which agent rejected it. Execution adopts it when it starts, and from that point the transport stops completing it. That is the fix for nonblocking unary requests: a2a-gov2.5.0detaches execution from the caller's cancellation, so a response can be delivered while the task is still working, and the previous interceptor closed the span at that point. Completion is idempotent, so the executor and the response interceptor observing the same terminal event cannot double count. a2a-go runs no final interceptor callback for a streaming consumer that stopped reading, so an invocation the transport still owns is completed asabandonedwhen the request context ends without a quiescent event, rather than left open and never exported.gen_ai.conversation.id,a2a.task.idandkagent.invocation.segmentthrough one shared helper, and the harness executor completes the invocation at each execution boundary with the actual task state. Failures set an OpenTelemetry error status with a safe category inerror.type, never a provider response or captured content. A consumer that stops accepting events is recorded askagent.invocation.disposition=abandonedrather than as a cancellation, and cancellation is recorded only when a client requested it, with the segment exported before the canceled event is published. Failures that never publish a task event export before the error leaves the process, and a runner panic is recorded asruntime_panic, without the panic value, before it propagates. The interceptor setsenduser.idfrom the gateway-established identity thatUserIDCallInterceptorputs on the request, and leaves it absent when there is no trusted identity.ForceFlush, three seconds by default, so an unreachable collector costs at most that budget once per segment and never converts a successful result into a failure.gen_ai.input.messagesand the text it produced asgen_ai.output.messages, each a JSON array holding one message with one text part, the output message carrying thefinish_reasonthe conventions require (stop,tool_callfor a segment parked for input,error, or the disposition name), bounded byKAGENT_OTEL_MAX_CAPTURE_BYTES(16 KiB default, 64 KiB ceiling), preserving UTF-8 and reporting truncation inkagent.capture.input_truncatedandkagent.capture.output_truncated. The collector is allocated only when capture is on and the span is recording, and it discards further text once full, so memory stays proportional to the limit whatever the response length or delta count. Tool arguments, tool results, approval structures, the rest of the conversation and native stderr are never written to these attributes; a resumed segment records no input messages because its input is a structured decision.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENTinto every compiled runtime from the same setting that governs the harness wrapper,SPAN_ONLYwhen capture is on andfalsewhen it is off, and renders it whether or not it exports traces, so a runtime reaching a collector through its own settings still follows the decision. The mode form matters: adk-go v2.3.0 reads a plaintrueas log records only and putsgen_ai.input.messagesandgen_ai.output.messageson its model spans only for the span modes. The ADK Go runtime already read that variable before recording its older payload attributes on model spans, but nothing set it, so those payloads were recorded whatever the controller's setting said; they now follow it. The variable is controller-owned: the Claude and Codex compilers reject aHarness.spec.enventry with that name, and the kagent compiler replaces one with the controller's value. This changes the ADK default under kagent from capturing to not capturing.kagent.invocation.relationship=resume_origin. A link states a relationship; it does not reparent spans and it does not claim ownership of the token usage recorded under the originating segment.The ADK runtime's descendant spans keep the
kagent.user_id,gen_ai.task.id,gen_ai.conversation.idandkagent.app_namekeys they always carried, because the Python runtimes stamp the same keys and the two should change in one step. That step, an invocation span emitted by the Python executors, is a separate change. The unusedinvoke_agentmiddleware undergo/core/internal/a2ais removed, since the real one now exists.The compiled runtime configuration is versioned and both harness versions move up one, so a controller must not run ahead of its pinned harness images. That rollout requirement, and the limitations this change does not close, are stated in
docs/architecture/telemetry.md: a caller that disconnects between a2a-go dispatching execution and reading the subscription leaves that request's invocation completed from the transport side, and a request rejected by a transport interceptor before execution begins has no invocation span at all.Not in this PR, and kept separate on purpose. The missing late-ending Codex
session_task.turnandop.dispatch.turn_inputspans are a native export-boundary question that needs its own reproduction against the pinned binary before any cleanup refactor, and addinggen_ai.request.modelto Codex's usage span is a change inopenai/codexfollowed by a version bump here. Neither blocks the identity and lifecycle work, and both are easier to review on their own.Related Issues
None filed.
Testing
Change Type
Enhancement.