Repository navigation
Instrument prompt composition so the trace can answer "full of what" #2323
Description
Edited after @ARE404's review: the original body missed that
/contextalready ships this breakdown, and claimed a retention cost that does not exist. Both are corrected below; the thread holds the reasoning.
Problem
The session trace's context bar answers "how full is the context" but not "full of what". Today it can only split the prompt into cacheRead / fresh — a billing dimension, because that is the only breakdown SessionTrace carries. A reader who sees 84% full has no way to learn which tools they could trim.
This is a data gap, not a styling gap. #2289 took the panel as far as the current data allows; every further visual iteration there redistributes the same two numbers.
What already exists
More than the first draft of this issue credited.
PreparedRequestSegment(request-shape.ts:58) is captured per item at the provider seam — one segment per tool, per system instruction, per message — each withkind,index,bytes,cacheable,hashand (for messages)role.- Those segments are persisted whole on
provider_request_attempt_recorded(agent-run.ts:412). readLatestContextDiagnostics(context-diagnostics.ts) reads them back, folds the four kinds intosystem_instructions/tool_definitions/messages/other, and estimatesceil(bytes / 4).formatContextDiagnostics(pi-tui-runner.ts:2732) already renders that to a user, under "Estimated breakdown", every figure prefixed≈.
So the kind-fold and the estimate policy are settled and shipped in the CLI. What is missing for the desktop panel is a per-tool label and a read.
Settled
- Segment kinds stay four. The actionable resolution is not a fifth kind, it is inside
tool_schema: "tool definitions ≈ 40%" is a fact nobody can act on, "Bash ≈ 8k, WebFetch ≈ 6k" is a decision. The tool objects at the seam are ai-sdk's and already carryname(provider-request-telemetry.ts:617);preparedSegmentdiscards it. One optional field recovers it. messageis not split. Messages reach the seam already serialized, so skill / memory / subagent attribution would have to be carried down from assembly. Note the asymmetry:system_promptis cheap to split —system-prompt/has five named contributors upstream — if that split ever earns itself. Separate question, not this one.- Composition is never drawn inside the bar. It is an estimate over serialized JSON bytes summing to
requestBytes;cacheRead/freshare provider-reported and sum toinputTokens. They are not two views of one total, and stacking them would make the estimate read as a decomposition of the reported number — exactly what [RFC] Canonical model-call accounting: one ModelCallAttempt record per real provider request #1679 forbids. Inherit the CLI's structure: sourced figures, then a separate≈-prefixed block. - Absent composition renders as a gap, never as zero.
provider_request_attempt_recordedis appended best-effort (agent-run.ts:414) whileModelCallAttemptis deliberately durable, so "metered, but no composition on record" is reachable — as is the pi backend, which emits neither. This is the rule the coverage notice already implements one layer up. - The trace is the right home.
Retention: a segment list per attempt multiplies it.It does not.SessionTraceis not persisted —readSessionTrace(inspector-ipc-main.ts:83) projects it on every read from the run event ledger and already walks every event of every run filtering by type. Recognizing a second event type on that walk stores nothing new and costs approximately nothing, so no second read path is needed.
Gate
If the per-tool label is not added, this does not ship: the four-slice breakdown alone is not actionable, and #2289's bar is already the honest answer to "how full".
Path
label?: stringontool_schemasegments, from the tool's own name at capture — @ARE404.- Composition on the trace projection, read from the segments already on the ledger — @ARE404.
- Renderer: a
≈-labelled block below the bar, per-tool rows under the tool total.
(3) is stacked on (2) and cannot be built or reviewed without it.
Activity
ARE404 commentedon Aug 6, 2026
Before the three-PR path starts, one correction to "what already exists" that changes the size of the first PR: this breakdown already ships, in the CLI.
/context(#1580) renders exactly the per-category composition this issue asks for, from the same segments:provider_request_attempt_recorded(agent-run.ts:323—segmentsis a field ofProviderRequestAttemptRecord).readLatestContextDiagnostics(packages/runtime/src/context-diagnostics.ts) reads them back and folds the four kinds intosystem_instructions/tool_definitions/messages/other, each withbytesandestimatedTokens = ceil(bytes / 4).formatContextDiagnostics(packages/cli/src/pi-tui-runner.ts:2732) prints it under "Estimated breakdown".So the issue is right that
PreparedRequestSegmentthe type is only referenced byprovider-request-telemetry.tsandprovider-request-trace.ts— but the data already reaches a user-facing surface with a settled kind-fold and a settled estimate policy. The runtime PR of the sketched path is close to empty unless we decide to splitmessage.With that, on the four questions:
1. Segment kinds. The four-kind fold has shipped precedent, and I'd keep it — but the actionable resolution isn't a fifth kind, it's inside
tool_schema. Segments are already emitted per item (one per tool, one per message, each withindexandrole—request-shape.ts:199-219), so per-tool bytes exist at capture time;contextSegmentEstimatesis what discards them, at read. Adding an optional label (the tool name) totool_schemasegments is a small runtime change and is the only thing here that answers "which tools do I trim".Splitting
messageis the expensive direction and I don't think it's knowable at the seam:messagesarrives there as already-serializedunknown[], so skill / memory / subagent attribution has to be carried down from assembly. Worth noting the asymmetry — the system prompt does have named contributors upstream (packages/runtime/src/system-prompt/: personalization, project context, workspace instructions, session environment), so splittingsystem_promptis materially cheaper than splittingmessage, if a split is wanted at all.2. Bytes vs tokens — one hazard the bar has that the CLI's list doesn't. Segment
bytesis the byte length of each item's serialized JSON: it includes JSON syntax, and for image/attachment parts (base64)bytes / 4is not close to a token count. More importantly the two numbers sum to different things:requestBytescacheRead/freshsums to the provider-reportedinputTokensA list can hold both apart; one bar cannot — stacking them invites the reader to treat the estimate as a decomposition of the reported number, which is exactly the #1679 rule. The CLI's structure is the precedent to inherit: a "Usage" block where every line names its source (
provider-reported/request-model snapshot/calculated), and a separate "Estimated breakdown" block where every figure is prefixed≈. I'd render composition as its own labelled block under the bar, not as segments of it.3. Where it lands. I don't think it needs to be on the
SessionTracecontract at all.readLatestContextDiagnosticsalready demonstrates the alternative: a command-time read over the AgentRun ledger for the latest completed request only — no new field, no retention multiplication. Two caveats to state if we go that way:// ponytail: command-time O(session ledger)note; the Inspector reads on a refresh policy, not once per command, so that cost lands differently.provider_request_attempt_recordedis appended best-effort, whileModelCallAttemptis deliberately durable ("Durable, unlike the diagnostic attempt append above: this is the metering source of truth"). So "metered, but no composition on record" is a reachable state, and the panel has to render it as a gap rather than zero. Same for the pi backend, which emits neither record.4. What the reader does with it. Only the per-tool number is actionable. "Tools are 40%" doesn't tell anyone which tool to drop; "Bash 8k / WebFetch 6k" does. I'd make that the gate rather than a follow-up: if we're not adding the tool label, the four-slice breakdown probably doesn't earn the widening, and #2289's bar is already the honest answer to "how full".
Happy to take the instrumentation PR (tool label on
tool_schemasegments + a read the Inspector can call) if the shape above is what you want.Astro-Han commentedon Aug 6, 2026
Verified every claim in this against the source before replying — all six hold, and two of them change the plan.
On
/contextalready shipping this. Confirmed:contextSegmentEstimates(context-diagnostics.ts:142) folds the four kinds and estimatesceil(bytes / 4);formatContextDiagnosticsprints it under "Estimated breakdown" with a≈on every figure. You're right that the sketched runtime PR is close to empty. Thank you — the issue was written from the type's reference graph and missed that the data already lands on a user-facing surface.On the tool label. Confirmed, and it's cheaper than you stated: the tool objects reaching the seam are ai-sdk's, each already carrying
name(provider-request-telemetry.ts:617). The name is present at capture and discarded bypreparedSegment, which keeps onlykind/index/cacheable/hash/bytes/role. So it is one optional field, not a plumbing change.I agree with making it the gate. "Tool definitions ≈ 40%" is a fact nobody can act on; "Bash ≈ 8k, WebFetch ≈ 6k" is a decision. If we are not adding the label, #2289's bar is already the honest answer and this issue should close.
On not splitting
message. Agreed, and the asymmetry you noted is the useful part:system-prompt/has five named contributors upstream (personalization, project context, workspace instructions, session environment, sandbox context), so that split is available whenever it earns itself. Filing it as a separate question rather than scope here.On the two numbers. Agreed, and this is the constraint I'd write into the issue as non-negotiable: composition is an estimate over serialized JSON bytes and sums to
requestBytes;cacheRead/freshare provider-reported and sum toinputTokens. They are not two views of one total. Stacking them into one bar would make the estimate read as a decomposition of the reported number, which is exactly what #1679 forbids. The CLI's two-block structure — sourced figures, then a≈-prefixed estimate block — is the precedent to inherit.On where it lands — one correction, and it is mine. The issue's question 3 said a segment list per attempt "multiplies retention". That is wrong, and your command-time alternative was reasoning from it.
SessionTraceis not persisted:readSessionTrace(inspector-ipc-main.ts:83) projects it on every read from the run event ledger, and already walks every event of every run filtering by type. Recognizing a second event type on that same walk costs approximately nothing and stores nothing new. So there is no retention argument against putting composition on the trace, and no need for a second read path with its own O(session ledger) note.What does survive from your caveat is the durability asymmetry, and it is the sharper point:
provider_request_attempt_recordedis appended best-effort (agent-run.ts:414) whileModelCallAttemptis deliberately durable ("a record lost to a crashed flush is spend nothing else can reconstruct"). "Metered, but no composition on record" is therefore a reachable state, as is the pi backend emitting neither. The panel must render that as a gap. That is the same rule the coverage notice already implements one layer up, so the pattern exists.Revised shape, if this matches your read:
label?: stringontool_schemasegments, set from the tool's own name at capture.≈-labelled block below the bar, not bands inside it; per-tool rows under the tool total.Yours if you want it — the shape above is what I'd review against. Happy to take the renderer half if you'd rather stop at the runtime and contract.
feat(inspector): say what the context is full of, not just how full
feat(inspector): say what the context is full of, not just how full
fix(context): scope the inline-run gate, and teach two suites the new…
feat(inspector): say what the context is full of, not just how full
fix(context): scope the inline-run gate, and teach two suites the new…
feat(inspector): say what the context is full of, not just how full
fix(context): scope the inline-run gate, and teach two suites the new…
feat(context): say what the context is made of, at the owner that alr…
feat(context): say what the context is made of, at the owner that alr…
feat(inspector): say what the context is full of, not just how full (#…
Astro-Han commentedon Aug 23, 2026
Closing as completed by #2346. The merged implementation added per-tool composition labels, exposed the shared context diagnostics through the Runtime Host, rendered the breakdown in both Desktop and CLI, and kept the canonical model-call attempt as the durable authority with an atomic rebuildable projection. The final head had all required checks green, including Desktop E2E and Storybook.\n\nTriage and wording were prepared with OpenAI Codex and reviewed by the maintainer.