Skip to content

fix(tangle): truncate oversized tool output instead of discarding a paid turn - #312

Merged
drewstone merged 1 commit into
mainfrom
fix/tool-output-truncate-not-throw
Sep 11, 2026
Merged

fix(tangle): truncate oversized tool output instead of discarding a paid turn#312
drewstone merged 1 commit into
mainfrom
fix/tool-output-truncate-not-throw

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

Follow-up to #311. That PR removed the instance; this one removes the class.

#311 left a 4× mismatch

It widened tool output from the 16 KiB metadata bound to the 1 MiB content bound. But the producer still serializes each tool value up to 4 MiB, and the consumer bound is 1 MiB across the whole record, not per value. Measured against the shipped 1.1.9:

0.9 MiB x1                    ACCEPTED
2 MiB x1 (producer allows 4)  REJECTED — Tangle prompt result exceeded its JSON bound
0.6 MiB x2 = 1.2 MiB total    REJECTED — Tangle prompt result exceeded its JSON bound

256× became 4×, and the multi-value case is worse than the single-value case.

The mismatch was never the real defect

This validator runs inside the terminal result read — after the live stream has drained and after the usage receipt has been credited. A throw here cannot prevent the work or the charge. It can only destroy a finished, fully paid turn, which a supervisor then reports as a child that did nothing at all.

That failure mode cost one Discovery Lab 143 of 199 children across 16 pursuits, and its six-stage sourcing graph blocked in 24 of 24 invocations.

Any bound that throws on this path is one large page away from doing it again. Widening it is not a fix; it is a bigger margin before the same outcome.

Change

Oversized tool output is replaced, not refused. Each pass cuts the widest toolInvocations[].result by the record's measured overflow and leaves a marker naming both byte counts:

[truncated by the Tangle provider: kept 1046528 of 4194304 bytes to stay inside the 1048576-byte record content bound]

The caller still receives the turn, its response text, its usage, and every tool call it made. What it loses is the tail of an oversized result, marked where it was cut, so a reader can tell truncation from a tool that genuinely returned little and can choose to re-fetch.

Only tool output is truncatable. Every other field is identity, accounting, or control material where a silently shortened value would be worse than a refusal — so a record whose overflow is elsewhere still refuses, as does one breaking the node, depth, or array limits, which trimming a string cannot satisfy.

Tests

One added, fails on 1.1.9. It pins: a 2 MiB single result survives; the producer's own 4 MiB per-value maximum survives; several results that each fit but together do not converge rather than the first being destroyed; the marker carries both byte counts; every tool call is still present; and an oversized response still throws.

Provider suite 264 pass, typecheck clean. 1.1.9 → 1.1.10.

🤖 Generated with Claude Code

…aid turn

1.1.9 widened the bound on tool output from the 16 KiB metadata limit to the
1 MiB content limit. That removed the instance and left the class: the producer
still serializes each tool value up to 4 MiB, and the consumer bound is 1 MiB
across the WHOLE record, so a single 2 MiB fetch still died, and so did two
0.6 MiB fetches together. A 256x mismatch became a 4x one.

The mismatch is not the real defect. This validator runs inside the terminal
result read, after the live stream has drained and after the usage receipt has
been credited, so a throw here cannot prevent the work or the charge. It can
only destroy a finished, fully paid turn, which a supervisor then reports as a
child that did nothing at all. Any bound that throws on this path is one large
page away from doing that again.

So oversized tool output is now replaced rather than refused. Each pass cuts the
widest `toolInvocations[].result` by the record's measured overflow and leaves a
marker naming both byte counts, so a reader can tell a truncated result from a
tool that genuinely returned little. The caller still receives the turn, its
response text, its usage, and every tool call it made.

Only tool output is truncatable. Every other field is identity, accounting, or
control material where a silently shortened value would be worse than a refusal,
so a record whose overflow is elsewhere still refuses — as does one that breaks
the node, depth, or array limits, which trimming a string cannot satisfy.

Tests: one added, failing on 1.1.9. It pins a 2 MiB single result, the
producer's own 4 MiB per-value maximum, several results that each fit but
together do not, the marker's byte counts, that every tool call survives, and
that an oversized `response` still refuses.

Provider suite 264 pass, typecheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@drewstone
drewstone merged commit c817a57 into main Sep 11, 2026
3 of 4 checks passed
@drewstone
drewstone deleted the fix/tool-output-truncate-not-throw branch September 11, 2026 16:08
drewstone added a commit that referenced this pull request Sep 11, 2026
`prompt` was `boundedStringSchema` — CONTRACT_MAX_STRING_LENGTH, 16,384
characters. That is the bound for identifiers and accounting fields. A prompt
is what an agent is asked to do: its size is set by the work, not by the
protocol.

Held to the metadata bound it silently capped what a manager could delegate.
Measured 2026-09-11 in a live run already carrying both prior bound fixes
(agent-provider-tangle 1.1.10, agent-runtime 0.214.0): two of ten child deaths
were `Too big: expected string to have <=16384 characters` at path ["prompt"],
refusing a director that was handing an independent-checker child a 27 KB patch
so it could rebuild and re-verify a measured result.

This is the third time agent-authored content has been held to the metadata
bound — tool output twice (#311, #312), now the prompt. The distinguishing test
is worth stating once: if an agent or a tool produced it, it is content; if it
names, routes, or accounts for the turn, it is metadata. `context` keeps the
metadata bound on exactly that reading.

Moved at all four sites: AgentTurnInput, the runtime-control prompt command,
portable context continuation, and interactive session control. The interactive
site kept its non-empty requirement as a refinement, because the content schema
is a custom schema and carries no `.min`.

Truncation is deliberately NOT used here, unlike tool output: a silently
shortened instruction is worse than a refused one. Only the ceiling moves, to
where the producer's own limits already sit.

Tests: three added to the turn-input bounds case, failing on the previous code.

agent-interface suite 546 pass. `src/certified-context.test.ts` has one
pre-existing failure that reproduces identically on unmodified main.

Closes #313.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
drewstone added a commit that referenced this pull request Sep 11, 2026
…eeds (#315)

Every bound defect this package shipped this week lived at the seam between
what the Sandbox SDK writes and what this provider accepts, and none was
caught by a unit test, because each side is individually correct. #311: tool
output written at 4 MiB, validated at 16 KiB. #312: widened to 1 MiB, still
4x and the same failure mode. #314: a turn prompt held to the metadata bound.
143 of 199 children in one Lab archive.

This test asserts the pair, not either side. It reads each producer limit out
of the SHIPPED sandbox dist — a renamed constant fails loudly rather than
silently dropping the check — and asserts it fits the contract bound it feeds,
unless the boundary is recorded as truncating instead of throwing, with the
reason. A second case enumerates every MAX_ limit the SDK ships and refuses an
unpaired one, so a new producer bound cannot arrive undecided.

On its first run it found three limits nobody had paired, and it could not read
four contract constants at all: agent-interface exported only
CONTRACT_MAX_JSON_BYTES. The full set is public now, which is what lets a
package that writes values assert its own limits fit.

agent-provider-tangle suite 271 pass. agent-interface 547 of 548; the one
failure, src/certified-context.test.ts, reproduces identically on unmodified
main.

Co-authored-by: Claude Fable 5.1 <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

Development

Successfully merging this pull request may close these issues.

1 participant