feat(telemetry): implement ADR 0050 Level 3 content capture - #6429
Conversation
E2E tests did not runE2E tests run automatically for org/repo members and collaborators on pull requests. For other contributors, a maintainer must add the See E2E testing guide for details. |
1 similar comment
E2E tests did not runE2E tests run automatically for org/repo members and collaborators on pull requests. For other contributors, a maintainer must add the See E2E testing guide for details. |
Site previewPreview: https://eca2d23e-site.fullsend-ai.workers.dev Commit: |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
5a11d23 to
589a882
Compare
PR Summary by Qodofeat(telemetry): implement Level 3 content capture on agent spans
AI Description
Diagram
High-Level Assessment
Files changed (12)
|
Code Review by Qodo
1.
|
|
You need to add the variable to the workflows and add the variable to the user guide, follow the same style and tone of the guide. It shouldn't take more than 7 or 8 lines. |
|
LGTM but I would want to see a review instrumented trace, as they are very complex. |
|
Fixed in 829bd9e — |
|
Review-instrumented trace, as requested: What it shows: the |
|
On the $10.57 for |
dbf888d to
0f8865a
Compare
Signed-off-by: Dharit Shah <dhshah@redhat.com>
Signed-off-by: Dharit Shah <dhshah@redhat.com>
Signed-off-by: Dharit Shah <dhshah@redhat.com>
…ation Add OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT to the managed workflows variable table and the bring-your-own-workflow env block so both match what the managed agent steps now forward. Signed-off-by: Dharit Shah <dhshah@redhat.com>
…i#6502 Also adapt the bounded-model test to agentSpanEndAttrs's new runtimeName parameter from the runtime-selection work (fullsend-ai#6526). Signed-off-by: Dharit Shah <dhshah@redhat.com>
0f8865a to
94c3c4b
Compare
|
Merge-queue dequeue was two unrelated infra flakes, not the diff: functional |
The final review gauntlet confirmed three mechanisms the per-id bound alone left open. Ids serialize into the attribute but counted toward nothing, so their bytes bypassed the budget in aggregate — partSize now includes them, making the dropped-byte accounting exact over every serialized part byte, and the suffix boundary reserves a part's id bytes before fitting its response tail. Ids were also the only stream-derived string never passed through the redaction pipeline — they are scanned now, and a finding drops the id entirely rather than substituting one that could falsely collide. Parts with no content-bearing bytes are refused at Handle: they contributed nothing to output yet accumulated unboundedly, invisible to size-based eviction. Also disclose two residuals instead of implying their absence: the marshaled attribute carries JSON syntax/escaping above the counted budget (pre-existing fullsend-ai#6429 semantics), and stream lines beyond 1MiB are skipped whole — newly lossy for tool results, noted at the skip site. Signed-off-by: Dharit Shah <dhshah@redhat.com>
The final review gauntlet confirmed three mechanisms the per-id bound alone left open. Ids serialize into the attribute but counted toward nothing, so their bytes bypassed the budget in aggregate — partSize now includes them, making the dropped-byte accounting exact over every serialized part byte, and the suffix boundary reserves a part's id bytes before fitting its response tail. Ids were also the only stream-derived string never passed through the redaction pipeline — they are scanned now, and a finding drops the id entirely rather than substituting one that could falsely collide. Parts with no content-bearing bytes are refused at Handle: they contributed nothing to output yet accumulated unboundedly, invisible to size-based eviction. Also disclose two residuals instead of implying their absence: the marshaled attribute carries JSON syntax/escaping above the counted budget (pre-existing fullsend-ai#6429 semantics), and stream lines beyond 1MiB are skipped whole — newly lossy for tool results, noted at the skip site. Signed-off-by: Dharit Shah <dhshah@redhat.com>
The final review gauntlet confirmed three mechanisms the per-id bound alone left open. Ids serialize into the attribute but counted toward nothing, so their bytes bypassed the budget in aggregate — partSize now includes them, making the dropped-byte accounting exact over every serialized part byte, and the suffix boundary reserves a part's id bytes before fitting its response tail. Ids were also the only stream-derived string never passed through the redaction pipeline — they are scanned now, and a finding drops the id entirely rather than substituting one that could falsely collide. Parts with no content-bearing bytes are refused at Handle: they contributed nothing to output yet accumulated unboundedly, invisible to size-based eviction. Also disclose two residuals instead of implying their absence: the marshaled attribute carries JSON syntax/escaping above the counted budget (pre-existing fullsend-ai#6429 semantics), and stream lines beyond 1MiB are skipped whole — newly lossy for tool results, noted at the skip site. Signed-off-by: Dharit Shah <dhshah@redhat.com>
The final review gauntlet confirmed three mechanisms the per-id bound alone left open. Ids serialize into the attribute but counted toward nothing, so their bytes bypassed the budget in aggregate — partSize now includes them, making the dropped-byte accounting exact over every serialized part byte, and the suffix boundary reserves a part's id bytes before fitting its response tail. Ids were also the only stream-derived string never passed through the redaction pipeline — they are scanned now, and a finding drops the id entirely rather than substituting one that could falsely collide. Parts with no content-bearing bytes are refused at Handle: they contributed nothing to output yet accumulated unboundedly, invisible to size-based eviction. Also disclose two residuals instead of implying their absence: the marshaled attribute carries JSON syntax/escaping above the counted budget (pre-existing fullsend-ai#6429 semantics), and stream lines beyond 1MiB are skipped whole — newly lossy for tool results, noted at the skip site. Signed-off-by: Dharit Shah <dhshah@redhat.com>
The final review gauntlet confirmed three mechanisms the per-id bound alone left open. Ids serialize into the attribute but counted toward nothing, so their bytes bypassed the budget in aggregate — partSize now includes them, making the dropped-byte accounting exact over every serialized part byte, and the suffix boundary reserves a part's id bytes before fitting its response tail. Ids were also the only stream-derived string never passed through the redaction pipeline — they are scanned now, and a finding drops the id entirely rather than substituting one that could falsely collide. Parts with no content-bearing bytes are refused at Handle: they contributed nothing to output yet accumulated unboundedly, invisible to size-based eviction. Also disclose two residuals instead of implying their absence: the marshaled attribute carries JSON syntax/escaping above the counted budget (pre-existing fullsend-ai#6429 semantics), and stream lines beyond 1MiB are skipped whole — newly lossy for tool results, noted at the skip site. Signed-off-by: Dharit Shah <dhshah@redhat.com>
The final review gauntlet confirmed three mechanisms the per-id bound alone left open. Ids serialize into the attribute but counted toward nothing, so their bytes bypassed the budget in aggregate — partSize now includes them, making the dropped-byte accounting exact over every serialized part byte, and the suffix boundary reserves a part's id bytes before fitting its response tail. Ids were also the only stream-derived string never passed through the redaction pipeline — they are scanned now, and a finding drops the id entirely rather than substituting one that could falsely collide. Parts with no content-bearing bytes are refused at Handle: they contributed nothing to output yet accumulated unboundedly, invisible to size-based eviction. Also disclose two residuals instead of implying their absence: the marshaled attribute carries JSON syntax/escaping above the counted budget (pre-existing fullsend-ai#6429 semantics), and stream lines beyond 1MiB are skipped whole — newly lossy for tool results, noted at the skip site. Signed-off-by: Dharit Shah <dhshah@redhat.com>
The final review gauntlet confirmed three mechanisms the per-id bound alone left open. Ids serialize into the attribute but counted toward nothing, so their bytes bypassed the budget in aggregate — partSize now includes them, making the dropped-byte accounting exact over every serialized part byte, and the suffix boundary reserves a part's id bytes before fitting its response tail. Ids were also the only stream-derived string never passed through the redaction pipeline — they are scanned now, and a finding drops the id entirely rather than substituting one that could falsely collide. Parts with no content-bearing bytes are refused at Handle: they contributed nothing to output yet accumulated unboundedly, invisible to size-based eviction. Also disclose two residuals instead of implying their absence: the marshaled attribute carries JSON syntax/escaping above the counted budget (pre-existing fullsend-ai#6429 semantics), and stream lines beyond 1MiB are skipped whole — newly lossy for tool results, noted at the skip site. Signed-off-by: Dharit Shah <dhshah@redhat.com>
The final review gauntlet confirmed three mechanisms the per-id bound alone left open. Ids serialize into the attribute but counted toward nothing, so their bytes bypassed the budget in aggregate — partSize now includes them, making the dropped-byte accounting exact over every serialized part byte, and the suffix boundary reserves a part's id bytes before fitting its response tail. Ids were also the only stream-derived string never passed through the redaction pipeline — they are scanned now, and a finding drops the id entirely rather than substituting one that could falsely collide. Parts with no content-bearing bytes are refused at Handle: they contributed nothing to output yet accumulated unboundedly, invisible to size-based eviction. Also disclose two residuals instead of implying their absence: the marshaled attribute carries JSON syntax/escaping above the counted budget (pre-existing fullsend-ai#6429 semantics), and stream lines beyond 1MiB are skipped whole — newly lossy for tool results, noted at the skip site. Signed-off-by: Dharit Shah <dhshah@redhat.com>
Review of the Level 3 tool-result capture asked why tool calls are parts of the message record rather than spans. Nothing had decided that: ADR 0050 never named the spans and left granularity to fullsend-ai#294, and at semconv v1.37.0 the execute_tool span is metadata-only, so the message record was the only conventional home for content. Both shapes fit together, and this adds the spans. toolSpanTracker (internal/cli/tool_spans.go) opens an `execute_tool <tool name>` span under the iteration's agent span when the runtime reports a call and ends it when the result arrives — runner-side receipt at both ends, one clock: tool_use lines carry no timestamp, tool_result lines carry a sandbox-clock one the parser ignores, and the start is arguments-complete rather than execution start. Attributes per v1.37.0: gen_ai.operation.name, gen_ai.tool.name (redacted, then bounded to 256 bytes), gen_ai.tool.call.id (ids beyond 256 bytes are dropped, never truncated); a result flagged is_error sets error.type=tool_error and status Error. A call still open when the iteration ends — the runtime was stopped, or its result line exceeded the parser's 1 MiB cap — is closed as error.type=unanswered; a result for a call never reported becomes a near-zero-duration span marked fullsend.tool.unmatched; events without an id produce no span: pi and codex streams, and server-side tools, whose result never arrives as a tool_result, so the parser now leaves their id empty. At most 1,024 spans are recorded per iteration — Finish ends every open call in a burst right before the agent span ends, and an agent-controlled flood would otherwise fill the OTLP batch queue and evict the agent span; the overflow is recorded as fullsend.tool_spans.dropped on that span. Tool content stays on gen_ai.output.messages, the scorer contract. The spans are Level 1 metadata, so RunParams.OnEvent is now always installed: iterationEventHandler calls the renderer first (console output unchanged), then the content collector, then the tracker. The nil-handler invariant from fullsend-ai#6429 no longer holds; its test is replaced. ADR 0102 records the topology, the runtime-native OpenTelemetry route it declines for now, and the sub-agent nesting it leaves deferred; it settles the granularity question in fullsend-ai#294 and scopes retention and access out. Docs: tracing reference, dev guide, user guide, runtimes matrix, architecture.md, the observability problem doc, an annotation on ADR 0050. Raised by rh-hemartin's review. Signed-off-by: Dharit Shah <dhshah@redhat.com>
The final review gauntlet confirmed three mechanisms the per-id bound alone left open. Ids serialize into the attribute but counted toward nothing, so their bytes bypassed the budget in aggregate — partSize now includes them, making the dropped-byte accounting exact over every serialized part byte, and the suffix boundary reserves a part's id bytes before fitting its response tail. Ids were also the only stream-derived string never passed through the redaction pipeline — they are scanned now, and a finding drops the id entirely rather than substituting one that could falsely collide. Parts with no content-bearing bytes are refused at Handle: they contributed nothing to output yet accumulated unboundedly, invisible to size-based eviction. Also disclose two residuals instead of implying their absence: the marshaled attribute carries JSON syntax/escaping above the counted budget (pre-existing fullsend-ai#6429 semantics), and stream lines beyond 1MiB are skipped whole — newly lossy for tool results, noted at the skip site. Signed-off-by: Dharit Shah <dhshah@redhat.com>
Review of the Level 3 tool-result capture asked why tool calls are parts of the message record rather than spans. Nothing had decided that: ADR 0050 never named the spans and left granularity to fullsend-ai#294, and at semconv v1.37.0 the execute_tool span is metadata-only, so the message record was the only conventional home for content. Both shapes fit together, and this adds the spans. toolSpanTracker (internal/cli/tool_spans.go) opens an `execute_tool <tool name>` span under the iteration's agent span when the runtime reports a call and ends it when the result arrives — runner-side receipt at both ends, one clock: tool_use lines carry no timestamp, tool_result lines carry a sandbox-clock one the parser ignores, and the start is arguments-complete rather than execution start. Attributes per v1.37.0: gen_ai.operation.name, gen_ai.tool.name (redacted, then bounded to 256 bytes), gen_ai.tool.call.id (ids beyond 256 bytes are dropped, never truncated); a result flagged is_error sets error.type=tool_error and status Error. A call still open when the iteration ends — the runtime was stopped, or its result line exceeded the parser's 1 MiB cap — is closed as error.type=unanswered; a result for a call never reported becomes a near-zero-duration span marked fullsend.tool.unmatched; events without an id produce no span: pi and codex streams, and server-side tools, whose result never arrives as a tool_result, so the parser now leaves their id empty. At most 1,024 spans are recorded per iteration — Finish ends every open call in a burst right before the agent span ends, and an agent-controlled flood would otherwise fill the OTLP batch queue and evict the agent span; the overflow is recorded as fullsend.tool_spans.dropped on that span. Tool content stays on gen_ai.output.messages, the scorer contract. The spans are Level 1 metadata, so RunParams.OnEvent is now always installed: iterationEventHandler calls the renderer first (console output unchanged), then the content collector, then the tracker. The nil-handler invariant from fullsend-ai#6429 no longer holds; its test is replaced. ADR 0102 records the topology, the runtime-native OpenTelemetry route it declines for now, and the sub-agent nesting it leaves deferred; it settles the granularity question in fullsend-ai#294 and scopes retention and access out. Docs: tracing reference, dev guide, user guide, runtimes matrix, architecture.md, the observability problem doc, an annotation on ADR 0050. Raised by rh-hemartin's review. Signed-off-by: Dharit Shah <dhshah@redhat.com>
The final review gauntlet confirmed three mechanisms the per-id bound alone left open. Ids serialize into the attribute but counted toward nothing, so their bytes bypassed the budget in aggregate — partSize now includes them, making the dropped-byte accounting exact over every serialized part byte, and the suffix boundary reserves a part's id bytes before fitting its response tail. Ids were also the only stream-derived string never passed through the redaction pipeline — they are scanned now, and a finding drops the id entirely rather than substituting one that could falsely collide. Parts with no content-bearing bytes are refused at Handle: they contributed nothing to output yet accumulated unboundedly, invisible to size-based eviction. Also disclose two residuals instead of implying their absence: the marshaled attribute carries JSON syntax/escaping above the counted budget (pre-existing fullsend-ai#6429 semantics), and stream lines beyond 1MiB are skipped whole — newly lossy for tool results, noted at the skip site. Signed-off-by: Dharit Shah <dhshah@redhat.com>
Review of the Level 3 tool-result capture asked why tool calls are parts of the message record rather than spans. Nothing had decided that: ADR 0050 never named the spans and left granularity to fullsend-ai#294, and at semconv v1.37.0 the execute_tool span is metadata-only, so the message record was the only conventional home for content. Both shapes fit together, and this adds the spans. toolSpanTracker (internal/cli/tool_spans.go) opens an `execute_tool <tool name>` span under the iteration's agent span when the runtime reports a call and ends it when the result arrives — runner-side receipt at both ends, one clock: tool_use lines carry no timestamp, tool_result lines carry a sandbox-clock one the parser ignores, and the start is arguments-complete rather than execution start. Attributes per v1.37.0: gen_ai.operation.name, gen_ai.tool.name (redacted, then bounded to 256 bytes), gen_ai.tool.call.id (ids beyond 256 bytes are dropped, never truncated); a result flagged is_error sets error.type=tool_error and status Error. A call still open when the iteration ends — the runtime was stopped, or its result line exceeded the parser's 1 MiB cap — is closed as error.type=unanswered; a result for a call never reported becomes a near-zero-duration span marked fullsend.tool.unmatched; events without an id produce no span: pi and codex streams, and server-side tools, whose result never arrives as a tool_result, so the parser now leaves their id empty. At most 1,024 spans are recorded per iteration — Finish ends every open call in a burst right before the agent span ends, and an agent-controlled flood would otherwise fill the OTLP batch queue and evict the agent span; the overflow is recorded as fullsend.tool_spans.dropped on that span. Tool content stays on gen_ai.output.messages, the scorer contract. The spans are Level 1 metadata, so RunParams.OnEvent is now always installed: iterationEventHandler calls the renderer first (console output unchanged), then the content collector, then the tracker. The nil-handler invariant from fullsend-ai#6429 no longer holds; its test is replaced. ADR 0102 records the topology, the runtime-native OpenTelemetry route it declines for now, and the sub-agent nesting it leaves deferred; it settles the granularity question in fullsend-ai#294 and scopes retention and access out. Docs: tracing reference, dev guide, user guide, runtimes matrix, architecture.md, the observability problem doc, an annotation on ADR 0050. Raised by rh-hemartin's review. Signed-off-by: Dharit Shah <dhshah@redhat.com>
The final review gauntlet confirmed three mechanisms the per-id bound alone left open. Ids serialize into the attribute but counted toward nothing, so their bytes bypassed the budget in aggregate — partSize now includes them, making the dropped-byte accounting exact over every serialized part byte, and the suffix boundary reserves a part's id bytes before fitting its response tail. Ids were also the only stream-derived string never passed through the redaction pipeline — they are scanned now, and a finding drops the id entirely rather than substituting one that could falsely collide. Parts with no content-bearing bytes are refused at Handle: they contributed nothing to output yet accumulated unboundedly, invisible to size-based eviction. Also disclose two residuals instead of implying their absence: the marshaled attribute carries JSON syntax/escaping above the counted budget (pre-existing fullsend-ai#6429 semantics), and stream lines beyond 1MiB are skipped whole — newly lossy for tool results, noted at the skip site. Signed-off-by: Dharit Shah <dhshah@redhat.com>
Review of the Level 3 tool-result capture asked why tool calls are parts of the message record rather than spans. Nothing had decided that: ADR 0050 never named the spans and left granularity to fullsend-ai#294, and at semconv v1.37.0 the execute_tool span is metadata-only, so the message record was the only conventional home for content. Both shapes fit together, and this adds the spans. toolSpanTracker (internal/cli/tool_spans.go) opens an `execute_tool <tool name>` span under the iteration's agent span when the runtime reports a call and ends it when the result arrives — runner-side receipt at both ends, one clock: tool_use lines carry no timestamp, tool_result lines carry a sandbox-clock one the parser ignores, and the start is arguments-complete rather than execution start. Attributes per v1.37.0: gen_ai.operation.name, gen_ai.tool.name (redacted, then bounded to 256 bytes), gen_ai.tool.call.id (ids beyond 256 bytes are dropped, never truncated); a result flagged is_error sets error.type=tool_error and status Error. A call still open when the iteration ends — the runtime was stopped, or its result line exceeded the parser's 1 MiB cap — is closed as error.type=unanswered; a result for a call never reported becomes a near-zero-duration span marked fullsend.tool.unmatched; events without an id produce no span: pi and codex streams, and server-side tools, whose result never arrives as a tool_result, so the parser now leaves their id empty. At most 1,024 spans are recorded per iteration — Finish ends every open call in a burst right before the agent span ends, and an agent-controlled flood would otherwise fill the OTLP batch queue and evict the agent span; the overflow is recorded as fullsend.tool_spans.dropped on that span. Tool content stays on gen_ai.output.messages, the scorer contract. The spans are Level 1 metadata, so RunParams.OnEvent is now always installed: iterationEventHandler calls the renderer first (console output unchanged), then the content collector, then the tracker. The nil-handler invariant from fullsend-ai#6429 no longer holds; its test is replaced. ADR 0102 records the topology, the runtime-native OpenTelemetry route it declines for now, and the sub-agent nesting it leaves deferred; it settles the granularity question in fullsend-ai#294 and scopes retention and access out. Docs: tracing reference, dev guide, user guide, runtimes matrix, architecture.md, the observability problem doc, an annotation on ADR 0050. Raised by rh-hemartin's review. Signed-off-by: Dharit Shah <dhshah@redhat.com>
The final review gauntlet confirmed three mechanisms the per-id bound alone left open. Ids serialize into the attribute but counted toward nothing, so their bytes bypassed the budget in aggregate — partSize now includes them, making the dropped-byte accounting exact over every serialized part byte, and the suffix boundary reserves a part's id bytes before fitting its response tail. Ids were also the only stream-derived string never passed through the redaction pipeline — they are scanned now, and a finding drops the id entirely rather than substituting one that could falsely collide. Parts with no content-bearing bytes are refused at Handle: they contributed nothing to output yet accumulated unboundedly, invisible to size-based eviction. Also disclose two residuals instead of implying their absence: the marshaled attribute carries JSON syntax/escaping above the counted budget (pre-existing fullsend-ai#6429 semantics), and stream lines beyond 1MiB are skipped whole — newly lossy for tool results, noted at the skip site. Signed-off-by: Dharit Shah <dhshah@redhat.com>
Review of the Level 3 tool-result capture asked why tool calls are parts of the message record rather than spans. Nothing had decided that: ADR 0050 never named the spans and left granularity to fullsend-ai#294, and at semconv v1.37.0 the execute_tool span is metadata-only, so the message record was the only conventional home for content. Both shapes fit together, and this adds the spans. toolSpanTracker (internal/cli/tool_spans.go) opens an `execute_tool <tool name>` span under the iteration's agent span when the runtime reports a call and ends it when the result arrives — runner-side receipt at both ends, one clock: tool_use lines carry no timestamp, tool_result lines carry a sandbox-clock one the parser ignores, and the start is arguments-complete rather than execution start. Attributes per v1.37.0: gen_ai.operation.name, gen_ai.tool.name (redacted, then bounded to 256 bytes), gen_ai.tool.call.id (ids beyond 256 bytes are dropped, never truncated); a result flagged is_error sets error.type=tool_error and status Error. A call still open when the iteration ends — the runtime was stopped, or its result line exceeded the parser's 1 MiB cap — is closed as error.type=unanswered; a result for a call never reported becomes a near-zero-duration span marked fullsend.tool.unmatched; events without an id produce no span: pi and codex streams, and server-side tools, whose result never arrives as a tool_result, so the parser now leaves their id empty. At most 1,024 spans are recorded per iteration — Finish ends every open call in a burst right before the agent span ends, and an agent-controlled flood would otherwise fill the OTLP batch queue and evict the agent span; the overflow is recorded as fullsend.tool_spans.dropped on that span. Tool content stays on gen_ai.output.messages, the scorer contract. The spans are Level 1 metadata, so RunParams.OnEvent is now always installed: iterationEventHandler calls the renderer first (console output unchanged), then the content collector, then the tracker. The nil-handler invariant from fullsend-ai#6429 no longer holds; its test is replaced. ADR 0102 records the topology, the runtime-native OpenTelemetry route it declines for now, and the sub-agent nesting it leaves deferred; it settles the granularity question in fullsend-ai#294 and scopes retention and access out. Docs: tracing reference, dev guide, user guide, runtimes matrix, architecture.md, the observability problem doc, an annotation on ADR 0050. Raised by rh-hemartin's review. Signed-off-by: Dharit Shah <dhshah@redhat.com>
Implements ADR 0050's Level 3 exactly as accepted: when the org sets
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, conversation content is included in spans. One environment variable, the Level 2 enablement pattern — no new ADR, no additional consent surface.The agent runtime's native content telemetry is never enabled. Content is assembled by fullsend's own runner from the normalized event stream the console already renders, redacted through
security.OutputPipeline()at assembly, and attached to the per-iterationagentspan. Fullsend reads the gate variable itself — it is a GenAI-instrumentation convention documented by opentelemetry-python-contrib; the pinned semconv v1.37.0 release does not define it, and Claude Code does not read it (0 occurrences in the shipped binary). Fullsend never sets the runtime's ownOTEL_LOG_*variables, so there is no second export pipeline and no redaction bypass.What ships
internal/telemetry/content.gointernal/cli/content_collector.gogen_ai.output.messagesattribute per iterationinternal/telemetry/telemetry.goboundedStringAttr;Setupwarns if a finite operator limit would cut content JSONinternal/cli/run.goRunParams.OnEventwith the console renderer preserved; gate off =OnEventnil = the pre-change path, byte-identicalGate values
true,span_only,span_and_event(case-insensitive)false,NO_CONTENT,event_only(we cannot honor "only"), anything unrecognizedContent shape. One assistant message per iteration, conformant to the v1.37.0 output-messages JSON schema including the required
finish_reason(stop/errorfrom the iteration outcome). Parts:text,reasoning(the schema's GenericPart extension point), andtool_callwith name + summary — no fabricatedarguments.Redaction and size. Redaction runs before any cut (truncating first could split a secret); hits are masked, counted in
fullsend.content.redactions, and warned on the console. The 256 KiB budget keeps an ordered suffix — the final answer survives a cut — with exact dropped-byte accounting surfaced asfullsend.content.truncated/fullsend.content.dropped_bytes, and early eviction keeps long sessions memory-bounded.Deliberately not captured (documented as dispositions)
gen_ai.input.messages— first-iteration runs have no meaningful runner-side input; retry iterations now carry the injected validation feedback (Validation feedback is injected into the agent prompt unframed and unsanitized (feedback_mode: append) #6502), which makes input capture a natural follow-up in this series.Next in this series (PR B, starts after this merges)
Tool results complete the captured record. One PR, three parts:
ToolResultEventininternal/runtime, emitted from thetool_resultblocks the Claude stream parser currently discards, as a runtime-agnostic addition to the normalizedAgentEventcontract.tool_call_responsepart ({type, id, result}); the gate, redaction, suffix budget, and markers in this PR handle it with no new surface (still one env var).The parser half ships together with its consumer rather than ahead of it, so no event type lands without production callers.
Evidence (pilot MLflow, experiment 1)
tr-5802e956713c146edef284bca8a0d338finish_reason=stop, byte-identical inrun-telemetry.jsonland on the backend; console rendered normally through the teetr-a1703820bb276f1121a9f808d0bde07etr-4a4ce9d96307f239880d5e2ba1802f72Test plan
go test ./internal/telemetry/ ./internal/cli/ -race— green. (The twoTestDummyRuntime_*failures on the author's machine are pre-existing environment flakes present on bare main.)telemetry.Setup()file sink for both gate states.Notes for review
Relates to #5361 (content capture is the last telemetry level; the enrichment legs remain open). Relates to #6036 (eval scorers read
run-telemetry.jsonl; content-aware scorers become possible once this lands). Relates to #294 (retention; unchanged by this PR).