Skip to content

fix(agent-core-v2): ensure tool_result content is never empty - #4269

Open
kimi-agent-bot wants to merge 5 commits into
MoonshotAI:mainfrom
kimi-agent-bot:fix/tool-result-content-not-empty
Open

kimi-agent-bot wants to merge 5 commits into
MoonshotAI:mainfrom
kimi-agent-bot:fix/tool-result-content-not-empty

Conversation

@kimi-agent-bot

@kimi-agent-bot kimi-agent-bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

After reading a local mp4 with the built-in ReadMediaFile tool, every subsequent model request in the session fails with HTTP 400 messages.56.content.0.tool_result.content: Field required (Anthropic-protocol endpoint). The bad tool result is persisted in the session history, so /compact and resume fail identically — the session is permanently bricked.

Root cause

Two independent gaps, both confirmed against the reporter's real wire history:

  1. Empty tool_result content. A persisted tool.result wire record whose output is neither a string nor a part array folds into a tool message with content: [], which lowers to a tool_result with empty content (same for OpenAI tool messages / Responses function_call_output).
  2. Gateway-rejected media (the reporter's actual case). The session's video tool result was well-formed — [text, video_url(inline data: URI), text], non-empty content — but the gateway rejects base64 video blocks inside tool_result. The 400 matched no recovery predicate (not request-too-large, not image-format, not structural), so the media rejection recovery ladder never engaged and the session could never self-heal.

Fix

Layer 1 — never serialize an empty tool result. When a tool result lowers to no content, emit a non-empty placeholder (Tool output is empty.) instead, across the agent-core-v2 Anthropic lower, kosong's toolResultToBlock, the OpenAI base lower, and the OpenAI Responses lower. Media-only tool results keep their media blocks unchanged.

Layer 2 — recover from provider-rejected media. New unsupported-media error family: isUnsupportedMediaStatusError (400/422; patterns anchored on tool_result+content field-required and common video-unsupported wordings, so tool_use.input: Field required and other fields deliberately do not match) and isUnsupportedMediaError (mirrors isImageFormatError, excludes overflow/too-large subclasses). The existing degrade-then-strip recovery ladder in nextProjectionPolicyForError now also triggers on this predicate: older media degrade to placeholders first, and if the gateway still rejects the request, the rejected media are stripped and the request is retried. User-visible error kind reuses image_format; telemetry projection values are unchanged (media-degraded/media-stripped).

Because every request re-lowers from the wire history, both layers self-heal already-affected sessions with no migration.

Tests

  • Empty/whitespace-only tool results lower to placeholder text; video-only results keep their video block with no added text anchor (agent-core-v2 anthropic lower, kosong anthropic provider, OpenAI keep_parts/extract_text, OpenAI Responses).
  • Self-heal: wire records with a corrupt tool.result (output = non-array object / empty array / null) fold to empty content, then lower to a non-empty tool_result.
  • Predicate: the reporter's exact 400 message matches; tool_use.input/text field-required, 429/500, and overflow/too-large subclasses do not.
  • Ladder: a recent inline video (degrade can't drop it) is stripped on the third attempt; an older inline video is degraded away on the second; non-media 400s never enter the ladder.
  • Full agent-core-v2 suite: 374 files / 7278 tests green; kosong suite: 49 files / 1375 green; typecheck + oxlint clean.

A tool result that serialized to empty content (e.g. a persisted
tool.result wire record whose output was neither a string nor a part
array, folded back as an empty tool message) was re-lowered into every
subsequent request, and Anthropic-protocol endpoints reject it with
400 "tool_result.content: Field required" — permanently bricking the
session, with /compact and resume failing the same way.

Guard the serialization branches so an empty tool result lowers to a
non-empty placeholder ("Tool output is empty.") instead:

- agent-core-v2 anthropic lower: pad an empty tool_result block list
- kosong anthropic toolResultToBlock: same guard
- agent-core-v2 openai lower: placeholder for empty string/part list
- agent-core-v2 openai-responses lower: placeholder for empty output

Since every request re-lowers from the wire history, fixing the
serialization self-heals already-corrupted sessions with no migration.
Media-only tool results keep their media blocks unchanged.
@changeset-bot

changeset-bot Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ee32057

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

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code 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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-18T14:06:31.961137Z ee32057 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pkg-pr-new

pkg-pr-new Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@ee32057
npx https://pkg.pr.new/@moonshot-ai/kimi-code@ee32057

commit: ee32057

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2153b5d1f3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread .changeset/tool-result-content-not-empty.md Outdated
kimi-agent-bot and others added 2 commits October 18, 2026 13:38
The empty-content guard was not the whole story: a session whose video
tool result is a well-formed [text, inline video, text] still bricked
when an Anthropic-protocol gateway rejects base64 video blocks inside
tool_result (400 "messages.N.content.0.tool_result.content: Field
required"). The error matched no recovery predicate — not too-large,
not image-format, not structural — so every retry, /compact, and
resume re-sent the same history and failed identically.

Add an unsupported-media error family and wire it into the media
recovery ladder:

- human/llm/errors: isUnsupportedMediaStatusError (400/422) with
  patterns anchored on tool_result+content field-required and common
  video-unsupported wordings; user-visible kind maps to image_format
- llm-adapter/contract/errors: isUnsupportedMediaError mirroring
  isImageFormatError, excluding overflow/too-large subclasses
- llmRequesterService: the too-large escalation now also triggers on
  isUnsupportedMediaError, reusing the degrade-then-strip projections
  and telemetry; log wording distinguishes the two triggers

Field-required errors on other fields (tool_use.input, text) and
non-400/422 statuses deliberately do not match, so unaffected sessions
never get their media degraded.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf7a93ee58

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

…a warnings

The media recovery ladder now also triggers on unsupported-media 400s,
but the user-facing WarningIssued for the degraded retry still said the
provider rejected the request as too large. Carry the rejection cause
from nextProjectionPolicyForError to the warning site and word the
degraded-media warning accordingly; the too-large wording is preserved
for size rejections.
@kimi-agent-bot

Copy link
Copy Markdown
Collaborator Author

@codex review
增量评审:请只审自上次评审以来的新增 commit;未变更代码与已 resolve 的主题请勿重复提出(其余约定同首轮)。

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

Reviewed commit: 674e61b59f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

The unsupported-media recovery family also matches conversion-time
ChatProviderErrors ("Unsupported media type for base64 video"), so an
unconvertible video in the prompt is now degraded, then stripped, and
the retried request completes the turn instead of failing it (and
poisoning every later turn through the persisted history). Update the
e2e case to pin the recovery: one outgoing request carrying the strip
placeholder, no video block, and a completed prompt.
@kimi-agent-bot

Copy link
Copy Markdown
Collaborator Author

@codex review
增量评审:请只审自上次评审以来的新增 commit;未变更代码与已 resolve 的主题请勿重复提出(其余约定同首轮)。

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: ee3205753f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

This branch has not been deployed

No deployments
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