Repository navigation
feat: record LLM input as deltas on trace spans - #7605
Merged
Merged
Conversation
Add RecordingOptions.input_truncation (off by default). When on, a span's gen_ai.input.messages / lk.pii.chat_ctx hold only the conversation items appended since the last committed generation, and gen_ai.system_instructions is left out while unchanged. Truncated spans point at their base span via lk.input.* attributes and a span link. Only scheduled generations move the baseline, so discarded preemptive generations never consume the delta. Items are fingerprinted when committed; any edit, removal or reorder falls back to the full context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fingerprints Every ChatItem type now derives from a fieldless _ChatItemBase and declares its _essential_fields() once. ChatContext.is_equivalent compares them and _fingerprint() hashes them, replacing the per-type branches in is_equivalent and the type switch in telemetry. is_equivalent's behavior is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing is truncated: the model always receives the full input, and spans record only what changed since the last committed generation, pointing at the span that holds the rest. Rename the option to RecordingOptions.input_delta, the marker attribute to lk.input.delta, and the helpers to InputDeltaTracker / InputDeltaScope / input_delta() / InputDelta, with docstrings describing what each does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
davidzhao
marked this pull request as ready for review
October 3, 2026 21:12
lk.pii.chat_ctx keeps system messages among the conversation, so a delta that replaced the instructions could not be rebuilt by following the base chain: the base's old system message and the new one both survived. The llm_node span now records either the full context or only appended items (instructions unchanged). gen_ai.* keeps its message delta, since its instructions are a separate attribute. Add a test that rebuilds each span's input from the lk.input.* chain and compares it with what was sent, for both attributes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chenghao-mou
reviewed
Oct 4, 2026
_PendingBaseline now stores a _MessagesBaseline and an _InstructionsBaseline, so committing a generation just stores them. delta() also builds the item keys once and compares the baseline against their prefix, instead of hashing the conversation twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the system/developer messages a chat context starts with are the agent's instructions. A later one (generate_reply(instructions=...), the expressive guide) exists for a single turn and is sent in place by chat-completion providers, so it is now part of gen_ai.input.messages (role "system") instead of gen_ai.system_instructions. Input deltas leave per-turn system messages out of the comparison and record them where the turn has them. Previously the guide appended to every turn stayed where the base span recorded it, so the rebuilt conversation put the new turns after it. To rebuild: the base's conversation without its system messages, followed by this span's. InputDelta keeps instructions and conversation apart, since a delta may start with a per-turn system message. The site and its record shape travel together as InputDeltaSite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chenghao-mou
reviewed
Oct 4, 2026
| span_context = span.get_span_context() | ||
|
|
||
| messages = _MessagesBaseline( | ||
| item_keys=_item_keys([i for i in conversation if not _is_system_message(i)]), |
Member
There was a problem hiding this comment.
nit: some provider format formats the non-first system message as an user message, so the otel data do not reflect the actual request. Example: a generate_reply(instructions=...) greeting, [I, greet]:
telemetry gen_ai.system_instructions: ['be brief', 'greet the user']
anthropic system: ['be brief']
anthropic messages: [user: '<instructions>greet the user</instructions>']
A span now continues the input recorded for the last committed generation, its parent: it records what follows the longest prefix they share, and leaves out instructions identical to the parent's. There is no separate instructions pointer and no assumption about which system messages last a single turn, so a persistent system message (added through update_chat_ctx), a per-turn RAG message and an edit mid-history all stay deltas. lk.pii.chat_ctx counts chat items (lk.input.base_items) and gen_ai.input.messages counts messages (lk.input.base_messages), never cutting a tool call from its message. gen_ai.system_instructions now holds only the agent's instructions message (or, without one, the system message the context starts with); every other system message stays in gen_ai.input.messages in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tracker gets its own module, whose docstring describes the design in ASD-STE100: terms, the procedure for each span, the rebuild rule, the rules for system messages, edits and tool calls, and an expressive-mode example (checked by a test). Replace lk.input.base_items / lk.input.base_messages with one lk.input.dropped_from_base: the number of entries removed from the end of the parent's record before this span's are appended. Each span name has one record (lk.pii.chat_ctx on llm_node, gen_ai.input.messages on llm_request), so one count is enough, and it stays small however long the history is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eltas A tool call merges into the assistant message before it even when a skipped item (such as the AgentConfigUpdate from update_tools) sits between them. _shared_prefix stopped backtracking at the skipped item, so the delta rendered the call as a separate assistant message. A cut is now valid only when the next item that lands in a message starts a new one, on both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chenghao-mou
approved these changes
Oct 5, 2026
The placeholder output got a new random id every turn, so while a tool kept running across turns, each input delta stopped at it and re-recorded everything after it. The placeholder call reused the real call's id, and its only difference (the flag in extra) is not part of the fingerprint, so once the tool finished, the rebuilt lk.pii.chat_ctx kept the flagged copy. The pair now uses "<call id>_running" and "<call id>_running_output": the same on every turn while the tool runs, and distinct from the real call and output that replace it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
At each turn, the LLM spans record all of the conversation and all of the system instructions. Thus, the size of the spans increases at each turn of a call.
This change adds an option. When you set the option, the spans record only the input that changed. The model always receives all of the input. The option changes only the telemetry.
The option is off by default. When the option is off, the spans do not change.
Design
The module
telemetry/input_delta.pycontains the full description. This is a summary.Terms
llm_nodespan, the record islk.pii.chat_ctx, with one entry for each chat item. On anllm_requestspan, the record isgen_ai.input.messages, with one entry for each GenAI message.llm_requestspan, the instructions are ingen_ai.system_instructions, not in the record. All other system messages are entries of the record, at their position. This change togen_ai.system_instructionsalso applies when the option is off.Procedure for each span
lk.input.dropped_from_basegives the number of parent entries after this prefix.llm_requestspan, if the instructions are the same as in the parent, the span does not recordgen_ai.system_instructions.Rebuild
lk.input.base_span_id). Use this procedure again if the parent also has a parent.lk.input.dropped_from_baseentries.If an
llm_requestspan with a parent has nogen_ai.system_instructions, use the instructions of the parent.Rules
gen_ai.input.messages, consecutive tool calls of an assistant turn are one message, also when a skipped item (for example, a config update) is between them. The prefix never stops inside such a message.llm_nodespan, the instructions are the first entry oflk.pii.chat_ctx. Thus, a change of the instructions causes a full record oflk.pii.chat_ctx.AgentActivityhas its own tracker. After a handoff, the first span records all of the input.Example (expressive mode)
llm_nodelk.input.dropped_from_base[I, u1, G][I, u1, G](full)[I, u1, a1, u2, G][a1, u2, G][I, …, u2, a2, u3, G][a2, u3, G][I, …, u3, G, fc, fo][fc, fo]Attributes on a delta span:
lk.input.delta,lk.input.base_span_id(and a span link to the parent),lk.input.dropped_from_base.Relation to #7539
This change replaces the delta mode of #7539. It keeps these items from #7539: content fingerprints, no input on delegating spans, and the test scenarios.
This change is different from #7539 in these items:
lk.pii.chat_ctxstays on the span as a delta. feat: add opt-in telemetry content export controls #7539 removes it.Refactor
Each
ChatItemtype now has the base class_ChatItemBase. This base class has no fields. Each type declares its essential fields one time in_essential_fields().ChatContext.is_equivalentcompares these fields, and_fingerprint()hashes them. The operation ofis_equivalentdoes not change. New tests intests/test_chat_ctx.pyexamine it.Tests
tests/test_input_delta.pycontains unit tests for the tracker. It also contains session tests: two turns with a tool step, the option off, and a fallback.