fix(llm): preserve structured tool results as text across providers#97183
fix(llm): preserve structured tool results as text across providers#97183snowzlmbot wants to merge 5 commits into
Conversation
Prevent structured tool result blocks (json, resource, etc.) from being silently dropped and replaced with '(see attached image)' placeholders. - Add extractToolResultText() to serialize structured non-image blocks - Apply across OpenAI Responses, OpenAI-compatible chat completions, and Google Gemini - Add regression tests for session_status-style structured results Closes openclaw#96857
|
Codex review: needs real behavior proof before merge. Reviewed June 27, 2026, 8:55 AM ET / 12:55 UTC. Summary PR surface: Source +67, Tests +398. Total +465 across 15 files. Reproducibility: yes. for the narrower conversion bug by source inspection: current main filters several provider and transport tool-result paths down to type text, so structured-only blocks degrade to placeholders or empty output. The broader provider-failover/session-corruption path is not fully reproduced by this PR. Review metrics: 1 noteworthy metric.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Keep the shared cross-provider helper direction, tighten or remove the data-URI redaction with focused coverage, require inspectable redacted after-fix proof, and leave the broader canonical issue open for failover/session-corruption follow-up. Do we have a high-confidence way to reproduce the issue? Yes for the narrower conversion bug by source inspection: current main filters several provider and transport tool-result paths down to type text, so structured-only blocks degrade to placeholders or empty output. The broader provider-failover/session-corruption path is not fully reproduced by this PR. Is this the best way to solve the issue? No, not yet. The shared helper across provider and active transport conversion paths is the right layer, but this head still needs safer redaction and inspectable real behavior proof before it is the best fix. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against c5d34c8376f8. Label changesLabel justifications:
Evidence reviewedPR surface: Source +67, Tests +398. Total +465 across 15 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
| (record) => record.type === "tool_result" && record.tool_use_id === "tool_1", | ||
| ); | ||
| // Structured non-image block should be serialized, not dropped | ||
| expect(toolResult.content).toBe( |
There was a problem hiding this comment.
ClawSweeper is right to call this out: toBe(expect.stringContaining(...)) compares the received value to the matcher object by identity, so this added regression test fails instead of proving structured tool-result text reaches Anthropic. Please switch this to a value matcher such as toEqual(expect.stringContaining(...)) or toContain(...) before using the test as merge evidence.
| if (!serialized || serialized === "{}") { | ||
| return undefined; | ||
| } | ||
| return serialized.length > 1_000 |
There was a problem hiding this comment.
This 1,000-character cap runs inside the provider conversion helper, before the existing prompt/tool-result truncation policy has a chance to budget the payload. That means a large JSON/resource result can still be silently delivered as partial text even when the target provider context could carry more. Please remove the helper-local cap, or make it an explicit product limit with focused tests that prove this is the intended truncation boundary.
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
1 similar comment
|
@clawsweeper re-review |
|
@clawsweeper re-review |
|
@clawsweeper re-review |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Superseded by clean replacement PR: #97268 Closing this noisy branch because the local repair loop and stale head divergence made the old PR untrustworthy. The replacement PR contains the focused visible tool-result display fix on a clean branch. |
|
Superseded by clean replacement PR: #97268 |
Summary
(see attached image).extractToolResultText()across OpenAI Completions, OpenAI Responses, Google/Gemini, Anthropic, and Mistral conversion paths.What Problem This Solves
Tool results can contain structured non-image blocks such as JSON, resource, or resource-link payloads. Several provider and transport converters previously extracted only
type: "text"blocks before building provider payloads. When a tool result contained structured data but no text block, those converters treated the result like an image-only payload and emitted(see attached image)or an empty fallback.That loses actionable tool output in the next model turn. The failure is especially visible in agent sessions where tool output is stored as structured blocks and then replayed through OpenAI Responses, Anthropic, Google/Gemini, or Mistral conversion paths.
Why This Change Was Made
The model-visible provider/transport conversion layer should not discard structured non-image tool output. The right boundary is to serialize those blocks into text before provider payload creation, while leaving truncation and payload budgeting to the established context/tool-result guards instead of introducing a provider-local cap.
User Impact
(see attached image)fallback when the target path cannot carry image input.Review Follow-Up
extractToolResultText()to acceptreadonly unknown[]and narrowing internally.Record<string, unknown>[]casts from provider call sites.convertResponsesMessages, usingtestAllowedToolCallProviders, and casting the intentional structured-content fixture at the boundary.no-base-to-stringguards viatypeofchecks, shadowedcontextvariable warnings, and type-cast boundaries in tests.src/llm/providers/tool-result-text.ts.src/llm/providers/tool-result-text.test.tsto prove the provider helper does not truncate structured blocks before the established payload-budgeting boundary.Evidence
git diff --checkpasses clean for this head.node scripts/run-vitest.mjs src/llm/providers/tool-result-text.test.tsnode scripts/run-vitest.mjs src/llm/providers/openai-responses-shared.test.ts src/agents/openai-transport-stream.test.ts src/agents/anthropic-transport-stream.test.ts src/llm/providers/mistral.test.ts src/llm/providers/google-shared.convert.test.ts src/llm/providers/openai-completions.test.tstail-marker, verifies the text length stays above 1,200 characters, and verifies the tail survives without the previous... (N chars)truncation marker.projects/openclaw-pr-maintenance/reports/pr-97183/local-machine-fix/; private host details are intentionally not published.Scope And Limitations
This PR fixes the structured non-image tool-result conversion failure: blocks such as JSON/resource payloads are serialized as text before provider/transport payload creation instead of being treated like image-only tool results.
This PR does not claim to fully resolve every failure mode described in #96857. The prompt-history truncation plus provider-failover/session-corruption path described in later issue evidence is broader than this conversion fix and should remain open for follow-up investigation if it reproduces after this change.
Image-only tool results intentionally keep the existing
(see attached image)fallback when the target model/provider cannot accept image input.Relationship To Prior Work
This updates and broadens the earlier provider-only direction in #96895 by adding type-safe helper boundaries, fixing test issues, covering active transport paths, removing the provider-local structured-output cap, and documenting the remaining scope boundary.
Refs #96857