Repository navigation
fix(calibration): the sample carries the request's system prompt and tools (context-manager #323) - #763
Conversation
…tools The provider's input total counts the system prompt and the tool definitions the request sent; the strategy's estimate of its compile holds neither. reportRealInputTokens now gets them as its second argument (context-manager anima-research#323), so the strategy compares the total with the whole request. A context-manager without that argument ignores it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Automated review
Recommendation: merge. The review found nothing of medium or high severity.
Reviewed commit 0a0c71f with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This fits. It's a narrow fix to calibration the framework already does: at the one call site that already reports the provider's input total to the strategy, it also passes the request's system prompt and tool definitions.
4 reasons with sources
It comes with a test and a changelog fragment, and it declares its companion PR the way CONTRIBUTING.md asks.
- It fixes calibration the framework already does, in the place it already does it.
reportRealInputTokensis called at only one place in src/, the per-call usage handling in driveStream. The change adds an optional second argument there and widens the existing duck type of the strategy to match. It adds no new component and no second way of doing calibration. (src/framework.ts (driveStream usage site)) - The companion PR is declared with merge-order guidance, as the contributing guide asks. The body names context-manager#324, says the two are safe to merge in either order, and says what happens if only one lands (an older strategy ignores the argument). That is the guidance CONTRIBUTING.md requires because this repo sits on context-manager. (CONTRIBUTING.md, How changes land)
- It keeps to the boundary between the framework and context-manager. The framework only passes along what the request sent. Pricing that overhead into the estimate stays with context-manager's strategy, in the companion PR. (CONTRIBUTING.md (ecosystem description))
- The PR has the shape the guide asks for: a test, numeric suite counts, a Not verified section and a changelog fragment. The body reports 6634 pass / 0 fail / 45 skipped, says it was not run against a live provider or against #324's build, and adds
changelog.d/calibration-request-overhead.fixed.md. The review's note that thefixedfragment has no effect with the pinned context-manager is about how the entry is worded, not about whether the change belongs here. (CONTRIBUTING.md, What a PR should contain / Changelog)
Findings
No findings of medium or high severity.
3 more findings, less severe
- Low severity, documentation,
changelog.d/calibration-request-overhead.fixed.md:1-7.fixedfragment describes a fix that has no effect with the pinned context-manager. The fragment says the strategy now compares the provider total with an estimate of the whole request, and that before this a tool-heavy agent's multiplier learned its tool schemas as a per-token rate. package.json still pins@animalabs/context-managerat^0.20.0, and the PR description says no dependency floor moves. The installed strategy ignores the second argument, so after this release the multiplier behaves exactly as it did before until a context-manager containing #324 is installed. A reader of CHANGELOG.md would think the miscalibration is fixed. The entry should say that the fix needs context-manager #324 (or the version that ships it), or the floor should move when #324 is released. - Low severity, test coverage,
test/calibration-request-overhead.test.ts:90-107. Test never checks that system-position injections reach the sample. The PR relies oncompiledRequest.systemalready holding system-position injections (assembleActivationRequestbuildssystemfrombuildSystemPrompt(systemInjections)), so the sample's overhead covers them. The test only sets a staticsystemPromptand adds no module that returns aposition: 'system'injection. A later change could build the sampledsystemfrom the agent's base prompt alone, leaving injections out of the overhead, and this test would still pass. Add a module whose injection is system-position, and assert that the sampledsystemcontains its text. - Low severity, documentation,
src/framework.ts:20434-20445. Comment and changelog say "whole request", but rows added after the compile are still left out. The new comment and the changelog say the strategy now prices the whole request, not just its compiled window. ButassembleActivationRequest(src/agent.ts:1460-1487) adds messages aftercompileWithInjectionsreturns: request-only rows (renderRequestOnlyRows), the silent-heartbeat separator turn, and thecontinueTurn()[Continue] row. These are counted in the provider total but are not in the compile's estimate, and they are not in the{ system, tools }overhead either. So the sample still carries some unexplained difference. Either say in the comment that only system and tools are passed, or include those rows.
… term in the sample; say the fix waits for context-manager anima-research#324 From our internal review of anima-research#763. - The test's request carried only the agent's configured system prompt, so a sample built from agent.systemPrompt instead of the request's system passed it, and so did a total that dropped cacheCreationTokens (both run against the test as it was). Its module now adds a system-position injection, which the request's system holds and the sample must carry, and its usage event splits the 4900 into input, cache write and cache read. Each of the two variants now fails it. - The changelog fragment says that context-manager 0.20.0 is among the versions that ignore the second argument, so the multiplier learns as before until the host runs a release with context-manager anima-research#324. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Our review of this PR: it passes the request's own system prompt and tools with the sample, and Anarchid#2, a pull request into this branch, tightens its test and the changelog fragment. Nothing is needed from you beyond reading it. Our internal review read
Written by Esther-2584. — Lottie-2661 |
Calibration test: pin the injected system prompt and the cache-write term; say the fix waits for anima-research/context-manager#324
Anarchid
left a comment
There was a problem hiding this comment.
Nightly dual review · round R1 at cfda701 · reviewers: Codex gpt-6-astra (xhigh) and Claude claude-fable-5-1, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: APPROVE (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.
Reviewed at cfda701 against main 091fa49
Verdict: approve. The change is what it says: at the one usage site that already feeds the strategy a per-call input total, it now also hands over the system and tools of the request membrane actually streamed, and the strategy duck type widens to match. Traced end to end, compiledRequest is the object startStreamWithInjections passed to membrane.streamYielding, so what the sample carries is what the framework sent, system-position injections and MCPL tools included. I ran the new test three ways: against the installed context-manager 0.20.0 (passes, strategy prices nothing, as the changelog says), with the source change reverted (fails on the overhead assertion, as claimed), and against a local build of context-manager #324 at d2bd6fb7 (passes, and the strategy's diagnostic shows the overhead being priced: est=1k incl. 1k system+tools). One minor item below; nothing blocks.
This is review round 1. No earlier review by Anarchid exists on this PR (the prior comments are the automated review of 0a0c71f and nissa-seru's note). Both nomination facts hold: the PR is the agent-framework half of context-manager#323, companion to context-manager#324 (open, unmerged at d2bd6fb7), and Anarchid#2 (the tightened test and changelog) is merged into this branch at the merge commit under review.
Blockers
None.
Major
None.
Minor
R1-1 — Provider-level overrides can make the sample describe content the provider never saw. src/framework.ts:20444 reads system and tools from the normalized request. An agent's providerParams (src/agent.ts:1519) ride along as extra and the adapters apply them after the system and tools are built: membrane src/providers/anthropic.ts:1309-1329 does Object.assign(params, rest), and the OpenAI Responses adapter does the same. A host that overrides tools (say, to []) or system through providerParams, or a membrane beforeRequest hook that rewrites the provider request, sends different bytes than the sample declares. With the companion context-manager change installed, the strategy then prices schemas that were never sent (or misses replacement text), and under its new consistent-run rule three such samples move the multiplier. Codex reproduced this in memory with the real adapter and strategy methods: one normalized tool, zero wire tools, three samples drove the multiplier from 1.0 to 0.6. This is an escape-hatch configuration, so I rate it minor rather than major: a host overriding its advertised tool list at the provider seam is already out of sync with the framework's own tool dispatch. Suggested fix, cheapest first: when compiledRequest.providerParams carries any of system, tools, or messages, pass undefined (the strategy then stays on its pre-#324 behaviour for that store) and say so in the comment; or, properly, have membrane report the effective request on its usage/receipt event and sample from that. The present test cannot see this because MockMembrane bypasses formatting and adapters.
Settling the nomination questions
Is what it passes exactly what the provider counted? It is exactly what the framework sent, which is the right contract for this seam; the provider counts a shaped version of it:
- System-position injections: yes.
assembleActivationRequestbuildssystemviabuildSystemPrompt(systemInjections)(src/agent.ts:1491,:1858-1866), and the test now pins an injected line. - MCPL tools: yes.
toolsis the advertised presentation surface (src/framework.ts:18312-18315), which includes MCPL definitions; the request getsavailableTools.length > 0 ? availableTools : undefined(src/agent.ts:1492). - Cached tool and system blocks: consistent.
realTotalsums fresh input, cache write and cache read (src/framework.ts:20390-20393), so tools and system are counted whether they hit the cache or not. Cache markers are added to the provider-side representation without touching the stringsystemfield. - Retries: each framework retry is a fresh compile and a fresh
compiledRequest(membraneRetries: false,src/framework.ts:18398-18402; retry re-entersstartAgentStream). A refusal retry replays the same request inside membrane, so the same{ system, tools }describe it. Membrane emits usage once per completed round, before tool dispatch, so the first sample after a compile is the first round, and the strategy's arm-once gate ignores the rest. - Forks and ephemerals: each is its own agent with its own strategy and its own pass through the same code; nothing special-cases them.
- What differs on the wire: the native formatter renames
inputSchema, the Anthropic adapter flattens root schema unions, XML tool protocols render tools as text into system or conversation, OpenAI-style adapters wrap tools as functions, and Anthropic's own tool-use system prompt is never visible. All proportional or fixed, and the estimate is a character heuristic anyway; the multiplier absorbs them.
What does an older context-manager do with the extra fields? Ignores them. v0.20.0 (src/strategies/autobiographical.ts:13705) and current main at 178a774c (:13935) declare reportRealInputTokens(realTotal: number) and never read a second argument. Run-verified: the test against 0.20.0 passes and the diagnostic shows est=0k real=5k with no overhead term. ^0.20.0 is unchanged in package.json, so the changelog's "0.20.0 included ... until a context-manager release with #324" is accurate.
What it doesn't claim to touch, and still leaves in the gap between estimate and provider total:
- Message-position injections (
beforeUser/afterUser): in the request's messages, in neither the window estimate nor{ system, tools }. Context-manager owns them and Anarchid/context-manager#2 (open, into the #324 branch) prices them there; nothing for this PR. - Rows the framework adds after the compile: request-only rows, the silent-heartbeat separator, the
[Continue]turn, and the tool-result guard's edits inprepareRequest. Counted by the provider, not in the estimate. Small and pre-existing. - R1-1's override path, above.
Verified fine
compiledRequestidentity:startStreamWithInjectionsreturns the very object it handed tomembrane.streamYielding(src/agent.ts:1644,:1669-1671);driveStreamreceives it as a frame-local parameter (src/framework.ts:18391-18398,:18490-18496) and never reassigns it. A superseded stream's events are dropped before the usage case (:19103-19109). No path passes an undefined or stale request.- Type compatibility: membrane's
NormalizedRequest.systemisstring | undefined(src/types/request.ts:213),toolsisToolDefinition[]; context-manager #324'sRequestOverheadimports the sameToolDefinitionfrom membrane and takesReadonlyArray, so the shapes line up with no cast games once the floor moves. - Membrane never mutates
request.systemorrequest.toolsin place (all uses on main are reads), so the sample is not polluted by cache-control blocks. - Test wiring: the strategy instance passed in
agents[].strategyis the onegetContextManager().getStrategy()returns; the spy shadows the prototype method on that instance;asMembrane()returnsthis, so the subclassstreamYieldingoverride is reached; the 4900 total is input 900 + cache write 600 + cache read 3400. tscbuild clean at head; the single test passes in 80 ms; negative run fails exactly on thedeepEqualof the overhead.- Changelog fragment follows
changelog.d/README.md(flat file,.fixed.md, bullet with two-space continuations); its version claims are correct. - Follow-up, not a finding: when the context-manager release containing #324 ships, bump the floor and replace the inline duck type with the exported
RequestOverhead.
Method: two passes merged. Codex (gpt-6-astra, reasoning effort xhigh, read-only sandbox, no network) ran 122 commands over the worktree and the local git refs of membrane and context-manager (2.39M input tokens, 2.21M cached, 15k output), including in-memory executions of the real adapter and strategy methods. Claude's pass had network: fetched the PR head, both upstream mains and context-manager #324, built the framework at head, ran the test against context-manager 0.20.0, with the fix reverted, and against a local build of #324. R1-1 was found by Codex and independently re-verified against the adapter source. The identity of compiledRequest, retry and refusal paths, type compatibility, and the 0.20.0 behaviour were confirmed by both passes. Codex could not run the test suite (its sandbox lacked dependencies); Claude ran the new test but not the full 6.6k-test suite.
…qualified channels meet the registry, tune-out and the reply anchor What main brings since e4d1e46, the sixth catch-up's base: 0.25.0 and 0.26.0, and anima-research#709, anima-research#710, anima-research#719, anima-research#722, anima-research#724, anima-research#728, anima-research#727, anima-research#743, anima-research#744, anima-research#755, anima-research#759, anima-research#761, anima-research#763, anima-research#767, anima-research#707, anima-research#740, anima-research#760, anima-research#726, anima-research#371, anima-research#452, anima-research#768, anima-research#465, anima-research#770, anima-research#771, anima-research#723, anima-research#777, anima-research#776, anima-research#773, anima-research#775, anima-research#778, anima-research#774, anima-research#779 and anima-research#781. Most of the conflicts are where anima-research#710 met the branch: in main a channel the framework acts on is a ChannelRef, { serverId, channelId }, since two MCPL servers can register one id. Conflicts in 14 files (61 hunks), resolved by keeping both sides' intent; where the two had different designs for one thing, the branch's stays, moved onto main's types: - src/framework.ts: main's conversationAgentHomes map stays gone. A fork's home is the registry record's homeChannel, which homeOf reads wherever main read the map (isForkBound, mayTakeGateRoute, canCloseChannels, holdsChannelTraffic, agentForPrimary, the drafts resend, the channel tools' guard, the registry's homeChannelResolver, stop()'s unbinding, releaseAgentRuntime's). The record still keeps a bare id, read as main reads a wake that names its channel without its server (channelNamed), or, where it names no one registered channel, as a home on no server ({ serverId: '' }), which a path reading the pair finds unregistered and one reading the name finds no channel or two for. main's dropContinuations goes into releaseAgentRuntime, so every way a live non-ephemeral agent ends drops a continuation of its turn, and beside the registry's end of an ephemeral run. Arrival carries main's channel ref and the branch's addressed; the addressed-elsewhere notice names channels with main's channelShown and keys its once-per-channel set by destinationKey. isAddressedMessage stays tag-only, with main's ambient marks. publishInputWithTurnLocus gives the locus with its server, and only to a call made inside the turn (callingTurnOf); outside it, a conversation fork's call takes its home, as its dispatched calls do, and anyone else's the registry's default. The tune_out handler keeps the branch's shape (serverId, scope, check, peek) and resolves through ChannelRegistry.resolveChannel, now resolveToolChannelEntry with the calling turn's own channels, check's peek included; its answers name the channel with nameShown. holdForProviderWait holds nothing for an agent a listener of the failure it reports ended (stopAgent), where main's report-before-hold (anima-research#774) meets the branch's stopAgent at a conversation agent's hold after its compile. handleSubconsciousTool gets main's own channels, then the branch's caller. - src/tune-out/coordinator.ts: main's shown() names beside the branch's reader helpers; deliver_summary, cancel_tuneout and speak_in_channel resolve as main's do, for the subconscious and a reader fork alike; cancel_tuneout keeps the branch's serverId and all-except fallback, and a name that picks no channel (one two servers share, or a turn's own channel ownChannelFor refuses) doesn't keep a call naming the scope from ending it; findByChannel goes, as on main. focusChannel() returns a ref. - src/mcpl/channel-registry.ts: markAccepted records no anchor (admission does), and handleIncoming's arrival count of removals, which main's guard there read, goes with it; noteDelivered takes the channel with its server and sets nothing for a channel its server no longer registers, read at admission where main's guard compared removal counts at arrival and acceptance (a message admitted after its server registers the channel again sets the anchor here). buildChannelContext is main's, per server, with the branch's sharedFallback; incomingFor reads the anchor itself, not the speech fallback. globalFallbackFor, sharedFallback, fallbackLocus and focusChannelResolver are on refs. - src/index.ts: main's DefaultErrorPolicy beside the branch's exports. - Tests: the branch's registry stubs take main's shapes; main's appendInTwoParts in batched-wake-locus's rig. Beyond the conflicts: the all-except announcement goes to the excepted channel on its server (publishForAgent refuses a shared bare id since anima-research#710), and tune_out's all-except answer names its channel with nameShown; the tune_out schema's serverId description says channelId takes serverId:channelId too; main's refusals for a tuned-out channel no name picks alone give the tune_out cancel by id and server, since this branch's tune_out takes a serverId; a stale comment naming conversationAgentHomes in channel-registry.ts names the registry; docs/dendrite.md says where a publish naming no channel goes outside the turn, and no longer lists ChannelRef as unbuilt. Test adaptations: main's tests that set conversationAgentHomes give the fork its home through the registry. main's tests written to the latest-inbound rule run under it (reply-server-routes: speech LEGACY; shared-channel-id's registry fixture: latestInboundLocus, and noteDelivered as admission records). Two reply-server-routes tests of an in-turn publish naming no channel now check the branch's rule, the turn's locus through its server. tune_out's shared-channel-id tests wait for the handler's answer, and its shared-id refusal offers serverId. all-except- unit's stub registry has nameShown; present-while-acting and deferred-window-only build routing state in main's shapes. main's marker and removed-server anchor tests record the anchor at admission (noteDelivered), where this branch records it. At this commit alone, the tests where a fork's home has to name its server fail (two servers' channels with one id, or a home the registry doesn't register at that moment); the pull request lists them. The next commit stores the home with its server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
anima-research/context-manager#323. The calibration sample hands the strategy the provider's input total for the first call after a compile. That total counts the system prompt and the tool definitions the request sent, and the strategy's estimate of its compile holds neither. A tool-heavy agent's multiplier then learns its tool schemas as a per-token rate, or sits at 1.00 rejecting every sample as out of band.
Changes
driveStream, the sample now passes{ system, tools }from the request it streamed (compiledRequest: the requeststartStreamWithInjectionshanded to membrane, whosesystemalready includes system-position injections) asreportRealInputTokens' second argument.Tests
test/calibration-request-overhead.test.ts: a turn whose request has a system prompt and a module tool. The spiedreportRealInputTokensgets4900(input + cache write + cache read) and exactly{ system, tools }of the request membrane received. Fails without the change, passes with it.npm test: 6634 pass / 0 fail / 45 skipped (6679 tests).tsc --noEmitclean.Not verified
changelog.d/calibration-request-overhead.fixed.md🤖 Generated with Claude Code