Skip to content

fix(llm): preserve structured tool results as text across providers#97183

Closed
snowzlmbot wants to merge 5 commits into
openclaw:mainfrom
snowzlmbot:fix/96857-tool-result-text
Closed

fix(llm): preserve structured tool results as text across providers#97183
snowzlmbot wants to merge 5 commits into
openclaw:mainfrom
snowzlmbot:fix/96857-tool-result-text

Conversation

@snowzlmbot

@snowzlmbot snowzlmbot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Preserve structured non-image tool result blocks as text instead of collapsing them to (see attached image).
  • Share the normalization through extractToolResultText() across OpenAI Completions, OpenAI Responses, Google/Gemini, Anthropic, and Mistral conversion paths.
  • Keep structured serialization lossless at the provider conversion helper boundary; existing context/tool-result guards remain the payload-budgeting and truncation owner.
  • Cover active transport paths for OpenAI Responses and Anthropic, plus provider conversion paths and focused regressions.

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

  • Structured JSON/resource-style tool results remain visible to the model as text.
  • Image-only tool results still keep the existing (see attached image) fallback when the target path cannot carry image input.
  • Large structured blocks are no longer silently truncated at 1,000 characters before the normal context/tool-result truncation policy can budget them.

Review Follow-Up

  • Fixed the TypeScript blocker by widening extractToolResultText() to accept readonly unknown[] and narrowing internally.
  • Removed unsafe Record<string, unknown>[] casts from provider call sites.
  • Fixed the OpenAI Responses regression test scope by moving it under convertResponsesMessages, using testAllowedToolCallProviders, and casting the intentional structured-content fixture at the boundary.
  • Added direct regression coverage for active OpenAI Responses and Anthropic transport conversion.
  • Expanded same-root coverage to Anthropic and Mistral provider conversions.
  • Resolved CI lint/type issues: unused imports, single-line if/continue braces, no-base-to-string guards via typeof checks, shadowed context variable warnings, and type-cast boundaries in tests.
  • Addressed the latest ClawSweeper review finding by removing the 1,000-character helper-local structured-output cap from src/llm/providers/tool-result-text.ts.
  • Added src/llm/providers/tool-result-text.test.ts to prove the provider helper does not truncate structured blocks before the established payload-budgeting boundary.

Evidence

  • git diff --check passes clean for this head.
  • Focused helper regression passed:
    • node scripts/run-vitest.mjs src/llm/providers/tool-result-text.test.ts
  • Earlier focused provider/transport regression gate passed during this PR fix loop:
    • node 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.ts
    • Result: 7 files passed, 437 tests passed.
  • Redacted real-path evidence from local OpenClaw session investigation:
    • Structured tool-result content without plain text blocks was observed in a local OpenClaw session as an attachment placeholder in the next assistant turn instead of visible text.
    • The provider helper fix keeps structured payload text intact through the model-visible conversion boundary.
    • The focused regression uses a structured payload longer than 1,000 characters with a tail-marker, verifies the text length stays above 1,200 characters, and verifies the tail survives without the previous ... (N chars) truncation marker.
    • Local recovery artifacts and sanitized investigation notes were written under projects/openclaw-pr-maintenance/reports/pr-97183/local-machine-fix/; private host details are intentionally not published.
  • GitHub Actions CI remains the authoritative gate for broad repo checks on this PR.

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

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
@openclaw-barnacle openclaw-barnacle Bot added size: S triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. labels Jun 27, 2026
@clawsweeper

clawsweeper Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 27, 2026, 8:55 AM ET / 12:55 UTC.

Summary
Adds a shared tool-result text extractor and routes structured non-image tool-result serialization through OpenAI, Anthropic, Google/Gemini, and Mistral provider and transport conversions with regression tests.

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.

  • Serializer redaction boundary: 1 added. The PR adds one model-visible string rewriting rule before provider payload construction, which matters because the stated helper boundary is supposed to stay lossless.

Stored data model
Persistent data-model change detected: serialized state: src/agents/anthropic-transport-stream.test.ts, serialized state: src/llm/providers/tool-result-text.ts. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #96857
Summary: This PR is a candidate partial fix for the open canonical tool-result placeholder issue; an older overlapping PR remains open but is not a merged or proof-positive superseder.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🦐 gold shrimp
Result: blocked until real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Add inspectable redacted provider/session proof showing structured tool results reach the next model turn as text.
  • [P1] Tighten or remove the broad data: redaction and add focused regression coverage for ordinary structured text containing that substring.

Proof guidance:

  • [P1] Needs real behavior proof before merge: The PR body mentions local redacted investigation artifacts, but no inspectable redacted logs, terminal output, copied live output, recording, or linked artifact shows after-fix real behavior; contributors should redact private details before posting proof and update the PR body to trigger review, or ask a maintainer to comment @clawsweeper re-review.

Risk before merge

  • [P1] The new helper can still corrupt model-visible structured text by replacing any data: substring, even when it is ordinary content rather than an inline data URI.
  • [P1] The contributor has not provided inspectable redacted after-fix provider/session proof, so tests alone do not prove the live agent path is fixed.
  • [P1] The broader prompt-history truncation plus provider-failover/session-corruption report remains outside this PR's proven scope and should stay open unless separately verified.

Maintainer options:

  1. Fix serializer and add proof before merge (recommended)
    Tighten or remove the broad data: replacement, add a regression for ordinary structured text containing that substring, and require inspectable redacted provider or session output showing the after-fix behavior.
  2. Accept early redaction as product policy
    Maintainers could intentionally keep data-URI redaction at the provider helper boundary, but that means accepting this helper is not lossless and documenting that boundary.
  3. Pause behind the canonical issue
    If maintainers want the broader failover/session-corruption path proven first, pause this PR and keep the canonical issue as the single review target.

Next step before merge

  • [P1] Contributor proof is required, and the remaining data-redaction choice needs a maintainer-visible fix or acceptance before merge; repair automation should not be queued while proof is missing.

Security
Cleared: No concrete security or supply-chain regression was found; the diff changes provider conversion code and tests without new dependencies, workflows, permissions, lockfiles, or secret handling.

Review findings

  • [P2] Tighten structured text redaction to real data URIs — src/llm/providers/tool-result-text.ts:9-10
Review details

Best 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:

  • [P2] Tighten structured text redaction to real data URIs — src/llm/providers/tool-result-text.ts:9-10
    The JSON replacer runs on every string and matches data: anywhere inside the value, so ordinary content like metadata:foo is rewritten before reaching the model. Bound this to actual data URI tokens or move redaction out of this lossless provider helper.
    Confidence: 0.88

Overall correctness: patch is incorrect
Overall confidence: 0.88

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against c5d34c8376f8.

Label changes

Label justifications:

  • P1: The PR targets model-visible tool-result loss that can break active agent workflows and still has a delivery-affecting patch defect plus missing proof.
  • merge-risk: 🚨 message-delivery: The diff changes delivery of tool-result content to model providers, and the current serializer can still corrupt structured text before it reaches the model.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🦐 gold shrimp.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR body mentions local redacted investigation artifacts, but no inspectable redacted logs, terminal output, copied live output, recording, or linked artifact shows after-fix real behavior; contributors should redact private details before posting proof and update the PR body to trigger review, or ask a maintainer to comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +67, Tests +398. Total +465 across 15 files.

View PR surface stats
Area Files Added Removed Net
Source 8 138 71 +67
Tests 7 399 1 +398
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 15 537 72 +465

What I checked:

  • Root policy read: Root AGENTS.md was read fully and its provider/runtime proof, whole-surface review, and message-delivery risk guidance apply to this PR. (AGENTS.md:1, c5d34c8376f8)
  • Scoped policy read: The scoped agents guide was read for the touched active transport tests and runtime; it adds test-performance guidance but no contrary maintainer decision. (src/agents/AGENTS.md:1, c5d34c8376f8)
  • Current main source-reproduces OpenAI Responses loss: Current main filters tool-result content to text blocks before constructing Responses function_call_output, so structured-only non-image blocks are dropped and can become the placeholder. (src/llm/providers/openai-responses-shared.ts:375, c5d34c8376f8)
  • Current main source-reproduces active OpenAI transport loss: Current main's active OpenAI transport converter has the same text-only extraction before function_call_output construction. (src/agents/openai-transport-stream.ts:1276, c5d34c8376f8)
  • Current main source-reproduces Google provider loss: Current main's Google/Gemini conversion extracts only TextContent blocks for tool responses before building the provider payload. (src/llm/providers/google-shared.ts:281, c5d34c8376f8)
  • PR serializer still rewrites ordinary structured strings: PR head runs a broad /data:/ replacement on every string while claiming the helper remains lossless, so ordinary values like metadata:foo can be changed before reaching the model. (src/llm/providers/tool-result-text.ts:9, 300aa821e977)

Likely related people:

  • steipete: Recent merged work touched active OpenAI transport, OpenAI provider replay, and adjacent terminal/tool-call handling paths that share this PR's runtime surface. (role: recent adjacent contributor and merger; confidence: high; commits: 56259606d15f, 439a9e97fd61, 2a6eeceb40fe; files: src/agents/openai-transport-stream.ts, src/llm/providers/openai-responses-shared.ts, src/llm/providers/google-shared.ts)
  • vincentkoc: Recent commits touched session guard, OpenAI transport, and provider conversion surfaces relevant to payload budgeting and replay behavior. (role: recent provider and tool-result surface contributor; confidence: high; commits: abd8a46b0a52, 345ad9862d1c, 2920dc32820c; files: src/agents/session-tool-result-guard.ts, src/agents/openai-transport-stream.ts, src/llm/providers/openai-responses-shared.ts)
  • 849261680: Authored the merged MCP materialization fix that normalized non-text/image tool-result blocks before provider conversion, an adjacent structured-result boundary. (role: prior structured tool-result normalization contributor; confidence: medium; commits: b1e4b6b65e2c, 054312672acd; files: src/agents/agent-bundle-mcp-materialize.ts, src/agents/agent-bundle-mcp-tools.materialize.test.ts)
  • amersheeny: Recently carried a substantial OpenAI Responses stream/replay fix touching the shared Responses conversion surface this PR modifies. (role: recent OpenAI Responses contributor; confidence: medium; commits: 9fbc8a74efa7; files: src/llm/providers/openai-responses-shared.ts, src/agents/openai-transport-stream.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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 keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. labels Jun 27, 2026
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: L and removed size: S labels Jun 27, 2026
@snowzlmbot

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@NianJiuZst NianJiuZst 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.

I focused on the two code-local blockers that can be fixed directly on this branch.

(record) => record.type === "tool_result" && record.tool_use_id === "tool_1",
);
// Structured non-image block should be serialized, not dropped
expect(toolResult.content).toBe(

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.

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.

Comment thread src/llm/providers/tool-result-text.ts Outdated
if (!serialized || serialized === "{}") {
return undefined;
}
return serialized.length > 1_000

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.

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.

@clawsweeper

clawsweeper Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@openclaw-barnacle openclaw-barnacle Bot removed the triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. label Jun 27, 2026
@snowzlmbot

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

1 similar comment
@snowzlmbot

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@snowzlmbot
snowzlmbot requested a review from NianJiuZst June 27, 2026 07:22
@snowzlmbot

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@snowzlmbot
snowzlmbot marked this pull request as draft June 27, 2026 10:24
@snowzlmbot
snowzlmbot marked this pull request as ready for review June 27, 2026 11:56
@snowzlmbot

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@snowzlmbot

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@snowzlmbot

Copy link
Copy Markdown
Contributor Author

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.

@snowzlmbot

Copy link
Copy Markdown
Contributor Author

Superseded by clean replacement PR: #97268

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P1 High-priority user-facing bug, regression, or broken workflow. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: L status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants