You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
codemode/Think execute tool: the full calls[] replay log is returned to the model, bypassing transformResult #2182
where calls = execution.log — the durable replay log (ToolLogEntry[]), i.e. every connector call with its complete args and result. Only result goes through transformResult; calls is echoed raw ("so UIs can render an audit trail without a separate getExecution round trip").
@cloudflare/think's createExecuteTool (packages/think/src/tools/execute.ts) returns that object as the AI SDK tool output, so the whole replay log becomes part of the model's transcript and is re-sent on every subsequent step. The transformResult docs promise "the audit trail keeps the full value while the model sees the transformed one" — that holds for result but not for calls.
Impact (measured)
Production chat turn, gpt-5.6 via AI Gateway: one execute() whose script called a single MCP method (list_tables) returned a 342,049-char tool output — a few KB of result, the rest calls[0].result. Later steps in the same turn re-sent it; the turn reached ~140k input tokens / 680 KB per request. With transformResult: truncateResult in place (Think's default), the cap looked like it was working while calls blew past it.
What other harnesses do
opencode 2's codemode returns only the script's return value + logs (nested call log is UI metadata without results); codex's code mode truncates the script output by token policy and records nested calls for telemetry only; DeepSeek's harness states "nested Code dispatch values never enter model context". The model channel is the script result; the call log is a UI/audit channel.
Proposed fix
Think (createExecuteTool): give the returned tool a toModelOutput that projects the output without calls ({ status, executionId, result, logs }, plus pending for paused / error for error). The full output stays in the persisted UI message part (audit trail, narration, citations), and convertToModelMessages(messages, { tools }) — which Think already uses on replay — applies the projection on every later turn. Think already does exactly this for its fetch and workspace tools.
codemode: document that calls is a UI/audit payload, not model context (and/or run calls[].result through transformResult for hosts that don't have a model/UI split).
A follow-up worth considering, per the opencode/DeepSeek pattern: spill an oversized result to the workspace (state.* is already a connector) and return a preview + path instead of truncating.
Related durable-log limit that interacts with this: toStored refuses any single args/result value > 1,000,000 bytes and fails the execution, so a script cannot spill > 1 MB in one state.writeFile call — chunked appendFile is required. Worth a note in the execute tool description.
I have the Think change ready on a fork and can open a PR if you want it.
fails before the script can spill: The result of tools.spill_payload is too large to record durably (1200014 bytes > 1000000 byte limit). Write large data to a file or workspace instead…
The "write large data to a file" advice in tooLargeMessage is only actionable when the connector result is already under 1 MB — the durable log records the host result before the script sees it, so a single oversized connector response fails the run regardless of what the script would have done with it. Chunking has to happen on the connector side.
Even when the script does the right thing (spills, returns a ~120-char reference), the tool output that reaches the model is 1.2–2.4 MB because calls[] carries both the connector results and the writeFile/appendFile args (the spilled content again). The spill defeats itself unless calls is kept out of the model channel.
calls is also a lot to embed in a persisted UI message part; since runtime.getExecution(id) already holds the log, UIs that want the audit trail could fetch it by executionId instead of the tool output carrying it.
packages/think/src/tools/execute.ts: the execute tool gets a toModelOutput that returns the ProxyToolOutput without calls (status / executionId / result / logs; pending and error untouched). The AI SDK applies it in the step loop and in convertToModelMessages(messages, { tools }) on replay, so the model sees the projection on this and later turns while the raw output with the full trace stays on the persisted UI part for audit/narration/citations — the same way Think already uses toModelOutput for its fetch and workspace tools. The paused-output truncation wrapper is preserved. Includes a test (packages/think/src/tests/execute-tool.test.ts + agent fixture) and a patch changeset for @cloudflare/think.
Happy to rebase or adjust if a maintainer wants to pull it in.
Second commit on the same fork branch, for the follow-up mentioned above: Konan69@460938b
transformResult in createExecuteRuntime now spills an oversized result to the workspace state backend (/tool-output/execute-<sha256>.json, chunked under the 1 MB durable-value limit) and returns { spilled, path, bytes, preview, hint } instead of truncating; falls back to truncateResult when no workspace is configured or the write fails. The state.* connector hint names the 1 MB per-connector-result limit so scripts chunk large fetches. Test + changeset included.
Activity
Konan69 commentedon Aug 29, 2026
Measured in a real workerd test (Think 0.16.0 + codemode 0.5.1,
createExecuteToolwith astateworkspace connector), to size the two channels:resultcalls)state.writeFile→ return{ path, sliceLength, totalLength }The result of tools.spill_payload is too large to record durably (1200014 bytes > 1000000 byte limit). Write large data to a file or workspace instead…state.appendFile×4Two things this shows beyond the original report:
tooLargeMessageis only actionable when the connector result is already under 1 MB — the durable log records the host result before the script sees it, so a single oversized connector response fails the run regardless of what the script would have done with it. Chunking has to happen on the connector side.calls[]carries both the connector results and thewriteFile/appendFileargs (the spilled content again). The spill defeats itself unlesscallsis kept out of the model channel.callsis also a lot to embed in a persisted UI message part; sinceruntime.getExecution(id)already holds the log, UIs that want the audit trail could fetch it byexecutionIdinstead of the tool output carrying it.Konan69 commentedon Aug 29, 2026
PR creation is blocked for external accounts on this repo, so here is the fix as a fork commit instead (same pattern as #1363):
Branch: https://github.com/Konan69/agents/tree/think-execute-to-model-output
Commit: Konan69@9367d0f
packages/think/src/tools/execute.ts: the execute tool gets atoModelOutputthat returns theProxyToolOutputwithoutcalls(status / executionId / result / logs;pendinganderroruntouched). The AI SDK applies it in the step loop and inconvertToModelMessages(messages, { tools })on replay, so the model sees the projection on this and later turns while the raw output with the full trace stays on the persisted UI part for audit/narration/citations — the same way Think already usestoModelOutputfor its fetch and workspace tools. The paused-output truncation wrapper is preserved. Includes a test (packages/think/src/tests/execute-tool.test.ts+ agent fixture) and a patch changeset for@cloudflare/think.Happy to rebase or adjust if a maintainer wants to pull it in.
Konan69 commentedon Aug 29, 2026
Second commit on the same fork branch, for the follow-up mentioned above: Konan69@460938b
transformResultincreateExecuteRuntimenow spills an oversized result to the workspace state backend (/tool-output/execute-<sha256>.json, chunked under the 1 MB durable-value limit) and returns{ spilled, path, bytes, preview, hint }instead of truncating; falls back totruncateResultwhen no workspace is configured or the write fails. Thestate.*connector hint names the 1 MB per-connector-result limit so scripts chunk large fetches. Test + changeset included.fix(codemode): truncate structured results structurally and keep the …