-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix(transcript): fold mid-turn task notifications into the current turn on cold rebuild #3102
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
base: main
Are you sure you want to change the base?
Changes from all commits
d9459ea
19e513c
599d0d9
0ccda12
04255a7
2f2b2c5
9905e3b
163ec98
b43e971
bbb2559
7c36d45
ea6d64d
57ee405
609d849
4733e9e
d86f3e3
3e5976e
2ea5f79
281ebf8
eae2920
7daf99e
11f9cd0
2f795cc
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,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Fix the cold transcript rebuild splitting a turn at background-task completion notices; they now fold into the current turn like the live stream does. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,8 @@ import type { | |
| PromptCompleted, | ||
| PromptSteered, | ||
| } from '@moonshot-ai/agent-core-v2/agent/prompt/promptService'; | ||
| import type { PromptAccepted } from '@moonshot-ai/agent-core-v2/agent/prompt/promptOps'; | ||
| import type { PromptQueued } from '@moonshot-ai/agent-core-v2/agent/prompt/promptService'; | ||
| import type { | ||
| ShellCompleted, | ||
| ShellOutput, | ||
|
|
@@ -89,6 +91,8 @@ export interface ProjectorInteraction { | |
| type PlanRevisionEvent = { readonly type: 'plan.revision' } & PlanRevision; | ||
|
|
||
| type AgentActivityUpdatedEvent = { readonly type: 'agent.activity.updated' } & AgentActivityUpdated; | ||
| type PromptAcceptedEvent = { readonly type: 'prompt.accepted' } & PromptAccepted; | ||
| type PromptQueuedEvent = { readonly type: 'prompt.queued' } & PromptQueued; | ||
| type PromptCompletedEvent = { readonly type: 'prompt.completed' } & PromptCompleted; | ||
| type PromptAbortedEvent = { readonly type: 'prompt.aborted' } & PromptAborted; | ||
| type PromptSteeredEvent = { readonly type: 'prompt.steered' } & PromptSteered; | ||
|
|
@@ -121,6 +125,8 @@ export type ProjectorBusEvent = | |
| | ({ readonly type: 'goal.updated' } & GoalUpdated) | ||
| | ({ readonly type: 'agent.status.updated' } & AgentStatusUpdated) | ||
| | AgentActivityUpdatedEvent | ||
| | PromptAcceptedEvent | ||
| | PromptQueuedEvent | ||
| | PromptCompletedEvent | ||
| | PromptAbortedEvent | ||
| | PromptSteeredEvent | ||
|
|
@@ -201,6 +207,7 @@ export class AgentTranscriptProjector { | |
| /** Latest header of the in-flight (or most recent) turn; kept whole so terminal upserts preserve `origin` / `startedAt` by reference. */ | ||
| private currentTurn: TurnHeader | undefined; | ||
| private currentStep: StepHeader | undefined; | ||
| private pendingTaskNotifications: { text: string; taskId: string | undefined }[] = []; | ||
| /** turnId → highest step ordinal seen (engine-reported placement hint). */ | ||
| private readonly stepOrdinals = new Map<string, number>(); | ||
| private frameOrdinal = 0; | ||
|
|
@@ -312,6 +319,10 @@ export class AgentTranscriptProjector { | |
| return this.onAgentStatusUpdated(event); | ||
| case 'agent.activity.updated': | ||
| return this.onAgentActivityUpdated(event); | ||
| case 'prompt.accepted': | ||
| return this.onPromptAccepted(event); | ||
| case 'prompt.queued': | ||
| return this.onPromptQueued(event); | ||
| case 'prompt.submitted': | ||
| return this.onPromptSubmitted(event); | ||
| case 'prompt.completed': | ||
|
|
@@ -379,9 +390,11 @@ export class AgentTranscriptProjector { | |
| startedAt: nowIso(), | ||
| }; | ||
| this.currentStep = undefined; | ||
| this.pendingTaskNotifications = []; | ||
| this.openText = undefined; | ||
| this.openThinking = undefined; | ||
| ops.push({ op: 'turn.upsert', turn: this.currentTurn }); | ||
| ops.push({ op: 'meta.merge', meta: { activity: 'turn' } }); | ||
| return ops; | ||
| } | ||
|
|
||
|
|
@@ -419,7 +432,9 @@ export class AgentTranscriptProjector { | |
| usage: this.takeTurnUsage(turnId), | ||
| }; | ||
| ops.push({ op: 'turn.upsert', turn: this.currentTurn }); | ||
| ops.push({ op: 'meta.merge', meta: { activity: 'idle' } }); | ||
| this.currentStep = undefined; | ||
| this.pendingTaskNotifications = []; | ||
| if (event.reason === 'cancelled' && event.interruptReason === 'user_cancelled') { | ||
| ops.push( | ||
| this.markerOp('interruption', { turnId: event.turnId, reason: event.interruptReason }), | ||
|
|
@@ -473,7 +488,23 @@ export class AgentTranscriptProjector { | |
| this.frameOrdinal = 0; | ||
| this.openText = undefined; | ||
| this.openThinking = undefined; | ||
| return [{ op: 'step.upsert', turnId, step: this.currentStep }]; | ||
| const ops: TranscriptOperation[] = [{ op: 'step.upsert', turnId, step: this.currentStep }]; | ||
| for (const pending of this.pendingTaskNotifications) { | ||
| ops.push({ | ||
| op: 'frame.upsert', | ||
| turnId, | ||
| stepId, | ||
| frame: { | ||
| kind: 'text', | ||
| frameId: `${stepId}.f${++this.frameOrdinal}`, | ||
| role: 'user', | ||
| text: pending.text, | ||
| taskId: pending.taskId, | ||
| }, | ||
| }); | ||
| } | ||
| this.pendingTaskNotifications = []; | ||
| return ops; | ||
| } | ||
|
|
||
| private onStepCompleted(event: { | ||
|
|
@@ -865,20 +896,21 @@ export class AgentTranscriptProjector { | |
| }): TranscriptOperation[] { | ||
| const step = this.currentStep; | ||
| const turn = this.currentTurn; | ||
| const midTurn = | ||
| step !== undefined && | ||
| turn !== undefined && | ||
| step.state === 'running' && | ||
| turn.state === 'running'; | ||
| if (!midTurn) return []; | ||
| const frame: TextFrame = { | ||
| kind: 'text', | ||
| frameId: `${step.stepId}.f${++this.frameOrdinal}`, | ||
| role: 'user', | ||
| text: `${event.title}\n${event.body}`.trim(), | ||
| taskId: event.sourceId, | ||
| }; | ||
| return [{ op: 'frame.upsert', turnId: turn.turnId, stepId: step.stepId, frame }]; | ||
| if (turn === undefined || turn.state !== 'running') return []; | ||
| const text = `${event.title}\n${event.body}`.trim(); | ||
| if (step !== undefined && step.state === 'running') { | ||
| const frame: TextFrame = { | ||
| kind: 'text', | ||
| frameId: `${step.stepId}.f${++this.frameOrdinal}`, | ||
| role: 'user', | ||
| text, | ||
| taskId: event.sourceId, | ||
| }; | ||
| return [{ op: 'frame.upsert', turnId: turn.turnId, stepId: step.stepId, frame }]; | ||
| } | ||
| if (turn.origin?.kind === 'task' && (turn.origin.taskId === undefined || turn.origin.taskId === event.sourceId)) return []; | ||
| this.pendingTaskNotifications.push({ text, taskId: event.sourceId }); | ||
| return []; | ||
| } | ||
|
|
||
| private onTaskLifecycle(event: { | ||
|
|
@@ -1046,6 +1078,8 @@ export class AgentTranscriptProjector { | |
| swarmIndex?: number; | ||
| runInBackground: boolean; | ||
| taskId?: string; | ||
| model?: string; | ||
| thinkingEffort?: string; | ||
| }): TranscriptOperation[] { | ||
| const taskKey = event.taskId ?? event.subagentId; | ||
| if (event.taskId !== undefined) { | ||
|
|
@@ -1063,6 +1097,8 @@ export class AgentTranscriptProjector { | |
| outputTail: prev?.outputTail ?? '', | ||
| startedAt: prev?.startedAt ?? nowIso(), | ||
| endedAt: prev?.endedAt, | ||
| model: event.model ?? prev?.model, | ||
| thinkingEffort: event.thinkingEffort ?? prev?.thinkingEffort, | ||
| })); | ||
| const ops: TranscriptOperation[] = [{ op: 'task.upsert', task }]; | ||
| const hit = | ||
|
|
@@ -1114,8 +1150,34 @@ export class AgentTranscriptProjector { | |
| usage: event.usage ?? prev?.usage, | ||
| error: event.error ?? prev?.error, | ||
| stateReason: event.reason ?? prev?.stateReason, | ||
| model: prev?.model, | ||
| thinkingEffort: prev?.thinkingEffort, | ||
| })); | ||
| return [{ op: 'task.upsert', task }]; | ||
| const ops: TranscriptOperation[] = [{ op: 'task.upsert', task }]; | ||
| if (taskKey !== event.subagentId && this.tasks.has(event.subagentId)) { | ||
| const agentTask = this.upsertTask(event.subagentId, (prev) => ({ | ||
| taskId: event.subagentId, | ||
| kind: 'subagent', | ||
| state, | ||
| detached: prev?.detached ?? true, | ||
| description: prev?.description, | ||
| agentId: event.subagentId, | ||
| outputTail: prev?.outputTail ?? '', | ||
| startedAt: prev?.startedAt ?? nowIso(), | ||
| endedAt: | ||
| event.type === 'subagent.completed' || event.type === 'subagent.failed' | ||
| ? nowIso() | ||
| : prev?.endedAt, | ||
| resultSummary: event.resultSummary ?? prev?.resultSummary, | ||
| usage: event.usage ?? prev?.usage, | ||
| error: event.error ?? prev?.error, | ||
| stateReason: event.reason ?? prev?.stateReason, | ||
| model: prev?.model, | ||
| thinkingEffort: prev?.thinkingEffort, | ||
| })); | ||
| ops.push({ op: 'task.upsert', task: agentTask }); | ||
| } | ||
| return ops; | ||
| } | ||
|
|
||
| private onGoalUpdated(event: { | ||
|
|
@@ -1269,6 +1331,25 @@ export class AgentTranscriptProjector { | |
| return this.markerOp('notice', { level, message, event: eventPayload }); | ||
| } | ||
|
|
||
| private onPromptAccepted(event: PromptAcceptedEvent): TranscriptOperation[] { | ||
| const prompt = this.upsertPrompt(event.promptId, () => ({ | ||
| promptId: event.promptId, | ||
| status: 'running', | ||
| createdAt: nowIso(), | ||
| })); | ||
| return [{ op: 'prompt.upsert', prompt }]; | ||
| } | ||
|
|
||
| private onPromptQueued(event: PromptQueuedEvent): TranscriptOperation[] { | ||
| const prompt = this.upsertPrompt(event.promptId, () => ({ | ||
| promptId: event.promptId, | ||
| status: 'queued', | ||
| content: event.content, | ||
|
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 a queued prompt contains uploaded image, video, or session-media content, this stores the engine AGENTS.md reference: packages/transcript/AGENTS.md:L23-L23 Useful? React with 👍 / 👎. |
||
| createdAt: nowIso(), | ||
| })); | ||
| return [{ op: 'prompt.upsert', prompt }]; | ||
| } | ||
|
|
||
| private onPromptSubmitted(event: ProjectorPromptSubmittedEvent): TranscriptOperation[] { | ||
| const prompt = this.upsertPrompt(event.promptId, () => ({ | ||
| promptId: event.promptId, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the transcript binding already exists before a normal prompt submission, the first immediately launched prompt is initialized only by
prompt.accepted;prompt.queuedis not emitted, and the terminal handlers merely preserve the previous fields. This initializer omits bothuserMessageIdandcontent, so the same prompt is complete when discovered throughlivePromptBackfillbut remains permanently partial when observed from submission onward. Carry the admitted prompt details in the event or project the submission into the transcript store.AGENTS.md reference: packages/transcript/AGENTS.md:L23-L23
Useful? React with 👍 / 👎.