Skip to content

Cap the agent's tool results and page its file reads, so one large read no longer ends the session - #3117

Merged
kriszyp merged 16 commits into
mainfrom
david/agent-tool-result-cap
Oct 18, 2026
Merged

kriszyp merged 16 commits into
mainfrom
david/agent-tool-result-cap

Conversation

@DavidCockerill

@DavidCockerill DavidCockerill commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

⊙ Problem

The built-in agent added every tool result to its transcript in full, and every model request replays the transcript. One large read_file of a log, the file an investigating agent most needs, pushed the next request past the model's context window. The provider rejected it, the run ended error, and every later prompt replayed the same oversized result, so the session could not be used again (agent: no cap on tool-result size — one large read kills the session (#3068)). Reproduced on 5230512f9: a 1 MB log read was stored as a 1,057,864-byte tool message, the next request was rejected, and a follow-up prompt on the same session was rejected the same way.

❓ Your call: the requirement, as harper#3068 specifies it, with two departures. The note after a cut result lives in the tool message itself, not a system message, because the Anthropic and Bedrock backends hoist inline system messages into the top-level system prompt, away from the result. The terminal reason is a distinct lastError, not a new structured field, matching how harper#3064 ends its runs. Milestoned v5.4 (the issue said v5.3), like its output-side twin harper#3064.

Closes #3068

Docs: Document agent.maxToolResultBytes and how the agent recovers when a conversation outgrows the model's context window (documentation#720)

💡 Solution

⚖️ Alternatives

  • Cap only, nothing else. Rejected: a cut log read gives the model no way to reach the rest of the file, and it cannot rescue a session that already holds an oversized result.
  • Cap when building the request, keep full results stored. Rejected: the session row is rewritten whole on every append, so stored multi-MB results multiply write cost, and the operator would see a transcript the model never saw.
  • Switch the agent to generate({ toolMode: 'auto' }). Rejected for now: the orchestrator has no destructive-call approval pause or per-turn persistence.
  • Line-only paging. Adopted after review: a byte cursor beside the line cursor. Without it every page rescanned the file, and a line longer than a page could not be read past its head, so a later write_file would drop its tail.

❓ Your call: the shrink rewrites the stored transcript. The cut results stay cut even if the retry still fails, and the originals survive only in the table's audit log. The alternative is to cut only the request, which then fails again on every later turn. Easy to reverse.

❓ Your call: the shrink takes the newest group of oversized results. That undoes the turn that crossed the limit and reaches a result stored before a later prompt. The cost: a session sitting at the limit loses each fresh result while older ones survive. Taking the oldest is a one-line change.

❓ Your call: the 65,536-byte default is lower than the issue's "a few hundred KB". It matches the orchestrator's default and keeps one result at about 16–20k tokens. The 1024 floor and 1 MiB ceiling stop a typo from switching the cap off.

❓ Your call: context-window detection reads provider wording (HTTP 400/413 plus a pattern, or OpenAI's context_length_exceeded code). If a provider changes its wording, recovery stops and the run ends error as before. A false positive costs one retry with the newest results cut.

❓ Your call: the system prompt now goes only in input.system. All four built-in backends honour it; a module backend that reads only messages would now lose it rather than receive it twice.

🔧 Changes

Product and architecture tour

What reaches the transcript

How big can one tool result get, and what does the model see when it is bigger?

Before After
A tool result was stored whole: a 1 MB log read became a 1,057,864-byte tool message, replayed on every later request. A result is cut to agent.maxToolResultBytes (65,536 by default) before it is stored, ending with a note naming its size and how to ask for less.

No observation over agent.maxToolResultBytes is appended to the transcript: tool results, tool errors and unknown-tool replies alike.
The cap applies when a result is stored, not when a request is built, because the session row is rewritten whole on every append.

Reading a file a page at a time

How does the agent read a log far larger than its cap?

Example: paging a 1 MB log

The model calls read_file with only a path and gets the first 286 lines, about 32 KB, with nextLine: 287 and nextOffset: 32384. It passes those back as startLine and offset for the next page, so the file is not rescanned. A minified bundle's single long line comes back in parts, continued the same way.

Outcome: the whole file is reachable, every page fits the cap, and nothing is skipped.

A page's JSON-escaped text never exceeds half the cap, less the room its path and fields need, so the loop never cuts a page and its cursor survives.
Paging keeps the fs tools' key-material refusal from #3098, including for a page that falls inside a key.

When the provider says the request is too long

What happens when a conversation outgrows the model's context window anyway?

One retry after a context-window rejection

The backend flags the error, so the loop never parses provider wording; it cuts the newest large results in the stored transcript and asks once more.

sequenceDiagram
    participant Loop
    participant Backend
    participant Session
    Loop->>Backend: generate with the transcript
    Backend-->>Loop: rejected, contextWindowExceeded
    Loop->>Session: cut newest results over 2 KiB
    Loop->>Backend: generate again
    Backend-->>Loop: answer, or a second rejection
    Note over Loop: second rejection ends the run error
Loading

A context-window rejection never ends a run before the newest oversized results are cut and the request retried once.
The retry is best effort: a long prompt or history still ends the run error, with a lastError that says to shorten the prompt or start a new session.

Every changed file:

✅ Verification

End-to-end route: live smoke with recorded evidence. Harper booted from dist/ with a fake OpenAI-compatible backend that returns OpenAI's real 400 context_length_exceeded envelope for any request over a byte limit, and a 1 MB logs/big.log the model reads with read_file.

Run Limit Result
base 5230512f9 300 KB tool message stored at 1,057,864 bytes; request 2 rejected; session error; a second prompt rejected the same way
this branch 300 KB read_file returns a 33 KB page with nextLine/nextOffset; both prompts completed; request 1 has one system message (24.3 KB, was 30.0 KB)
this branch 45 KB request 2 (58,024 bytes) rejected through the real OpenAIBackend; the page shrunk to 2,046 bytes; the retry (26,860 bytes) completed; the same on the second prompt
this branch, on the base run's dead session 300 KB the stored 1,057,864-byte result shrunk to 2,046 bytes on the first rejection; the session completed and paged the log

Unit tests (all pass on the final head):

Fails on base: the new tests, applied to 5230512f9, fail at their own assertions there. The loop tests die with the unrecovered prompt is too long, the system-prompt test sees ['system', 'user'], and the paging tests hit the old 5 MiB refusal.

After Kris's review (5d4578300, ae4dca04f, 105151e0f): new tests fail closed on a key block longer than the look-back at both read_file and tail_file (plain and indented), page a 120-entry directory at the 1,024-byte cap, and refuse a path too long for a page in all four read tools; the fail-closed test fails without its fix. All 521 agent, backend, agentLoop and backendHelpers tests pass.

After the second merge of main (72594d687, which brought harper#3064 into the same loop and config lines): the agent, backend, agentLoop and backendHelpers suites pass (515), including #3064's own tests, and the 45 KB live smoke repeats its result. Its --final review raised the key look-back to 128 KiB, since a FrodoKEM-1344 private key is about 57 KB of PEM, and added a test that cancels while the answer is being recorded (519b80131; both tests fail without their fix).

After the first merge of main (4e2148d0e, which brought #3098 into the same fs functions): unitTests/agent/*.test.js plus the backend, agentLoop and backendHelpers suites pass (500), and the 45 KB live smoke above repeats its result. A --final review of the merged state found that the look-back counted an END line just past the page start, so a page starting on a key's last body line was served; 1c3a31f33 counts only armor lines that begin before the start, with a test that fails without it. The full gates below ran before the merge; CI runs them on the merged head.

Gates before the merge (this Mac; every failure also fails on 5230512f9 in the same checkout, compared file by file):

  • test:unit:main, run on 5b71d2642 (the final head differs only in a test comment), with applicationSpawn.test.js excluded because it hangs (#2538): 6,703 passing, 32 failing. The failures are in globalIsolation, resolvePreload, RuntimeModuleTracker, cliOperations, processGroupReclaim, tokenAuthentication, jsLoader, OptionsWatcher, deployStagingRetention and deployActivation; base fails the same files (31 and 52 in two runs).
  • test:unit:resources, run on 474871b86 (later commits touch only agent/): 4,015 passing, 3 failing, all in sourceApplyConflictRetry, the same 3 on base.
  • test:integration:all, run on 474871b86: 2,270 passing, 21 failing, in isolated-application, shutdown-drain-e2e, log-rotation-write-path, rolling-restart, cert-key-reload and record-lock-concurrency. Base fails the identical 21 tests in those six files.
  • oxlint --deny-warnings, prettier --check and check:design-docs pass on the changed files.

❓ Your call: post-review deltas, not re-reviewed: grep_files's skippedFiles (about ten lines, for a finding from the pre-merge final round), and the armor look-back fix in 1c3a31f33 (five lines, the post-merge reviewer's own proposed fix), plus stubbing every Models sink in one test.
🤖 Generated by Claude (Anthropic); posted via @DavidCockerill.

Related PRs: #562 independent, #1770 overlaps (same backend throw sites; its retry wrapper must keep contextWindowExceeded and not retry a flagged 400), #1835 independent, #2155 independent, #2170 independent, #2436 independent, #2446 independent, #2532 overlaps (set_agent_config input schema would need maxToolResultBytes), #2649 independent, #2752 independent, #2763 independent, #2775 independent, #2901 independent, #2906 independent, #2930 independent, #2939 independent, #2986 independent, #2990 independent, #2994 independent, #3021 independent, #3030 independent, #3035 independent, #3057 independent, #3064 overlaps (merged; same doRun, config and backend lines, resolved in this branch), #3069 independent, #3065 independent, #3036 independent, #2981 independent, #3070 independent, #3072 independent, #3078 independent, #3079 independent, #3082 independent, #3083 independent, #3084 independent, #3086 independent, #3093 independent, #3107 independent, #3110 independent, #3113 overlaps (adjacent CONFIG_PARAMS lines in utility/hdbTerms.ts), #3098 overlaps (merged; same fs functions, conflict resolved in this branch), #3100 independent, #3103 independent, #3085 independent, #3121 independent, #3126 independent, #3137 independent, #3174 independent, #3177 overlaps (same loop, session, serializer and backend helpers; whichever lands second merges), #3178 independent, #3185 overlaps (agent/agent.ts and agent/DESIGN.md), #3187 independent, #3199 independent, #3200 independent, #3138 independent, #3140 independent, #3139 independent, #3148 independent, #3141 independent, #3128 independent, #3119 independent, #3124 independent, #3123 independent, #3116 independent, #3118 independent, #2825 independent
Complexity: medium

Review-Coverage: authored=claude; ran=gemini,codex; adjudicated=domain; blocked=cursor-grok(not-installed); declined=cursor-composer,cursor-kimi,cursor-muse; rounds=6; full=4 @ 105151e

Review-Attention: read ~8m (decisions: do-less-alternative, persistent-shrink, key-refusal-granularity, body-only-fail-closed, wording-classifier, list-dir-offset-cursor) @ 105151e

DavidCockerill and others added 9 commits October 6, 2026 16:37
…rom a context-window rejection

One large read_file/tail_file/http_fetch result was appended to the agent's
transcript verbatim, so the next model request overflowed the context window
and every later prompt replayed it: the session was dead.

- agent/loop.ts caps every observation at agent.maxToolResultBytes (default
  65536, 1024..1048576) with a marker saying how to ask for less, reusing the
  toolMode 'auto' serializer, now exact to the byte on UTF-8 boundaries.
- read_file pages by line (startLine/lineCount, nextLine/totalLines) and
  streams, so files over 5 MiB are readable; tail_file and grep_files return at
  most one page (half the cap) and say when they stopped early.
- The openai, anthropic and bedrock backends flag a context-window rejection on
  their error; the loop then shrinks the newest oversized tool results to 2 KiB
  and retries once, else ends the run with a lastError that says what to do.
- The system prompt is sent once (as `system`), not also as a message.
- A plain-object throw (Harper's BigInt toJSON) reaches the model as its message.

Refs #3068

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…indow rejection is recovered

Refs #3068

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ges by their JSON-escaped length

Review fixes on the tool-result cap:

- read_file returns a byte cursor (offset/nextOffset) beside its line
  cursor. Passing both back resumes without rescanning the file, and a
  line longer than a page now comes back in parts instead of being
  skipped, so read-then-write_file no longer loses its tail.
- read_file, tail_file and grep_files measure a page JSON-escaped, so
  text dense in control characters or quotes no longer overflows the
  loop's cap and loses its cursor fields.
- tail_file uses bytesRead, so a file truncated between stat and read
  is not decoded from a zero-filled buffer.
- grep_files reports truncated when maxResults leaves matches unreturned,
  and does not split a surrogate pair when it cuts a line.
- An unknown-tool observation is capped like every other.
- A cancel that lands while the model is answering keeps the run aborted.
- A test drives the real OpenAI backend through Models into the loop.

Refs #3068

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o a long path cannot cut its cursor

Refs #3068

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a cancelled agent run aborted

- grep_files at a small cap returned no result when its first match alone
  exceeded the page; it now returns that match cut to fit, with truncated.
- The loop rechecks the abort signal before recording completed and before
  each model request, so a cancel that lands during a session write keeps
  the run aborted.

Refs #3068

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t read as "nothing found"

grep_files never searches a file over 5 MiB, and a busy hdb.log rotates at
64 MB, so a grep of the logs could report no matches without having looked.
It now returns skippedFiles, and the description points at read_file, which
pages through any size.

Refs #3068

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
new Models(writer) replaced only the analytics writer; the metric emitter and
decision store still defaulted to Harper's real ones, so the test wrote into
the real analytics tables of the unit-test process.

Refs #3068

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the agent filesystem tools against agent.configScope (#3098): paged
read_file and tail_file keep its scope rules and key-material refusal, and
both look back 32 KiB before their window for a BEGIN armor line, so a page
of key body alone, which carries no armor line, is refused too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
startsInsidePrivateKey reads 64 bytes past the page start so an armor line
cut there still matches, but it also counted an END line found in those 64
bytes, so a page starting within 39 bytes of a key's END line was returned:
up to 38 base64 characters of the key. Only armor lines that begin before the
page start now count.

Refs #3068

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust mechanism to prevent agent sessions from failing due to oversized tool results or context-window overflows. It adds a configurable maxToolResultBytes parameter (defaulting to 64 KiB) to cap the size of tool results appended to the transcript. The filesystem tools (read_file, grep_files, and tail_file) have been updated to dynamically page and size their outputs to fit within this budget. Additionally, the agent loop now detects context-window rejections from LLM providers (OpenAI, Anthropic, Bedrock), automatically shrinks the newest oversized tool results in the transcript, and retries the request once. Comprehensive unit tests have been added to validate these paging, capping, and recovery behaviors. No review comments were provided, so there is no feedback to address.

…ending on the tmpdir length

The long-path test assumed macOS's long tmpdir and the grep page test left
no room for the separators between matches; both failed on Linux CI. The
path is now sized to about 430 characters on every platform, and the grep
test asserts what matters: the whole tool result fits the cap.

Refs #3068

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DavidCockerill
DavidCockerill marked this pull request as ready for review October 8, 2026 19:54
@github-actions
github-actions Bot requested a review from kriszyp October 8, 2026 19:54

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Reviewed with Codex

Comment thread agent/tools/fsTools.ts
/** Whether byte `start` lies inside a PEM private-key block, judged by the armor lines that begin before it. */
async function startsInsidePrivateKey(fh: FileHandle, start: number): Promise<boolean> {
if (start === 0) return false;
const from = Math.max(0, start - KEY_LOOKBACK_BYTES);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this check establish the PEM state beyond a fixed 32 KiB lookback, or fail closed when it cannot? With a 40 KiB PEM block, read_file at offset 35,000 scans back only to 2,232, sees no BEGIN line, and returns body text. At maxToolResultBytes: 1024, tail_file likewise misses the BEGIN line in a log ending inside that block and returns its last body lines. That text enters the persisted transcript despite the key-material guard. Please cover both boundaries with a block longer than the lookback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thank you. A fixed look-back can't settle it on its own, so this now fails closed. When the 128 KiB before a read_file page (or a tail_file window) holds no armor line, doesn't reach the start of the file, and is nothing but key-body lines (base64, indented or not, as in a key embedded in YAML), the tool refuses with a "may be inside a PEM private key" error instead of serving the text. Only a block whose BEGIN lies further back looks like that; any ordinary text in the span still clears it. The look-back itself went from 32 to 128 KiB in the meantime, since a FrodoKEM-1344 key is about 57 KB of PEM. Tests cover both boundaries with a ~187 KiB block, an indented block, and a deep page of an ordinary log that must still be served. Fixed in 5d45783 and ae4dca0.

— DAIvid (Claude Opus 5.5)

🤖 Generated by Claude (Anthropic); posted via @DavidCockerill.

Comment thread agent/loop.ts
try {
const result = await tool.handler(call.arguments ?? {}, ctx);
return JSON.stringify({ ok: true, result });
return capObservation({ ok: true, result }, ctx);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could list_dir return a resumable page before this cap is applied? For a flat directory whose entries exceed maxToolResultBytes (readily possible at the 1,024-byte setting), the serialized result is cut mid-list. list_dir accepts only a directory path, so the agent cannot request the omitted entries or know which names to search for. A cursor or offset would let it complete the listing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the cap made this reachable. list_dir now sorts its entries by name and returns them a page at a time within the same budget as the read tools, with total and a nextOffset the agent passes back as offset. A name too long for a page on its own (only escaping can do that) comes back cut and flagged nameTruncated, and the cursor still moves past it. Tests page 120 entries at the 1,024-byte cap, check every page fits uncut, and check the pages reassemble the full listing. Fixed in 5d45783 and ae4dca0.

— DAIvid (Claude Opus 5.5)

🤖 Generated by Claude (Anthropic); posted via @DavidCockerill.

Comment thread agent/tools/fsTools.ts Outdated
function pageBytes(ctx: AgentToolContext, path: string): number {
const cap = ctx.maxResultBytes ?? 2 * DEFAULT_PAGE_BYTES;
const besidePage = cap - escapedBytes(path) - RESULT_FIELD_BYTES;
return Math.max(MIN_PAGE_BYTES, Math.min(Math.floor(cap / 2), besidePage));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 64-byte floor can exceed the space left after path and the result fields. At the valid 1,024-byte cap, a roughly 1,007-byte absolute path made from several legal directory components yields a 64-byte page budget, while even a six-byte read_file result serializes to about 1,121 bytes. The loop then cuts the result before nextOffset and nextLine, so the advertised pagination cannot continue. Please account for the complete envelope or return an explicit error when its metadata alone cannot fit; a nested-path test would catch this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the floor could push the result past the cap. It's gone: when the path and the result's fields leave less than a page, the tool now returns an explicit error naming agent.maxToolResultBytes and saying to raise it, rather than a result the loop would cut before its cursor. The minimum page is 320 bytes, enough for one list_dir entry with a 255-byte name, and grep_files no longer returns a first match that can't fit. A nested ~800-byte path at the 1,024-byte cap now gets that error from read_file, tail_file, list_dir and grep_files. Fixed in 5d45783.

— DAIvid (Claude Opus 5.5)

🤖 Generated by Claude (Anthropic); posted via @DavidCockerill.

DavidCockerill and others added 6 commits October 13, 2026 16:02
Keeps agent.maxTokens (#3064) beside agent.maxToolResultBytes in the agent
config, schema, set_agent_config and CONFIG_PARAMS. The loop runs #3064's
rejectTurn on whatever generateTurn returns, so a reply cut off at the output
limit still ends the run error, and a rejected request is still shrunk and
retried once first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s while the answer is recorded

32 KiB covered every classic PEM private key but not a post-quantum one: a
FrodoKEM-1344 PKCS#8 key is about 57 KB of PEM, so a page deep in its body
would have been served. The new loop test cancels during the assistant
append, the window the guard before `completed` closes; it fails without it.

Refs #3068

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…too long for a page

Kris's review on #3117:

- A window with no armor line in the 128 KiB before it, where that span is
  all key-body characters and does not reach the start of the file, may be
  deep inside a long key block; read_file and tail_file now refuse it rather
  than serve key body. Ordinary text in the span still clears it.
- list_dir sorts its entries and returns them within the page budget, with
  nextOffset to continue, so a large directory is no longer cut mid-list by
  the result cap.
- A path that leaves under 320 bytes for a page beside the result's fields
  is an explicit error, not a result the loop cuts before its cursor;
  grep_files no longer returns a first match that cannot fit.

Refs #3068

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… long for a page

- The fail-closed check now accepts indented base64 lines, as in a key
  embedded in YAML, while a space inside a line still marks ordinary text.
- A list_dir entry whose escaped name alone exceeds the page is returned
  with its name cut and nameTruncated set, so the page stays under the cap
  and nextOffset still moves past it.

Refs #3068

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…en a grep match cannot fit

From the review of the previous round:

- The look-back's first line is cut by its start, so it may be the tail of
  a BEGIN line; it no longer counts against the key-body check.
- A test pins the 64-byte read past a page start that catches an armor line
  the page start cuts through.
- A grep match whose path alone leaves no room under the cap is an explicit
  error, not an empty result marked truncated.

Refs #3068

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows refuses control characters in file names, so the test now uses a
multi-byte name there; NTFS counts UTF-16 units, so 200 of them still
outgrow a page. The test no longer depends on sort order. Two comments
claimed only escaping can make a name outgrow a page; APFS and NTFS
accept longer multi-byte names, so they now say less.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Reviewed with Codex

Comment thread agent/tools/fsTools.ts
*/
function pageBytes(ctx: AgentToolContext, path: string): number {
const cap = ctx.maxResultBytes ?? 2 * DEFAULT_PAGE_BYTES;
const besidePage = cap - escapedBytes(path) - RESULT_FIELD_BYTES;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 64-byte floor can exceed the space left after path and the result fields. At the valid 1,024-byte cap, a roughly 1,007-byte absolute path made from several legal directory components yields a 64-byte page budget, while even a six-byte read_file result serializes to about 1,121 bytes. The loop then cuts the result before nextOffset and nextLine, so the advertised pagination cannot continue. Please account for the complete envelope or return an explicit error when its metadata alone cannot fit; a nested-path test would catch this.

@kriszyp
kriszyp merged commit 957cb83 into main Oct 18, 2026
52 of 53 checks passed
@kriszyp
kriszyp deleted the david/agent-tool-result-cap branch October 18, 2026 02:22
DavidCockerill added a commit that referenced this pull request Oct 19, 2026
…the tool-result cap (#3117)

Rejected replies stay out of the transcript, as #3064 requires, but their billed usage is recorded on
the session's rejectedCalls and folded into its usage, so a cap counts them. The rejection and
context-window failure paths end the run through endRun, which keeps a cancel's aborted status.

Refs #3040

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants