Skip to content

Preserve structured tool outputs during truncation#1500

Merged
threepointone merged 2 commits into
mainfrom
fix/structured-tool-output-truncation
May 11, 2026
Merged

Preserve structured tool outputs during truncation#1500
threepointone merged 2 commits into
mainfrom
fix/structured-tool-output-truncation

Conversation

@threepointone
Copy link
Copy Markdown
Contributor

@threepointone threepointone commented May 11, 2026

Summary

  • Preserve structured tool output shapes during read-time context truncation and row-size enforcement so custom toModelOutput handlers keep seeing compatible replay data.
  • Add bounded fallback markers for objects/arrays that are too large to preserve structurally, keeping persisted rows under the storage safety limit.
  • Harden Think's workspace read replay path so legacy raw-string outputs and unknown object shapes replay safely instead of stalling inference.

Details

This fixes the failure mode from #1498 where truncateOlderMessages() stringified older tool-read outputs, then Think's read.toModelOutput attempted object membership checks on a string during convertToModelMessages().

The new shared truncation helper keeps strings as strings, objects as objects, and arrays as arrays while recursively truncating large nested values. If an object or array remains too large because its size is spread across many keys or primitive values, it falls back to a compact truncation marker rather than returning an oversized row.

Test plan

  • npm run test:workers -w agents -- --run src/tests/experimental/memory/utils/compaction.test.ts
  • npm run test -w agents
  • npm run test:workers -w @cloudflare/think -- src/tests/think-session.test.ts
  • npm run test -w @cloudflare/think
  • npm run check

Made with Cursor


Open in Devin Review

Read-time context truncation and row-size enforcement now compact tool outputs without blindly replacing structured objects with strings. This keeps tool-specific toModelOutput handlers on their expected replay contracts, while still falling back to compact markers when a large object or array cannot be bounded structurally.

Harden Think's workspace read replay path so legacy raw-string outputs and unknown object shapes no longer crash or fall through to multimodal rehydration. Add regressions for truncated read replay, legacy raw-string replay, structured object truncation, and primitive-heavy row-size enforcement.

Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 11, 2026

🦋 Changeset detected

Latest commit: 92ca4f6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agents Patch
@cloudflare/think Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

return marker;
}

return [truncateString("", maxChars, originalLength)];
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.

🟡 compactArrayMarker fallback produces an empty string because truncateString("", ...) always returns ""

When the primary array marker string doesn't fit within maxChars, the fallback calls truncateString("", maxChars, originalLength). Because "".length (0) is always <= maxChars (for any non-negative value), truncateString returns "" immediately at tool-output-truncation.ts:151, bypassing the truncation suffix logic entirely. The result is [""] — an array containing an empty string with no truncation context.

This is reachable when an array is a nested child of a larger structure, where childMaxChars (tool-output-truncation.ts:198-203) assigns a budget of 80 chars. The primary marker string ("Array output omitted because...") JSON-serializes to ~105+ chars, exceeding 80, so the fallback triggers. Compare with compactObjectMarker (tool-output-truncation.ts:177-180) which has a proper minimal fallback with __truncated and __truncatedChars fields.

Suggested change
return [truncateString("", maxChars, originalLength)];
return [truncateString(truncatedSuffix(originalLength), Math.max(0, maxChars - 4), originalLength)];
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Use a structured marker when an array is too large to preserve within its budget, and fall back to a non-empty truncation suffix for extremely small budgets. This avoids returning arrays like [\"\"] for nested array outputs while keeping the row-size fallback bounded.

Add a regression covering nested arrays with small child budgets.

Co-authored-by: Cursor <cursoragent@cursor.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 11, 2026

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@1500

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@1500

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@1500

hono-agents

npm i https://pkg.pr.new/hono-agents@1500

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@1500

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@1500

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@1500

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@1500

commit: 92ca4f6

@threepointone threepointone merged commit 7090e9e into main May 11, 2026
4 checks passed
@threepointone threepointone deleted the fix/structured-tool-output-truncation branch May 11, 2026 17:37
@github-actions github-actions Bot mentioned this pull request May 11, 2026
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