-
Notifications
You must be signed in to change notification settings - Fork 649
fix(agent-core): drop vacuous assistant messages that permanently wedge sessions #1968
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1c0bce6
895b347
23229bf
f36d46a
4302327
b28a6f9
d7a59ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| "@moonshot-ai/agent-core": patch | ||
| "@moonshot-ai/agent-core-v2": patch | ||
| "@moonshot-ai/kosong": patch | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Fix sessions getting stuck on every turn with a provider "message must not be empty" error after a content-filtered response. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,9 +25,10 @@ | |
| * wrapping), clear its pending id | ||
| * - `step.end` → settle the assistant | ||
| * "Settle" closes any tool exchange left open (interrupted result messages), | ||
| * then drops the partial assistant when it is empty (no content, no tool | ||
| * calls — an empty assistant only trips provider message validation) and | ||
| * seals it (`partial: undefined`) when it carries output. v1 never produced | ||
| * then drops the partial assistant when nothing sendable was recorded (no | ||
| * tool calls; every content part vacuous — an output-free assistant only | ||
| * trips provider message validation) and seals it (`partial: undefined`) | ||
| * when it carries output. v1 never produced | ||
| * `step.begin` without `step.end` (its retries stayed inside one request), so | ||
| * the drop/seal rule is the v2 extension that makes loop-level retries — a | ||
| * retried attempt is its own `step.begin` — replay to the same history the | ||
|
|
@@ -47,6 +48,7 @@ import { createToolMessage, type ContentPart, type ToolCall } from '#/app/llmPro | |
| import type { TokenUsage } from '#/app/llmProtocol/usage'; | ||
|
|
||
| import type { ContextMessage } from './types'; | ||
| import { isVacuousContentPart } from './vacuousContent'; | ||
|
|
||
| const TOOL_INTERRUPTED_ON_RESUME_OUTPUT = | ||
| 'Tool execution was interrupted before its result was recorded. Do not assume the tool completed successfully.'; | ||
|
|
@@ -215,7 +217,7 @@ function settleOpenStep( | |
| const index = findOpenAssistantIndex(closed); | ||
| if (index === -1) return closed; | ||
| const open = closed[index]!; | ||
| if (open.content.length === 0 && open.toolCalls.length === 0) { | ||
| if (open.toolCalls.length === 0 && open.content.every(isVacuousContentPart)) { | ||
| return [...closed.slice(0, index), ...closed.slice(index + 1)]; | ||
|
Comment on lines
+220
to
221
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When replaying an older v2 wire log whose Useful? React with 👍 / 👎. |
||
| } | ||
| const next = closed.slice(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| /** | ||
| * `contextMemory` vacuous-content predicate — shared test for content parts | ||
| * that carry nothing the provider wire can represent, used by the loop-event | ||
| * fold (settle-time drop of output-free steps) and the context projector | ||
| * (wire-time drop of wholly-vacuous messages). Vacuous means an empty or | ||
| * whitespace-only text block, or an empty thinking block with no provider | ||
| * signature; a signed thinking block (`encrypted`) is never vacuous — | ||
| * reasoning providers require it back verbatim — and media parts always | ||
| * carry content. | ||
| */ | ||
|
|
||
| import type { ContentPart } from '#/app/llmProtocol/message'; | ||
|
|
||
| export function isVacuousContentPart(part: ContentPart): boolean { | ||
| if (part.type === 'text') return part.text.trim().length === 0; | ||
| if (part.type === 'think') return part.encrypted === undefined && part.think.trim().length === 0; | ||
| return false; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.