Skip to content

Commit 9bc3c7e

Browse files
committed
fix: clean up v2 projection and cold history output
- skip empty thinking parts in the agent loop (live stream, interrupted drain, cold history) so no empty thinking shells are emitted - emit the steered prompt's user message when submission races with turn completion, assigning it to the new turn exactly once - emit goal system entries only on goal status transitions, and mirror that in cold history (goal.create produces the active entry, goal.update/clear produce none) - let before_turn history paging reach past the compaction floor so loading earlier messages keeps returning pages - refresh the v2 example fixtures and add a steer-race projection case
1 parent 751c70e commit 9bc3c7e

8 files changed

Lines changed: 3620 additions & 3532 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
Fix empty thinking entries recorded for steps without reasoning content.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
Fix earlier-message history paging returning empty pages before compaction points.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
Emit goal status stream entries only when the goal status actually changes.

packages/agent-core-v2/src/agent/loop/loopService.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
932932
response: AgentLLMRequestFinish,
933933
): void {
934934
for (const part of response.message.content) {
935+
if (part.type === 'think' && part.think === '' && (part as { encrypted?: string }).encrypted === undefined) continue;
935936
this.context.appendLoopEvent({
936937
type: 'content.part',
937938
uuid: randomUUID(),
@@ -950,6 +951,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
950951
streamParts: StreamPartCollector,
951952
): void {
952953
for (const part of streamParts.drainInterruptedContent()) {
954+
if (part.type === 'think' && part.think === '' && (part as { encrypted?: string }).encrypted === undefined) continue;
953955
this.context.appendLoopEvent({
954956
type: 'content.part',
955957
uuid: randomUUID(),
@@ -1144,13 +1146,19 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
11441146
new AssistantDelta({ agentId: this.scopeContext.agentId, turnId, delta: part.text }),
11451147
);
11461148
return;
1147-
case 'think':
1149+
case 'think': {
1150+
const hasPayload =
1151+
part.think !== '' || (part as { encrypted?: string }).encrypted !== undefined;
1152+
if (!hasPayload) return;
11481153
onResponseEvent();
11491154
accumulate(part);
1150-
void this.dispatcher.dispatch(
1151-
new ThinkingDelta({ agentId: this.scopeContext.agentId, turnId, delta: part.think }),
1152-
);
1155+
if (part.think !== '') {
1156+
void this.dispatcher.dispatch(
1157+
new ThinkingDelta({ agentId: this.scopeContext.agentId, turnId, delta: part.think }),
1158+
);
1159+
}
11531160
return;
1161+
}
11541162
case 'image_url':
11551163
case 'audio_url':
11561164
case 'video_url':

packages/kap-server/src/services/v2Projection/agentProjector.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ export class AgentV2Projector {
587587
const acc = this.prompts.get(event.promptId as string);
588588
if (!acc || acc.status === 'completed') return;
589589
acc.status = 'completed';
590+
if (!acc.emitted) this.assignHeld(acc, this.maxTurnId + 1);
590591
out.push(this.userMessage(acc, event.time, (event.finishedAt as string) ?? iso(event.time)));
591592
}
592593

@@ -597,6 +598,7 @@ export class AgentV2Projector {
597598
if (qi >= 0) this.queue.splice(qi, 1);
598599
if (acc.status !== 'completed') {
599600
acc.status = 'completed';
601+
if (!acc.emitted) this.assignHeld(acc, this.maxTurnId + 1);
600602
out.push(this.userMessage(acc, event.time, (event.abortedAt as string) ?? iso(event.time)));
601603
}
602604
out.push({
@@ -608,6 +610,14 @@ export class AgentV2Projector {
608610
});
609611
}
610612

613+
private assignHeld(acc: PromptAcc, engineTurnId: number): void {
614+
const turnId = this.protocolTurnId(engineTurnId);
615+
const seq = this.nextUserSeq(engineTurnId);
616+
acc.messageId = `${turnId}.u${seq}`;
617+
acc.turnId = turnId;
618+
acc.emitted = true;
619+
}
620+
611621
private onTurnStarted(event: ProjectionEvent, out: ServerMessage[]): void {
612622
const engineTurnId = event.turnId as number;
613623
this.maxTurnId = Math.max(this.maxTurnId, engineTurnId);
@@ -629,14 +639,20 @@ export class AgentV2Projector {
629639
}
630640
turn.userSeq = maxSeq;
631641
const promptId = event.promptId as string | undefined;
642+
let heldAcc: PromptAcc | undefined;
632643
if (promptId) {
633644
const acc = this.prompts.get(promptId);
645+
if (acc && !acc.emitted) {
646+
this.assignHeld(acc, engineTurnId);
647+
heldAcc = acc;
648+
}
634649
turn.userMessageId = acc?.messageId ?? promptId;
635650
turn.promptIds.push(promptId);
636651
turn.attachmentIds = acc?.attachmentIds;
637652
}
638653
this.turns.set(engineTurnId, turn);
639654
out.push(this.turnMessage(turn, event.time));
655+
if (heldAcc) out.push(this.userMessage(heldAcc, event.time));
640656
}
641657

642658
private onTurnEnded(event: ProjectionEvent, out: ServerMessage[]): void {
@@ -762,6 +778,7 @@ export class AgentV2Projector {
762778
const step = this.currentStep;
763779
if (!step) return;
764780
let acc = kind === 'assistant' ? this.openAssistant : this.openThinking;
781+
if (!acc && delta.length === 0) return;
765782
if (!acc || acc.stepKey !== step.stepId) {
766783
this.closeOpenTexts(event.time, out);
767784
const seq = kind === 'assistant' ? step.textSeq.a++ : step.textSeq.h++;
@@ -1025,8 +1042,13 @@ export class AgentV2Projector {
10251042
});
10261043
}
10271044

1045+
private lastGoalStatus?: string;
1046+
10281047
private onGoalUpdated(event: ProjectionEvent, out: ServerMessage[]): void {
10291048
const snapshot = event.snapshot as { status?: string; objective?: string } | null | undefined;
1049+
const status = snapshot?.status;
1050+
if (status === this.lastGoalStatus) return;
1051+
this.lastGoalStatus = status;
10301052
if (!snapshot) return;
10311053
out.push({
10321054
type: 'system',

packages/kap-server/src/services/v2Projection/coldHistory.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ interface InteractionAcc {
105105
}
106106

107107
interface SystemAcc {
108-
subtype: 'interruption' | 'undo';
108+
subtype: 'interruption' | 'undo' | 'goal';
109109
payload: Record<string, unknown>;
110110
time?: number;
111111
recordIndex: number;
@@ -381,6 +381,7 @@ export function buildColdHistory(
381381
const kind = partType === 'think' ? 'thinking' : partType === 'text' ? 'assistant' : undefined;
382382
if (!kind) break;
383383
const text = kind === 'thinking' ? asText(part?.['think']) ?? '' : asText(part?.['text']) ?? '';
384+
if (text.length === 0) break;
384385
if (!step.openText || step.openText.kind !== kind) {
385386
sealOpenText(step);
386387
const seq = kind === 'assistant' ? step.textSeq.a++ : step.textSeq.h++;
@@ -507,6 +508,19 @@ export function buildColdHistory(
507508
});
508509
break;
509510
}
511+
case 'goal.create': {
512+
looseSystems.push({
513+
subtype: 'goal',
514+
payload: { status: 'active', objective: asText(record['objective']) },
515+
time,
516+
recordIndex,
517+
});
518+
break;
519+
}
520+
case 'goal.update':
521+
case 'goal.clear': {
522+
break;
523+
}
510524
case 'context.apply_compaction': {
511525
floorIndex = recordIndex;
512526
floorTime = time;
@@ -544,7 +558,7 @@ export function buildColdHistory(
544558
}
545559
}
546560

547-
const keptTurns = turns.filter((turn) => turn.lastRecordIndex > floorIndex);
561+
const keptTurns = query.beforeTurn !== undefined ? turns : turns.filter((turn) => turn.lastRecordIndex > floorIndex);
548562

549563
interface FlatUnit {
550564
pos: number;
@@ -775,7 +789,7 @@ export function buildColdHistory(
775789

776790
for (const task of tasks.values()) {
777791
const lastTime = task.terminatedTime ?? task.lastTime ?? task.startedTime;
778-
if (lastTime !== undefined && floorTime !== undefined && lastTime <= floorTime) continue;
792+
if (query.beforeTurn === undefined && lastTime !== undefined && floorTime !== undefined && lastTime <= floorTime) continue;
779793
const startedInfo = task.startedInfo;
780794
const terminatedInfo = task.terminatedInfo;
781795
units.push({

0 commit comments

Comments
 (0)