mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix: drop empty text blocks during projection (#910)
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
Some checks are pending
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
Nix Build / Check flake.nix workspace sync (push) Waiting to run
Nix Build / nix build .#kimi-code (push) Blocked by required conditions
Release / Release (push) Waiting to run
Release / Deploy docs (push) Blocked by required conditions
Release / Native release artifact (push) Blocked by required conditions
Release / Publish native release assets (push) Blocked by required conditions
This commit is contained in:
parent
710277a34b
commit
7644f1036c
3 changed files with 119 additions and 11 deletions
5
.changeset/clean-empty-text-blocks.md
Normal file
5
.changeset/clean-empty-text-blocks.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Fix provider requests failing when restored conversation history contains empty text content blocks.
|
||||
|
|
@ -1,23 +1,18 @@
|
|||
import type { ContentPart, Message, TextPart } from '@moonshot-ai/kosong';
|
||||
|
||||
import { ErrorCodes, KimiError } from '../../errors';
|
||||
import type { ContextMessage } from './types';
|
||||
|
||||
export function project(history: readonly ContextMessage[]): Message[] {
|
||||
// Keep partial or empty assistant placeholders away from providers.
|
||||
// They can appear when a turn is aborted or errors before any content
|
||||
// or tool call is appended.
|
||||
const usable = history.filter((message) => {
|
||||
return (
|
||||
message.partial !== true &&
|
||||
!(message.role === 'assistant' && message.content.length === 0 && message.toolCalls.length === 0)
|
||||
);
|
||||
});
|
||||
return mergeAdjacentUserMessages(usable);
|
||||
return mergeAdjacentUserMessages(history);
|
||||
}
|
||||
|
||||
function mergeAdjacentUserMessages(history: readonly ContextMessage[]): Message[] {
|
||||
const out: ContextMessage[] = [];
|
||||
for (const message of history) {
|
||||
for (const source of history) {
|
||||
const message = prepareMessageForProjection(source);
|
||||
if (message === null) continue;
|
||||
|
||||
const previous = out.at(-1);
|
||||
if (
|
||||
canMergeUserMessage(message) &&
|
||||
|
|
@ -32,6 +27,33 @@ function mergeAdjacentUserMessages(history: readonly ContextMessage[]): Message[
|
|||
return out.map(stripContextMetadata);
|
||||
}
|
||||
|
||||
function prepareMessageForProjection(message: ContextMessage): ContextMessage | null {
|
||||
if (message.partial === true) return null;
|
||||
|
||||
let content: ContentPart[] | undefined;
|
||||
for (const [index, part] of message.content.entries()) {
|
||||
if (part.type === 'text' && part.text.length === 0) {
|
||||
content ??= message.content.slice(0, index);
|
||||
continue;
|
||||
}
|
||||
content?.push(part);
|
||||
}
|
||||
|
||||
const next = content === undefined ? message : { ...message, content };
|
||||
if (next.role === 'tool' && next.content.length === 0) {
|
||||
throw new KimiError(
|
||||
ErrorCodes.REQUEST_INVALID,
|
||||
'Tool result message content cannot be empty after removing empty text blocks.',
|
||||
{
|
||||
details: {
|
||||
toolCallId: next.toolCallId,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
return next.content.length === 0 && next.toolCalls.length === 0 ? null : next;
|
||||
}
|
||||
|
||||
function canMergeUserMessage(message: ContextMessage): boolean {
|
||||
return message.role === 'user' && message.origin?.kind === 'user';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,87 @@ describe('Agent context', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('drops empty text parts only in LLM projection', () => {
|
||||
const history: ContextMessage[] = [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: '' },
|
||||
{ type: 'text', text: 'Run the tool' },
|
||||
],
|
||||
toolCalls: [],
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: '' }],
|
||||
toolCalls: [],
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: '' }],
|
||||
toolCalls: [{ type: 'function', id: 'call_empty', name: 'empty', arguments: '{}' }],
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [{ type: 'think', think: '', encrypted: 'enc_empty_thinking' }],
|
||||
toolCalls: [],
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: ' ' }],
|
||||
toolCalls: [],
|
||||
},
|
||||
];
|
||||
|
||||
expect(project(history)).toEqual([
|
||||
{
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: 'Run the tool' }],
|
||||
toolCalls: [],
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
toolCalls: [{ type: 'function', id: 'call_empty', name: 'empty', arguments: '{}' }],
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [{ type: 'think', think: '', encrypted: 'enc_empty_thinking' }],
|
||||
toolCalls: [],
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: ' ' }],
|
||||
toolCalls: [],
|
||||
},
|
||||
]);
|
||||
expect(history[0]?.content).toEqual([
|
||||
{ type: 'text', text: '' },
|
||||
{ type: 'text', text: 'Run the tool' },
|
||||
]);
|
||||
expect(history[1]?.content).toEqual([{ type: 'text', text: '' }]);
|
||||
});
|
||||
|
||||
it('rejects tool result messages left empty by LLM projection cleanup', () => {
|
||||
const history: ContextMessage[] = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
toolCalls: [{ type: 'function', id: 'call_empty', name: 'empty', arguments: '{}' }],
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
content: [{ type: 'text', text: '' }],
|
||||
toolCallId: 'call_empty',
|
||||
toolCalls: [],
|
||||
},
|
||||
];
|
||||
|
||||
expect(() => project(history)).toThrow(
|
||||
'Tool result message content cannot be empty after removing empty text blocks.',
|
||||
);
|
||||
});
|
||||
|
||||
it('projects hook result messages into LLM projection', async () => {
|
||||
const ctx = testAgent();
|
||||
ctx.configure();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue