Repository navigation
fix(agent-core-v2): ensure tool_result content is never empty - #4269
kimi-agent-bot wants to merge 5 commits into
Conversation
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 detectedLatest commit: ee32057 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
commit: |
There was a problem hiding this comment.
💡 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".
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.
There was a problem hiding this comment.
💡 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.
|
@codex review |
|
Codex Review: Didn't find any major issues. What shall we delve into next? Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
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.
|
@codex review |
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
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". |
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/compactand resume fail identically — the session is permanently bricked.Root cause
Two independent gaps, both confirmed against the reporter's real wire history:
tool.resultwire record whoseoutputis neither a string nor a part array folds into a tool message withcontent: [], which lowers to atool_resultwith empty content (same for OpenAI tool messages / Responsesfunction_call_output).[text, video_url(inline data: URI), text], non-empty content — but the gateway rejects base64 video blocks insidetool_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'stoolResultToBlock, 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 ontool_result+contentfield-required and common video-unsupported wordings, sotool_use.input: Field requiredand other fields deliberately do not match) andisUnsupportedMediaError(mirrorsisImageFormatError, excludes overflow/too-large subclasses). The existing degrade-then-strip recovery ladder innextProjectionPolicyForErrornow 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 reusesimage_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
tool.result(output = non-array object / empty array / null) fold to empty content, then lower to a non-emptytool_result.tool_use.input/textfield-required, 429/500, and overflow/too-large subclasses do not.