diff --git a/packages/agent-core-v2/docs/state-manifest.d.ts b/packages/agent-core-v2/docs/state-manifest.d.ts index fed980fbf1..d6ed6bd8bc 100644 --- a/packages/agent-core-v2/docs/state-manifest.d.ts +++ b/packages/agent-core-v2/docs/state-manifest.d.ts @@ -1004,7 +1004,7 @@ export interface AgentStateSnapshot { 'llmRequester.lastConfigLogSignature': string | undefined; 'llmRequester.mediaDegradedTurns': Set; 'llmRequester.mediaStrippedTurns': Map; 'llmRequester.turnConfigs': Map>; + retryable: boolean; + cause?: { + code: (typeof ErrorCodes)[keyof typeof ErrorCodes]; + message: string; + name?: string; + details?: Readonly>; + retryable: boolean; + cause?: { + code: (typeof ErrorCodes)[keyof typeof ErrorCodes]; + message: string; + name?: string; + details?: Readonly>; + retryable: boolean; + cause?: { + code: (typeof ErrorCodes)[keyof typeof ErrorCodes]; + message: string; + name?: string; + details?: Readonly>; + retryable: boolean; + cause?: { + code: (typeof ErrorCodes)[keyof typeof ErrorCodes]; + message: string; + name?: string; + details?: Readonly>; + retryable: boolean; + cause?: { + code: ErrorCode; + message: string; + name?: string; + details?: Readonly>; + retryable: boolean; + cause?: ErrorPayload; + }; + }; + }; + }; + }; + }; + durationMs?: number; +} + /** * model: turn · persisted * owner: src/agent/loop/turnOps.ts @@ -654,6 +710,7 @@ interface WirePayloadMap { "tools.unregister_user_tool": ToolsUnregisterUserToolPayload; "tools.update_store": ToolsUpdateStorePayload; "turn.cancel": TurnCancelPayload; + "turn.ended": TurnEndedPayload; "turn.prompt": TurnPromptPayload; "turn.steer": TurnSteerPayload; "usage.record": UsageRecordPayload; diff --git a/packages/agent-core-v2/scripts/gen-state-manifest.mts b/packages/agent-core-v2/scripts/gen-state-manifest.mts index d24bbfa843..7fdb480292 100644 --- a/packages/agent-core-v2/scripts/gen-state-manifest.mts +++ b/packages/agent-core-v2/scripts/gen-state-manifest.mts @@ -122,6 +122,16 @@ function tsFieldKey(key: string): string { return /^[$A-Z_a-z][$\w]*$/.test(key) ? key : JSON.stringify(key); } +/** + * The checker names a `unique symbol` key `__@@` — + * the numeric id is a compilation-global counter that shifts with unrelated + * edits, so the manifest renders the stable `__@` form instead. + */ +function stableSymbolKey(key: string): string { + const match = /^__@(.+)@\d+$/.exec(key); + return match === null ? key : `__@${match[1]}`; +} + // --------------------------------------------------------------------------- // Static pass — key constants and their register call sites // --------------------------------------------------------------------------- @@ -635,7 +645,7 @@ class TypeRenderer { const propLines = rendered.split('\n'); propLines[propLines.length - 1] += ';'; lines.push( - ` ${readonly}${tsFieldKey(prop.getName())}${optional ? '?' : ''}: ${propLines[0]}`, + ` ${readonly}${tsFieldKey(stableSymbolKey(prop.getName()))}${optional ? '?' : ''}: ${propLines[0]}`, ...propLines.slice(1).map((line) => ` ${line}`), ); } diff --git a/packages/agent-core-v2/src/agent/loop/loopService.ts b/packages/agent-core-v2/src/agent/loop/loopService.ts index ab568cef23..110e1c76f5 100644 --- a/packages/agent-core-v2/src/agent/loop/loopService.ts +++ b/packages/agent-core-v2/src/agent/loop/loopService.ts @@ -84,7 +84,7 @@ import { } from './stepRequest'; import { StepRequestQueue, type StepRequestBatch } from './stepRequestQueue'; import { isDisplayablePromptOrigin, turnPromptText } from './turnEvents'; -import { cancelTurn, promptTurn, TurnModel } from './turnOps'; +import { cancelTurn, endTurn, promptTurn, TurnModel } from './turnOps'; export type LoopInterruptReason = 'aborted' | 'max_steps' | 'error'; @@ -495,12 +495,14 @@ export class AgentLoopService extends Disposable implements IAgentLoopService { : this.activeRequestTrace?.traceId; if (result !== undefined) { const error = result.type === 'failed' ? toKimiErrorPayload(result.error) : undefined; + const durationMs = Date.now() - startedAt; + this.wire.dispatch(endTurn({ turnId: turn.id, reason: result.type, error, durationMs })); this.eventBus.publish({ type: 'turn.ended', turnId: turn.id, reason: result.type, error, - durationMs: Date.now() - startedAt, + durationMs, }); if (error !== undefined) this.eventBus.publish({ type: 'error', ...error }); if (result.type !== 'completed') { diff --git a/packages/agent-core-v2/src/agent/loop/turnOps.ts b/packages/agent-core-v2/src/agent/loop/turnOps.ts index 2a5c0dd236..4082a3421a 100644 --- a/packages/agent-core-v2/src/agent/loop/turnOps.ts +++ b/packages/agent-core-v2/src/agent/loop/turnOps.ts @@ -3,12 +3,16 @@ * identity. * * Owns the next available turn id, including cancelled queued reservations and - * legacy loop-event observations. + * legacy loop-event observations. Also persists the terminal `turn.ended` + * record (reason / error / durationMs) so downstream history rebuilds can + * recover how a turn ended; the record carries no engine-restorable state, so + * its `apply` is a no-op. */ import { z } from 'zod'; import { defineModel } from '#/wire/model'; +import type { KimiErrorPayload } from '#/_base/errors/serialize'; import type { ContentPart } from '#/kosong/contract/message'; import type { PromptOrigin } from '#/agent/contextMemory/types'; @@ -46,6 +50,7 @@ declare module '#/wire/types' { 'turn.prompt': typeof promptTurn; 'turn.steer': typeof steerTurn; 'turn.cancel': typeof cancelTurn; + 'turn.ended': typeof endTurn; } } @@ -70,6 +75,16 @@ export const cancelTurn = TurnModel.defineOp('turn.cancel', { }, }); +export const endTurn = TurnModel.defineOp('turn.ended', { + schema: z.object({ + turnId: z.number(), + reason: z.enum(['completed', 'cancelled', 'failed', 'blocked']), + error: z.custom().optional(), + durationMs: z.number().optional(), + }), + apply: (s) => s, +}); + function advanceTurnClock( state: TurnModelState, nextTurnId: number, diff --git a/packages/agent-core-v2/test/agent/loop/loop.test.ts b/packages/agent-core-v2/test/agent/loop/loop.test.ts index 9d0242ec1d..89bddbe559 100644 --- a/packages/agent-core-v2/test/agent/loop/loop.test.ts +++ b/packages/agent-core-v2/test/agent/loop/loop.test.ts @@ -78,6 +78,7 @@ describe('Agent loop', () => { [wire] context.append_loop_event { "event": { "type": "content.part", "uuid": "", "turnId": "0", "step": 1, "stepUuid": "", "part": { "type": "think", "think": "" } }, "time": "