Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
40b3d7a
feat(agent-core): rework compaction to keep only user prompts and sum…
RealKai42 Jun 30, 2026
1827136
Merge remote-tracking branch 'origin/main' into polish-compact
RealKai42 Jun 30, 2026
faf0df2
refactor(agent-core): rewrite compaction summary as first-person handoff
RealKai42 Jun 30, 2026
8f1aa54
Merge remote-tracking branch 'origin/main' into polish-compact
RealKai42 Jun 30, 2026
72657f2
fix(agent-core): count image/audio/video parts in token estimation
RealKai42 Jun 30, 2026
782d73a
feat(agent-core): re-surface active background tasks after compaction
RealKai42 Jun 30, 2026
1ddc463
test(agent-core): add compaction scenario guards and risk probes
RealKai42 Jun 30, 2026
e8f0589
fix(agent-core): repair tool_use/tool_result adjacency in projected c…
RealKai42 Jun 30, 2026
914ae17
fix(agent-core): preserve the verbatim tail when restoring legacy com…
RealKai42 Jun 30, 2026
43b973a
fix(agent-core): align legacy compaction foldedLength with live restore
RealKai42 Jun 30, 2026
24a5083
fix(kosong): merge a follow-up user turn into the preceding tool_results
RealKai42 Jun 30, 2026
572e17e
test(agent-core): pin micro-compaction flag in the shrunk-suffix probe
RealKai42 Jun 30, 2026
e861675
fix(agent-core): harden full compaction against in-flight races, unbo…
RealKai42 Jun 30, 2026
0b7562b
fix(vis): mirror legacy compaction tail in the model-mode projector
RealKai42 Jun 30, 2026
3cb4a9e
Merge origin/main into polish-compact
RealKai42 Jun 30, 2026
5a72dc9
fix(agent-core): cancel compaction on any droppable user-role tail
RealKai42 Jun 30, 2026
f3dbf4b
fix(agent-core): exclude pre-clear prompts from legacy folded length
RealKai42 Jun 30, 2026
9d77c5b
fix(agent-core): drop media when truncating the oldest kept prompt
RealKai42 Jun 30, 2026
b0fd186
fix(agent-core): make manual compaction and turns mutually exclusive
RealKai42 Jun 30, 2026
7ceef00
chore(changeset): consolidate compaction changesets into one
RealKai42 Jun 30, 2026
fa519a7
chore(agent-core): drop external-product references from compaction c…
RealKai42 Jun 30, 2026
fd2d462
test(agent-core): add Anthropic wire-compliance smoke tests for compa…
RealKai42 Jun 30, 2026
7a1d84b
Merge remote-tracking branch 'origin/main' into polish-compact
RealKai42 Jun 30, 2026
ecbe650
fix(agent-core): defer and replay inputs during manual compaction ins…
RealKai42 Jun 30, 2026
6755eeb
Merge remote-tracking branch 'origin/main' into polish-compact
RealKai42 Jun 30, 2026
fc83bd5
feat(kosong): detect tool_use/tool_result adjacency errors
RealKai42 Jun 30, 2026
efec213
fix(agent-core): close mid-history orphan tool calls and resend wire-…
RealKai42 Jun 30, 2026
006b66c
Merge branch 'polish-compact' into fix-tool-exchange-wire-compliance
RealKai42 Jun 30, 2026
30c633b
fix(kosong): merge consecutive user turns for strict providers
RealKai42 Jun 30, 2026
21f4f39
Merge branch 'polish-compact' into fix-tool-exchange-wire-compliance
RealKai42 Jun 30, 2026
b65eb8c
feat(kosong): recognize the broader structural request-rejection family
RealKai42 Jun 30, 2026
52d49b3
fix(agent-core): sanitize whitespace and strict-resend structural 400…
RealKai42 Jun 30, 2026
2b137fd
Merge remote-tracking branch 'origin/main' into fix-tool-exchange-wir…
RealKai42 Jun 30, 2026
bb419b8
fix(agent-core): normalize empty-equivalent tool result arrays to the…
RealKai42 Jun 30, 2026
7d8081b
chore(changeset): simplify the wire-compliance changeset
RealKai42 Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/harden-strict-provider-wire-compliance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Stop a malformed message history from permanently bricking a session on strict providers (Anthropic). The request is repaired before sending — orphaned tool calls are closed and empty/whitespace-only text blocks dropped — and if the provider still rejects its structure, it is resent once with a wire-compliant rebuild.
107 changes: 103 additions & 4 deletions packages/agent-core/src/agent/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import {
type CompactionInput,
type CompactionResult,
} from '../compaction';
import { project, type ProjectOptions, trimTrailingOpenToolExchange } from './projector';
import {
project,
type ProjectionAnomaly,
type ProjectOptions,
trimTrailingOpenToolExchange,
} from './projector';
import {
USER_PROMPT_ORIGIN,
type AgentContextData,
Expand Down Expand Up @@ -43,6 +48,9 @@ export class ContextMemory {
private pendingToolResultIds = new Set<string>();
private deferredMessages: ContextMessage[] = [];
private _lastAssistantAt: number | null = null;
// Signature of the last logged set of projection repairs, so a repair that
// recurs identically on every send is logged once rather than per step.
private lastProjectionRepairSignature: string | null = null;

constructor(protected readonly agent: Agent) {}

Expand Down Expand Up @@ -314,13 +322,95 @@ export class ContextMemory {
}

project(messages: readonly ContextMessage[], options?: ProjectOptions): Message[] {
return project(this.agent.microCompaction.compact(messages), options);
const anomalies: ProjectionAnomaly[] = [];
const result = project(this.agent.microCompaction.compact(messages), {
...options,
onAnomaly: (anomaly) => {
anomalies.push(anomaly);
options?.onAnomaly?.(anomaly);
},
});
this.reportProjectionRepairs(anomalies);
return result;
}

// Surface the projector's wire-repairs so a silently-mangled history leaves a
// trace instead of being papered over. Deduped by signature: a repair that
// recurs identically every send (e.g. a persistently lost result re-synthesized
// each turn) logs once, not per step. Trailing-tail synthesis is excluded — it
// is the expected close of an in-flight call under `synthesizeMissing`
// (compaction / strict resend), not a defect.
private reportProjectionRepairs(anomalies: readonly ProjectionAnomaly[]): void {
const notable = anomalies.filter(
(anomaly) => !(anomaly.kind === 'tool_result_synthesized' && anomaly.trailing),
);
if (notable.length === 0) {
this.lastProjectionRepairSignature = null;
return;
}
const signature = notable
.map((anomaly) => ('toolCallId' in anomaly ? `${anomaly.kind}:${anomaly.toolCallId}` : anomaly.kind))
.toSorted()
.join('|');
if (signature === this.lastProjectionRepairSignature) return;
this.lastProjectionRepairSignature = signature;

let reordered = 0;
let synthesized = 0;
let droppedOrphan = 0;
let leadingDropped = 0;
let assistantsMerged = 0;
let whitespaceDropped = 0;
for (const anomaly of notable) {
if (anomaly.kind === 'tool_result_reordered') reordered += 1;
else if (anomaly.kind === 'tool_result_synthesized') synthesized += 1;
else if (anomaly.kind === 'orphan_tool_result_dropped') droppedOrphan += 1;
else if (anomaly.kind === 'leading_non_user_dropped') leadingDropped += 1;
else if (anomaly.kind === 'consecutive_assistants_merged') assistantsMerged += 1;
else whitespaceDropped += 1;
}
const toolCallIds = [
...new Set(
notable.flatMap((anomaly) => ('toolCallId' in anomaly ? [anomaly.toolCallId] : [])),
),
].slice(0, 5);
this.agent.log.warn('repaired the request to keep it wire-valid', {
reordered,
synthesized,
droppedOrphan,
leadingDropped,
assistantsMerged,
whitespaceDropped,
toolCallIds,
});
this.agent.telemetry.track('context_projection_repaired', {
reordered,
synthesized,
dropped_orphan: droppedOrphan,
leading_dropped: leadingDropped,
assistants_merged: assistantsMerged,
whitespace_dropped: whitespaceDropped,
});
}

get messages(): Message[] {
return this.project(this.history);
}

// Last-resort projection for the post-400 strict resend: close every open tool
// call (including a trailing in-flight one) and drop any stray tool result with
// no matching call, so the request is wire-compliant for strict providers no
// matter how the history was mangled. Only used when the provider has already
// rejected the normal projection — see the adjacency fallback in `turn-step`.
get strictMessages(): Message[] {
return this.project(this.history, {
synthesizeMissing: true,
dropOrphanResults: true,
dropLeadingNonUser: true,
mergeConsecutiveAssistants: true,
});
}

useProjectedHistoryFrom(source: ContextMemory): void {
this.clear();
this.pushHistory(...trimTrailingOpenToolExchange(source.project(source.history)));
Expand Down Expand Up @@ -500,7 +590,12 @@ function toolResultOutputForModel(result: ExecutableToolResult): string | Conten
return isEmptyOutputText(output) ? TOOL_EMPTY_STATUS : output;
}

if (output.length === 0) {
// Treat an array output with no sendable content (empty, or only empty/
// whitespace-only text blocks) the same as an empty string output: emit the
// placeholder. Otherwise projection would strip the blank text blocks, leave
// the tool message empty, and throw on every send — bricking the session. A
// non-text part (image/etc.) or any non-whitespace text keeps the real output.
if (isEmptyEquivalentContentArray(output)) {
return [
{
type: 'text',
Expand All @@ -514,8 +609,12 @@ function toolResultOutputForModel(result: ExecutableToolResult): string | Conten
return output;
}

function isEmptyEquivalentContentArray(output: readonly ContentPart[]): boolean {
return output.every((part) => part.type === 'text' && part.text.trim().length === 0);
}

function isEmptyOutputText(output: string): boolean {
return output.length === 0 || output.trim() === TOOL_OUTPUT_EMPTY_TEXT;
return output.trim().length === 0 || output.trim() === TOOL_OUTPUT_EMPTY_TEXT;
}

function formatUndoUnavailableMessage(
Expand Down
Loading
Loading