Download packages/agent-core-v2/src/agent/loop/loopService.ts from SaylorTwift/kimi-code: direct link, hf CLI and curl.
- Browser
- Download file 79.3 kB
-
https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/agent-core-v2/src/agent/loop/loopService.ts
- Command line
-
hf download hf://SaylorTwift/kimi-code/packages/agent-core-v2/src/agent/loop/loopService.ts
-
curl -L -o loopService.ts https://huggingface.co/SaylorTwift/kimi-code/resolve/main/packages/agent-core-v2/src/agent/loop/loopService.ts
79.3 kB
| import { randomUUID } from 'node:crypto'; | |
| import { EventEmitter } from 'node:events'; | |
| import { createControlledPromise } from '@antfu/utils'; | |
| import { Disposable, toDisposable, type IDisposable } from '#/_base/di/lifecycle'; | |
| import { IInstantiationService } from '#/_base/di/instantiation'; | |
| import { LifecycleScope } from '#/app/scopes'; | |
| import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; | |
| import { defineState } from '#/state/state'; | |
| import { abortError, isAbortError, isUserCancellation, userCancellationReason } from '#/_base/utils/abort'; | |
| import { toErrorMessage } from '#/_base/errors/errorMessage'; | |
| import { onUnexpectedError } from '#/_base/errors/unexpectedError'; | |
| import { retryErrorFields } from '#/_base/utils/retry'; | |
| import { IAgentLLMRequesterService } from '#/agent/llmRequester/llmRequester'; | |
| import type { LLMRequestTrace } from '#/llm-adapter/contract/request-trace'; | |
| import type { ModelRequestTiming } from '#/llm-adapter/model/model-requester'; | |
| import { IAgentToolExecutorService } from '#/agent/toolExecutor/toolExecutor'; | |
| import { abortedToolOutput } from '#/agent/toolExecutor/toolExecutorService'; | |
| import type { ToolDidExecuteContext } from '#/agent/toolExecutor/toolHooks'; | |
| import type { ExecutableToolResult } from '#/tool/toolContract'; | |
| import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry'; | |
| import { IConfigService } from '#/app/config/config'; | |
| import { AgentErrorEvent } from '#/agent/mcp/mcpEvents'; | |
| import { type FinishReason } from '#human/llm/finish-reason'; | |
| import { mergeInPlace } from '#/llm-adapter/contract/message'; | |
| import type { ContentPart, UserMessage } from '#human/llm/message'; | |
| import { emptyUsage, type TokenUsage } from '#human/llm/usage'; | |
| import { BugIndicatingError, ErrorCodes, Error2, isError2, toKimiErrorPayload } from '#/errors'; | |
| import { OrderedHookSlot } from '#/hooks'; | |
| import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory'; | |
| import { isVacuousContentPart } from '#/agent/contextMemory/vacuousContent'; | |
| import { newMessageId } from '#/agent/contextMemory/messageId'; | |
| import { type ContextMessage, type PromptOrigin } from '#/agent/contextMemory/types'; | |
| import { gateImageFormatParts } from '#/agent/media/image-compress'; | |
| import { daemonFileRefFromPart } from '#/agent/media/mediaRef'; | |
| import { materializePromptDaemonRefs } from '#/agent/media/promptMediaIntake'; | |
| import { ISessionMediaStore } from '#/agent/media/sessionMediaStore'; | |
| import { IAgentProfileService } from '#/agent/profile/profile'; | |
| import { IAgentScopeContext } from '#/agent/scopeContext/scopeContext'; | |
| import { IAgentStateService } from '#/agent/state/agentState'; | |
| import { IFileService } from '#/app/file/fileService'; | |
| import type { | |
| TurnEndedEvent as TurnEndedTelemetryEvent, | |
| TurnInterruptedEvent, | |
| TurnStartedEvent as TurnStartedTelemetryEvent, | |
| } from '#/app/telemetry/events'; | |
| import { ITelemetryService } from '#/app/telemetry/telemetry'; | |
| import { IEventDispatcher } from '#/state/eventDispatcher'; | |
| import { IWireService } from '#/wire/wire'; | |
| import { | |
| PromptAborted, | |
| PromptCompleted, | |
| PromptQueued, | |
| PromptStarted, | |
| PromptSteered, | |
| PromptSubmitted, | |
| } from '#/agent/prompt/promptEvents'; | |
| import { LOOP_CONTROL_SECTION, type LoopControl } from './configSection'; | |
| import { | |
| createMaxStepsExceededError, | |
| IAgentLoopService, | |
| isMaxStepsExceededError, | |
| type AfterStepContext, | |
| type LoopCancelTarget, | |
| type LoopError, | |
| type LoopErrorContext, | |
| type LoopErrorHandler, | |
| type LoopErrorHandlerRegistrationOptions, | |
| type LoopNotify, | |
| type LoopNotifyHandle, | |
| type LoopRunResult, | |
| type LoopSnapshot, | |
| type LoopSubmitOptions, | |
| type LoopSubmitResult, | |
| type PromptCompletion, | |
| type PromptHandle, | |
| type PromptState, | |
| type PromptSubmitContext, | |
| type Turn, | |
| type TurnResult, | |
| } from './loop'; | |
| import { mergeSteerMessages, stripBundledSkillBlocks } from '#human/agent/origin'; | |
| import { createUserEntry, type UserEntry } from '#human/agent/turn'; | |
| import { | |
| AssistantDelta, | |
| isDisplayablePromptOrigin, | |
| ThinkingDelta, | |
| ToolCallDelta, | |
| turnPromptAttachments, | |
| turnPromptText, | |
| TurnStarted, | |
| TurnStepCompleted, | |
| TurnStepInterrupted, | |
| TurnStepRetrying, | |
| TurnStepStarted, | |
| type TurnInterruptReason, | |
| } from './turnEvents'; | |
| import { TurnCancel, TurnEnded, turnKey, TurnPrompt, TurnSteer } from './turnOps'; | |
| import { | |
| attachMachineEngine, | |
| EMPTY_MACHINE_PROMPT, | |
| ENGINE_JOURNAL_DOMAIN, | |
| engineJournal, | |
| historyFromContext, | |
| MACHINE_LOOP_MODEL, | |
| machineEngineAttachBundle, | |
| wireStoreJournal, | |
| type CreateMachineEngineOptions, | |
| type MachineEngine, | |
| type MachineEngineAttachBundle, | |
| type MachineEngineAttachRef, | |
| type MachineEngineEvent, | |
| type MachineTurnOutcome, | |
| type PromptGateVerdict, | |
| } from './machine'; | |
| export type LoopInterruptReason = 'aborted' | 'max_steps' | 'error'; | |
| export const loopLastRequestTraceIdKey = defineState<string | undefined>( | |
| 'loop.lastRequestTraceId', | |
| () => undefined as string | undefined, | |
| ); | |
| export const loopDisposingKey = defineState<boolean>('loop.disposing', () => false); | |
| const MAX_STEP_SIGNAL_LISTENERS = 64; | |
| export class AgentLoopService extends Disposable implements IAgentLoopService { | |
| declare readonly _serviceBrand: undefined; | |
| readonly hooks: IAgentLoopService['hooks'] = { | |
| onWillBeginStep: new OrderedHookSlot(), | |
| onDidFinishStep: new OrderedHookSlot(), | |
| onBeforeSubmitPrompt: new OrderedHookSlot(), | |
| }; | |
| private readonly errorHandlers: LoopErrorHandler[] = []; | |
| private readonly promptWaiters = new Map<string, PromptWaiter>(); | |
| private readonly steered = new Map<string, SteeredPrompt>(); | |
| private readonly terminalStates = new Map<string, PromptState>(); | |
| private readonly pendingSubmissions: UserEntry[] = []; | |
| private readonly nudges: Nudge[] = []; | |
| private nudgeCursor = 0; | |
| private active: ActiveTurn | undefined; | |
| private pendingMachineTurn: | |
| | { readonly id: number; readonly queueItemId?: string; readonly entry?: UserEntry } | |
| | undefined; | |
| private machineTurnSuppressed = false; | |
| private readonly settleWaiters: Array<() => void> = []; | |
| private quiescenceDepth = 0; | |
| private activeRequestTrace: LLMRequestTrace | undefined; | |
| private engine: MachineEngine | undefined; | |
| constructor( | |
| private readonly context: IAgentContextMemoryService, | |
| private readonly llmRequester: IAgentLLMRequesterService, | |
| private readonly toolExecutor: IAgentToolExecutorService, | |
| private readonly toolRegistry: IAgentToolRegistryService, | |
| private readonly config: IConfigService, | |
| private readonly dispatcher: IEventDispatcher, | |
| private readonly scopeContext: IAgentScopeContext, | |
| private readonly telemetry: ITelemetryService, | |
| private readonly states: IAgentStateService, | |
| private readonly wire: IWireService, | |
| private readonly instantiation: IInstantiationService, | |
| private readonly profile: IAgentProfileService, | |
| ) { | |
| super(); | |
| this.states.contributeState(turnKey); | |
| this.states.contributeState(loopLastRequestTraceIdKey); | |
| this.states.contributeState(loopDisposingKey); | |
| this.toolExecutor.hooks.onDidExecuteTool.register('prompt-service-delivery', async (ctx, next) => { | |
| await this.deliverToolResult(ctx); | |
| await next(); | |
| }); | |
| } | |
| private get lastRequestTraceId(): string | undefined { | |
| return this.states.get(loopLastRequestTraceIdKey); | |
| } | |
| private set lastRequestTraceId(value: string | undefined) { | |
| this.states.set(loopLastRequestTraceIdKey, value); | |
| } | |
| private get disposing(): boolean { | |
| return this.states.get(loopDisposingKey); | |
| } | |
| private set disposing(value: boolean) { | |
| this.states.set(loopDisposingKey, value); | |
| } | |
| private engineOptions(): CreateMachineEngineOptions { | |
| return { | |
| model: MACHINE_LOOP_MODEL, | |
| llmRequester: this.llmRequester, | |
| toolExecutor: this.toolExecutor, | |
| toolInfos: () => this.toolRegistry.list(), | |
| maxAttemptsPerStep: this.config.get<LoopControl>(LOOP_CONTROL_SECTION)?.maxAttemptsPerStep, | |
| initialTurnId: this.states.get(turnKey).nextTurnId, | |
| journal: wireStoreJournal(this.wire, ENGINE_JOURNAL_DOMAIN), | |
| trace: () => this.activeRequestTrace, | |
| toolTurnId: () => this.active?.id, | |
| steerSignal: () => this.active?.steerController.signal, | |
| source: () => | |
| this.active === undefined | |
| ? undefined | |
| : { | |
| type: 'turn', | |
| turnId: this.active.id, | |
| step: this.active.gatedSteps, | |
| }, | |
| gate: (signal) => this.gate(signal), | |
| promptGate: (queueItemId, message) => this.runPromptGate(queueItemId, message), | |
| onTrace: (trace) => { | |
| this.activeRequestTrace = trace; | |
| }, | |
| onEvent: (event) => this.projectMachineEvent(event), | |
| onToolResult: (toolCallId, result) => this.appendMachineToolResult(toolCallId, result), | |
| }; | |
| } | |
| buildAttachBundle(): MachineEngineAttachBundle { | |
| return machineEngineAttachBundle(this.engineOptions()); | |
| } | |
| attachEngine(ref: MachineEngineAttachRef, bundle: MachineEngineAttachBundle): MachineEngine { | |
| if (this.engine !== undefined) { | |
| throw new BugIndicatingError('Machine engine already attached'); | |
| } | |
| this.engine = attachMachineEngine(ref, bundle, this.engineOptions()); | |
| if (this.dispatcher.restorePhase === 'new') { | |
| const hook = this.dispatcher.hooks.onDidRestore.register('loop.engineRefold', async (_ctx, next) => { | |
| hook.dispose(); | |
| try { | |
| if (!this.disposing && this.active === undefined && this.pendingMachineTurn === undefined) { | |
| await this.machineEngine().resetJournal(this.freshEngineJournal()); | |
| } | |
| } catch (error) { | |
| onUnexpectedError(error); | |
| } | |
| await next(); | |
| }); | |
| } | |
| this.rebuildRestoredRecords(); | |
| if (this.quiescenceDepth > 0) { | |
| this.machineEngine().pause(); | |
| } | |
| if (!this.disposing) { | |
| this.drainPendingToMachine(); | |
| this.maybeSettle(); | |
| } | |
| return this.engine; | |
| } | |
| private rebuildRestoredRecords(): void { | |
| if (this.engine === undefined) return; | |
| for (const item of this.engine.snapshot().queue) { | |
| const promptId = item.meta?.promptId; | |
| if (promptId === undefined || this.promptWaiters.has(promptId)) continue; | |
| this.terminalStates.delete(promptId); | |
| this.promptWaiters.set(promptId, this.createWaiter(promptId)); | |
| } | |
| } | |
| private machineEngine(): MachineEngine { | |
| if (this.engine === undefined) { | |
| throw new BugIndicatingError('Machine engine not attached'); | |
| } | |
| return this.engine; | |
| } | |
| override dispose(): void { | |
| if (this.disposing) return; | |
| this.disposing = true; | |
| const reason = abortError('Agent loop disposed'); | |
| for (const waiter of this.promptWaiters.values()) { | |
| this.settleWaiterCancelled(waiter); | |
| this.terminalStates.set(waiter.id, 'cancelled'); | |
| } | |
| this.promptWaiters.clear(); | |
| this.steered.clear(); | |
| this.pendingSubmissions.length = 0; | |
| const active = this.active; | |
| active?.turn.cancel(reason); | |
| this.engine?.stop(); | |
| if (active !== undefined) { | |
| this.interruptMachineRunForCancel(active, reason); | |
| void this.endTurn(active, { type: 'cancelled', steps: active.steps, reason }); | |
| } | |
| this.maybeSettle(); | |
| super.dispose(); | |
| } | |
| submit(input: UserEntry, options?: LoopSubmitOptions): LoopSubmitResult { | |
| if (this.disposing) throw abortError('Agent loop disposed'); | |
| const meta = input.meta; | |
| const id = meta?.promptId ?? newMessageId(); | |
| const origin = (meta?.origin as PromptOrigin | undefined) ?? { kind: 'user' }; | |
| const tracked = meta?.tracked === true; | |
| const createdAt = meta?.createdAt ?? (tracked ? new Date().toISOString() : ''); | |
| const userMessageId = meta?.userMessageId ?? (tracked ? id : ''); | |
| const waiter = this.createWaiter(id, meta?.promptId, options?.onMaterialize); | |
| this.terminalStates.delete(id); | |
| this.promptWaiters.set(id, waiter); | |
| const message: ContextMessage = { | |
| role: 'user', | |
| content: [...input.message.content], | |
| id, | |
| toolCalls: [], | |
| origin: meta?.origin as PromptOrigin | undefined, | |
| }; | |
| if (tracked) { | |
| const queued = | |
| this.active !== undefined || | |
| this.machinePaused() || | |
| (this.engine !== undefined && this.engine.snapshot().queue.length > 0); | |
| this.publishPromptSubmitted( | |
| { promptId: id, origin, userMessageId, createdAt, message }, | |
| queued ? 'queued' : 'running', | |
| ); | |
| if (queued) this.publishPromptQueued({ promptId: id, origin, message }); | |
| } | |
| const entry: UserEntry = { | |
| message: { role: 'user', content: [...input.message.content] }, | |
| meta: { promptId: id, origin, tracked, createdAt, userMessageId }, | |
| }; | |
| if (this.engine !== undefined) { | |
| try { | |
| this.machineEngine().submit(entry); | |
| } catch { | |
| waiter.launched.resolve(undefined); | |
| waiter.completion.resolve({ | |
| promptId: id, | |
| result: undefined, | |
| state: 'failed', | |
| }); | |
| this.publishPromptCompleted(id, 'failed'); | |
| this.terminalStates.set(id, 'failed'); | |
| waiter.failedEntry = entry; | |
| return { id }; | |
| } | |
| } else { | |
| this.pendingSubmissions.push(entry); | |
| } | |
| if ( | |
| options?.steerIfActive === true && | |
| this.active !== undefined && | |
| this.active.prompt.tracked && | |
| this.engine !== undefined | |
| ) { | |
| this.machineEngine().steer(id); | |
| } | |
| return { id }; | |
| } | |
| async steer(promptIds: readonly string[]): Promise<void> { | |
| if (this.disposing) throw abortError('Agent loop disposed'); | |
| if (promptIds.length === 0) { | |
| throw new Error2(ErrorCodes.REQUEST_INVALID, 'prompt_ids must not be empty'); | |
| } | |
| const active = this.active; | |
| if (active === undefined || !active.prompt.tracked) { | |
| throw new Error2(ErrorCodes.PROMPT_NOT_FOUND, 'no active prompt to steer into'); | |
| } | |
| const engine = this.machineEngine(); | |
| const ids = new Set(promptIds); | |
| const queuedIds = new Set(engine.snapshot().queue.map((item) => item.meta?.promptId)); | |
| if (ids.size !== promptIds.length || ![...ids].every((id) => queuedIds.has(id))) { | |
| throw new Error2(ErrorCodes.PROMPT_NOT_FOUND, 'one or more prompts are not pending'); | |
| } | |
| for (const id of ids) { | |
| const entry = engine.snapshot().queue.find((item) => item.meta?.promptId === id); | |
| if (entry !== undefined) await this.materializeDaemonRefs(entry.message); | |
| } | |
| if ( | |
| this.active !== active || | |
| ![...ids].every((id) => new Set(engine.snapshot().queue.map((item) => item.meta?.promptId)).has(id)) | |
| ) { | |
| throw new Error2(ErrorCodes.PROMPT_NOT_FOUND, 'one or more prompts are no longer pending'); | |
| } | |
| engine.steer(promptIds); | |
| } | |
| promptHandle(id: string): PromptHandle | undefined { | |
| const waiter = this.promptWaiters.get(id); | |
| if (waiter === undefined) return undefined; | |
| const projection = this.promptProjection(id); | |
| const state = (): PromptState => this.promptStateOf(id); | |
| const handle: PromptHandle = { | |
| id, | |
| userMessageId: projection?.userMessageId ?? '', | |
| createdAt: projection?.createdAt ?? '', | |
| get state() { | |
| return state(); | |
| }, | |
| message: projection?.message ?? EMPTY_HANDLE_MESSAGE, | |
| launched: waiter.launched, | |
| completion: waiter.completion, | |
| }; | |
| if (this.terminalStates.has(id)) this.promptWaiters.delete(id); | |
| return handle; | |
| } | |
| private promptStateOf(id: string): PromptState { | |
| if (this.active?.prompt.id === id) return 'running'; | |
| if (this.steered.has(id)) return 'steered'; | |
| return this.terminalStates.get(id) ?? 'pending'; | |
| } | |
| private promptProjection(id: string): PromptProjection | undefined { | |
| const failedEntry = this.promptWaiters.get(id)?.failedEntry; | |
| if (failedEntry !== undefined) return projectionFromEntry(failedEntry); | |
| const active = this.active; | |
| if (active !== undefined && active.prompt.id === id) return active.prompt; | |
| const steered = this.steered.get(id); | |
| if (steered !== undefined) return steered; | |
| const pending = this.pendingMachineTurn; | |
| if (pending?.queueItemId === id && pending.entry !== undefined) { | |
| return projectionFromEntry(pending.entry); | |
| } | |
| const queued = this.engine | |
| ?.snapshot() | |
| .queue.find((item) => item.meta?.promptId === id); | |
| if (queued !== undefined) return projectionFromEntry(queued); | |
| const parked = this.pendingSubmissions.find((item) => item.meta?.promptId === id); | |
| if (parked !== undefined) return projectionFromEntry(parked); | |
| return undefined; | |
| } | |
| notify(note: LoopNotify = {}): LoopNotifyHandle { | |
| if (this.disposing) throw abortError('Agent loop disposed'); | |
| const nudge: Nudge = { | |
| contextMessage: note.message, | |
| bypassMaxSteps: note.bypassMaxSteps ?? false, | |
| turnScoped: note.turnScoped ?? true, | |
| onConsume: note.onConsume, | |
| onDrop: note.onDrop, | |
| }; | |
| this.nudges.push(nudge); | |
| if (this.quiescenceDepth === 0 && this.engine !== undefined) { | |
| nudge.sentToMachine = true; | |
| this.machineEngine().notify(createUserEntry(machineUserMessage(note.message))); | |
| } | |
| return { | |
| get dropped() { | |
| return nudge.dropped === true; | |
| }, | |
| drop: () => { | |
| if (nudge.dropped === true || nudge.consumed === true) return; | |
| nudge.dropped = true; | |
| nudge.onDrop?.(); | |
| this.maybeSettle(); | |
| }, | |
| }; | |
| } | |
| private createWaiter( | |
| id: string, | |
| dispatchPromptId?: string, | |
| onMaterialize?: () => void, | |
| ): PromptWaiter { | |
| return { | |
| id, | |
| dispatchPromptId, | |
| launched: createControlledPromise<Turn | undefined>(), | |
| completion: createControlledPromise<PromptCompletion>(), | |
| onMaterialize, | |
| }; | |
| } | |
| private machinePaused(): boolean { | |
| return this.engine?.snapshot().paused ?? false; | |
| } | |
| snapshot(): LoopSnapshot { | |
| const engine = this.engine; | |
| const engineSnapshot = engine?.snapshot(); | |
| const machineQueue = engineSnapshot?.queue ?? []; | |
| const parked = this.pendingSubmissions.filter( | |
| (entry) => !machineQueue.some((item) => item.meta?.promptId === entry.meta?.promptId), | |
| ); | |
| const queue = [...machineQueue, ...parked]; | |
| const turn = engineSnapshot?.turn; | |
| return { | |
| state: this.active === undefined ? 'idle' : 'running', | |
| activeTurnId: this.active?.id, | |
| activePromptId: | |
| this.active !== undefined && this.active.prompt.tracked ? this.active.prompt.id : undefined, | |
| queue, | |
| notificationCount: engineSnapshot?.notificationCount ?? 0, | |
| paused: engineSnapshot?.paused ?? false, | |
| hasPendingRequests: this.hasPendingRequests(), | |
| turn: | |
| turn === undefined | |
| ? undefined | |
| : { | |
| turnId: turn.turnId, | |
| phase: turn.phase, | |
| step: turn.step, | |
| ending: engineSnapshot?.aborting ?? false, | |
| endingReason: engineSnapshot?.aborting === true ? 'aborted' : undefined, | |
| retry: turn.retry, | |
| activeToolCalls: turn.activeToolCalls, | |
| since: this.active?.startedAt, | |
| }, | |
| activeTraceId: this.activeRequestTrace?.traceId, | |
| }; | |
| } | |
| private settlePromptLaunched(waiter: PromptWaiter, active: ActiveTurn): void { | |
| waiter.launched.resolve(active.turn); | |
| void active.turn.result.then((result) => | |
| this.settlePromptCompletion(waiter, active.prompt, result), | |
| ); | |
| if (!active.prompt.tracked) return; | |
| this.publishPromptStarted(active.prompt.id, active.prompt.origin); | |
| } | |
| private settlePromptCompletion( | |
| waiter: PromptWaiter, | |
| prompt: ActivePrompt, | |
| result: TurnResult, | |
| ): void { | |
| const state = | |
| result.type === 'cancelled' ? 'cancelled' : result.type === 'failed' ? 'failed' : 'completed'; | |
| waiter.completion.resolve({ | |
| promptId: waiter.id, | |
| result, | |
| state, | |
| }); | |
| for (const [childId, steeredEntry] of this.steered) { | |
| if (steeredEntry.parentId !== waiter.id) continue; | |
| const child = this.promptWaiters.get(childId); | |
| if (child !== undefined) { | |
| child.completion.resolve({ | |
| promptId: childId, | |
| result, | |
| state, | |
| }); | |
| this.promptWaiters.delete(childId); | |
| } | |
| this.terminalStates.set(childId, state); | |
| this.steered.delete(childId); | |
| } | |
| if (prompt.tracked) { | |
| if (state === 'cancelled') this.publishPromptAborted(waiter.id); | |
| else this.publishPromptCompleted(waiter.id, state); | |
| } | |
| this.terminalStates.set(waiter.id, state); | |
| this.promptWaiters.delete(waiter.id); | |
| } | |
| private async materializeDaemonRefs(message: { | |
| readonly content: readonly ContentPart[]; | |
| }): Promise<void> { | |
| if (!message.content.some((part) => daemonFileRefFromPart(part) !== undefined)) return; | |
| const files = this.instantiation.invokeFunction((accessor) => accessor.get(IFileService)); | |
| const mediaStore = this.instantiation.invokeFunction((accessor) => | |
| accessor.get(ISessionMediaStore), | |
| ); | |
| await materializePromptDaemonRefs(message.content, { files, mediaStore }); | |
| } | |
| private async runPromptGate( | |
| queueItemId: string | undefined, | |
| message: UserMessage, | |
| ): Promise<PromptGateVerdict> { | |
| const waiter = queueItemId === undefined ? undefined : this.promptWaiters.get(queueItemId); | |
| const entry = | |
| queueItemId === undefined | |
| ? undefined | |
| : this.machineEngine().snapshot().queue.find((item) => item.meta?.promptId === queueItemId); | |
| if (waiter === undefined || entry?.meta?.tracked !== true) { | |
| return false; | |
| } | |
| const promptMessage: ContextMessage = { | |
| role: 'user', | |
| content: [...message.content], | |
| toolCalls: [], | |
| id: queueItemId, | |
| origin: entry.meta?.origin as PromptOrigin | undefined, | |
| }; | |
| const ctx: PromptSubmitContext = { | |
| promptMessage, | |
| isSteer: false, | |
| block: false, | |
| }; | |
| await this.hooks.onBeforeSubmitPrompt.run(ctx); | |
| if (ctx.block) return { block: true }; | |
| await this.materializeDaemonRefs(promptMessage); | |
| return { | |
| block: false, | |
| message: { | |
| role: 'user', | |
| content: gateImageFormatParts(promptMessage.content, this.profile.getModelProviderType()), | |
| }, | |
| }; | |
| } | |
| private settleGateRejectedPrompt( | |
| queueItemId: string | undefined, | |
| entry: UserEntry | undefined, | |
| state: 'blocked' | 'failed', | |
| ): void { | |
| const waiter = queueItemId === undefined ? undefined : this.promptWaiters.get(queueItemId); | |
| if (waiter === undefined) return; | |
| if (state === 'blocked' && entry !== undefined && entry.message.content.length > 0) { | |
| this.context.append({ | |
| role: 'user', | |
| content: [...entry.message.content], | |
| id: waiter.id, | |
| toolCalls: [], | |
| origin: entry.meta?.origin as PromptOrigin | undefined, | |
| }); | |
| } | |
| waiter.launched.resolve(undefined); | |
| waiter.completion.resolve({ | |
| promptId: waiter.id, | |
| result: undefined, | |
| state, | |
| }); | |
| this.publishPromptCompleted(waiter.id, state); | |
| this.terminalStates.set(waiter.id, state); | |
| this.promptWaiters.delete(waiter.id); | |
| this.maybeSettle(); | |
| } | |
| private async deliverToolResult(ctx: ToolDidExecuteContext): Promise<void> { | |
| const delivery = ctx.result.delivery; | |
| if (delivery === undefined) return; | |
| const { delivery: _delivery, ...rest } = ctx.result; | |
| ctx.result = rest as ExecutableToolResult; | |
| if (delivery.kind === 'steer') { | |
| const message = delivery.message as ContextMessage; | |
| this.submit( | |
| { message: machineUserMessage(message), meta: { origin: message.origin } }, | |
| { steerIfActive: true }, | |
| ); | |
| } | |
| } | |
| private publishPromptCompleted(promptId: string, reason: 'completed' | 'failed' | 'blocked'): void { | |
| void this.dispatcher.dispatch( | |
| new PromptCompleted({ | |
| agentId: this.scopeContext.agentId, | |
| promptId, | |
| finishedAt: new Date().toISOString(), | |
| reason, | |
| }), | |
| ); | |
| } | |
| private publishPromptQueued(input: { | |
| readonly promptId: string; | |
| readonly origin: PromptOrigin; | |
| readonly message: ContextMessage; | |
| }): void { | |
| if (input.origin.kind !== 'user') return; | |
| void this.dispatcher.dispatch( | |
| new PromptQueued({ | |
| agentId: this.scopeContext.agentId, | |
| promptId: input.promptId, | |
| content: stripBundledSkillBlocks(input.message), | |
| clientMetadata: input.origin.clientMetadata, | |
| queueLength: (this.engine?.snapshot().queue.length ?? 0) + 1, | |
| }), | |
| ); | |
| } | |
| private publishPromptSubmitted( | |
| input: { | |
| readonly promptId: string; | |
| readonly origin: PromptOrigin; | |
| readonly userMessageId: string; | |
| readonly createdAt: string; | |
| readonly message: ContextMessage; | |
| }, | |
| status: 'running' | 'queued', | |
| ): void { | |
| if (input.origin.kind !== 'user') return; | |
| void this.dispatcher.dispatch( | |
| new PromptSubmitted({ | |
| agentId: this.scopeContext.agentId, | |
| promptId: input.promptId, | |
| userMessageId: input.userMessageId, | |
| status, | |
| content: stripBundledSkillBlocks(input.message), | |
| clientMetadata: input.origin.clientMetadata, | |
| createdAt: input.createdAt, | |
| }), | |
| ); | |
| } | |
| private publishPromptStarted(promptId: string, origin: PromptOrigin): void { | |
| if (origin.kind !== 'user') return; | |
| void this.dispatcher.dispatch( | |
| new PromptStarted({ | |
| agentId: this.scopeContext.agentId, | |
| promptId, | |
| }), | |
| ); | |
| } | |
| private publishPromptAborted(promptId: string): void { | |
| void this.dispatcher.dispatch( | |
| new PromptAborted({ | |
| agentId: this.scopeContext.agentId, | |
| promptId, | |
| abortedAt: new Date().toISOString(), | |
| }), | |
| ); | |
| } | |
| cancel(target?: LoopCancelTarget, reason?: unknown): boolean { | |
| const cancellation = reason ?? userCancellationReason(); | |
| if (target?.promptId !== undefined) { | |
| const active = this.active; | |
| if (active !== undefined && active.prompt.tracked && active.prompt.id === target.promptId) { | |
| return this.cancelActiveTurn(undefined, cancellation); | |
| } | |
| const waiter = this.promptWaiters.get(target.promptId); | |
| if (waiter === undefined) { | |
| throw new Error2(ErrorCodes.PROMPT_NOT_FOUND, `prompt ${target.promptId} not found`); | |
| } | |
| return this.cancelWaiter(waiter, cancellation); | |
| } | |
| return this.cancelActiveTurn(target?.turnId, cancellation); | |
| } | |
| private cancelWaiter(waiter: PromptWaiter, cancellation: unknown): boolean { | |
| const active = this.active; | |
| if (active !== undefined && active.prompt.id === waiter.id) { | |
| return this.cancelActiveTurn(undefined, cancellation); | |
| } | |
| const tracked = this.promptProjection(waiter.id)?.tracked === true; | |
| this.engine?.cancelQueueItem(waiter.id); | |
| this.settleWaiterCancelled(waiter); | |
| if (tracked) { | |
| this.publishPromptAborted(waiter.id); | |
| } | |
| this.terminalStates.set(waiter.id, 'cancelled'); | |
| this.promptWaiters.delete(waiter.id); | |
| this.steered.delete(waiter.id); | |
| return true; | |
| } | |
| private settleWaiterCancelled(waiter: PromptWaiter): void { | |
| waiter.launched.resolve(undefined); | |
| waiter.completion.resolve({ | |
| promptId: waiter.id, | |
| result: undefined, | |
| state: 'cancelled', | |
| }); | |
| this.maybeSettle(); | |
| } | |
| tryAcquireQuiescence(): IDisposable | undefined { | |
| if (this.disposing) throw abortError('Agent loop disposed'); | |
| if ( | |
| this.quiescenceDepth > 0 || | |
| this.active !== undefined || | |
| this.hasPendingRequests() || | |
| this.pendingMachineTurn !== undefined | |
| ) { | |
| return undefined; | |
| } | |
| this.quiescenceDepth += 1; | |
| this.engine?.pause(); | |
| return toDisposable(() => this.releaseQuiescence()); | |
| } | |
| private releaseQuiescence(): void { | |
| if (this.quiescenceDepth === 0) return; | |
| this.quiescenceDepth -= 1; | |
| if (this.quiescenceDepth > 0 || this.disposing) return; | |
| this.engine?.resume(); | |
| this.drainPendingToMachine(); | |
| this.maybeSettle(); | |
| } | |
| private drainPendingToMachine(): void { | |
| if (this.engine === undefined) return; | |
| const queued = new Set(this.engine.snapshot().queue.map((item) => item.meta?.promptId)); | |
| for (const entry of this.pendingSubmissions.splice(0)) { | |
| const id = entry.meta?.promptId; | |
| if (id === undefined || queued.has(id) || !this.promptWaiters.has(id)) continue; | |
| this.machineEngine().submit(entry); | |
| } | |
| if (this.quiescenceDepth > 0) return; | |
| for (const nudge of this.nudges.slice(this.nudgeCursor)) { | |
| if (!nudge.dropped && !nudge.sentToMachine) { | |
| nudge.sentToMachine = true; | |
| this.machineEngine().notify(createUserEntry(machineUserMessage(nudge.contextMessage))); | |
| } | |
| } | |
| } | |
| async resetMachineEngine(): Promise<void> { | |
| if (this.disposing) return; | |
| if (this.active !== undefined || this.pendingMachineTurn !== undefined) { | |
| throw new BugIndicatingError('Machine engine reset requires a quiescent loop'); | |
| } | |
| await this.machineEngine().resetJournal(this.freshEngineJournal()); | |
| } | |
| private freshEngineJournal(): ReturnType<typeof engineJournal> { | |
| return engineJournal( | |
| wireStoreJournal(this.wire, ENGINE_JOURNAL_DOMAIN), | |
| this.states.get(turnKey).nextTurnId, | |
| ); | |
| } | |
| private cancelActiveTurn(turnId: number | undefined, cancellation: unknown): boolean { | |
| const active = this.active; | |
| if (active === undefined || (turnId !== undefined && active.id !== turnId)) return false; | |
| if (active.controller.signal.aborted) { | |
| this.machineEngine().abort(active.controller.signal.reason); | |
| return true; | |
| } | |
| void this.dispatcher.dispatch( | |
| new TurnCancel({ | |
| agentId: this.scopeContext.agentId, | |
| turnId: active.id, | |
| target: 'active', | |
| reason: cancelReasonFor(cancellation), | |
| }), | |
| ); | |
| active.controller.abort(cancellation); | |
| this.machineEngine().abort(cancellation); | |
| return true; | |
| } | |
| private settleUnboundRecord( | |
| pending: { readonly id: number; readonly queueItemId?: string; readonly entry?: UserEntry }, | |
| outcome: { readonly outcome: MachineTurnOutcome; readonly error?: unknown }, | |
| ): void { | |
| const active = this.active; | |
| if (active !== undefined) { | |
| active.afterChain = active.afterChain.then(() => { | |
| this.settleUnboundRecord(pending, outcome); | |
| }); | |
| return; | |
| } | |
| if (pending.queueItemId === undefined) { | |
| const seeded = this.nudges.slice(this.nudgeCursor).find( | |
| (nudge) => !nudge.dropped && nudge.contextMessage !== undefined && nudge.contextMessage.content.length > 0, | |
| ); | |
| if (seeded === undefined) { | |
| this.consumeDrainedNudges(); | |
| return; | |
| } | |
| const seededMessage = seeded.contextMessage as ContextMessage; | |
| const waiter = this.createWaiter(seededMessage.id ?? newMessageId(), seededMessage.id); | |
| this.terminalStates.delete(waiter.id); | |
| this.promptWaiters.set(waiter.id, waiter); | |
| const entry: UserEntry = { | |
| message: { role: 'user', content: [...seededMessage.content] }, | |
| meta: { promptId: waiter.id, origin: seededMessage.origin, tracked: false }, | |
| }; | |
| const seededTurn = this.beginActiveTurn(waiter, entry, pending.id); | |
| this.mirrorConsumedNudges(seededTurn); | |
| this.endPreGateTurn(seededTurn, outcome); | |
| return; | |
| } | |
| const waiter = this.promptWaiters.get(pending.queueItemId); | |
| if (waiter === undefined || pending.entry === undefined) return; | |
| const boundTurn = this.beginActiveTurn(waiter, pending.entry, pending.id); | |
| waiter.onMaterialize?.(); | |
| this.materializeMessage(this.gatedProjectionMessage(boundTurn.prompt)); | |
| this.settlePromptLaunched(waiter, boundTurn); | |
| this.endPreGateTurn(boundTurn, outcome); | |
| } | |
| private endPreGateTurn( | |
| turn: ActiveTurn, | |
| outcome: { readonly outcome: MachineTurnOutcome; readonly error?: unknown }, | |
| ): void { | |
| if (outcome.outcome === 'aborted') { | |
| const reason = turn.controller.signal.aborted | |
| ? turn.controller.signal.reason | |
| : abortError('Turn aborted'); | |
| turn.controller.abort(reason); | |
| turn.afterChain = turn.afterChain.then(() => | |
| this.endTurn(turn, { type: 'cancelled', steps: 0, reason }), | |
| ); | |
| return; | |
| } | |
| const error = outcome.error ?? new Error2(ErrorCodes.INTERNAL, 'Turn ended before first step'); | |
| turn.afterChain = turn.afterChain.then(() => | |
| this.endTurn(turn, { type: 'failed', steps: 0, error }), | |
| ); | |
| } | |
| private hasPendingRequests(): boolean { | |
| return ( | |
| this.pendingSubmissions.length > 0 || | |
| (this.engine?.snapshot().queue.length ?? 0) > 0 || | |
| this.nudges.slice(this.nudgeCursor).some((nudge) => !nudge.dropped) | |
| ); | |
| } | |
| settled(): Promise<void> { | |
| if ( | |
| this.active === undefined && | |
| !this.hasPendingRequests() && | |
| this.pendingMachineTurn === undefined | |
| ) { | |
| return Promise.resolve(); | |
| } | |
| return new Promise<void>((resolve) => { | |
| this.settleWaiters.push(resolve); | |
| }); | |
| } | |
| private maybeSettle(): void { | |
| if ( | |
| this.active !== undefined || | |
| this.pendingMachineTurn !== undefined || | |
| this.hasPendingRequests() | |
| ) return; | |
| if (this.settleWaiters.length === 0) return; | |
| const waiters = this.settleWaiters.splice(0); | |
| for (const resolve of waiters) resolve(); | |
| } | |
| registerLoopErrorHandler( | |
| handler: LoopErrorHandler, | |
| options: LoopErrorHandlerRegistrationOptions = {}, | |
| ): IDisposable { | |
| if (options.before !== undefined && options.after !== undefined) { | |
| throw new BugIndicatingError('Loop error handler registration cannot specify both before and after'); | |
| } | |
| this.deleteErrorHandler(handler.id); | |
| const target = options.before ?? options.after; | |
| if (target === undefined) { | |
| this.errorHandlers.push(handler); | |
| } else { | |
| const targetIndex = this.errorHandlers.findIndex((entry) => entry.id === target); | |
| if (targetIndex < 0) { | |
| throw new BugIndicatingError(`Loop error handler target "${target}" is not registered`); | |
| } | |
| const insertAt = options.before !== undefined ? targetIndex : targetIndex + 1; | |
| this.errorHandlers.splice(insertAt, 0, handler); | |
| } | |
| return toDisposable(() => { | |
| this.deleteErrorHandler(handler.id); | |
| }); | |
| } | |
| private deleteErrorHandler(id: string): boolean { | |
| const index = this.errorHandlers.findIndex((entry) => entry.id === id); | |
| if (index < 0) return false; | |
| this.errorHandlers.splice(index, 1); | |
| return true; | |
| } | |
| private async gate(machineSignal: AbortSignal): Promise<MachineGateDecision> { | |
| const active = this.active; | |
| if (active !== undefined) await active.afterChain; | |
| const pending = this.pendingMachineTurn; | |
| if (pending !== undefined) { | |
| this.pendingMachineTurn = undefined; | |
| if (!this.bindMachineTurn(pending)) return { type: 'fail' }; | |
| } | |
| const turn = this.active; | |
| if (turn === undefined) return { type: 'fail' }; | |
| if (turn.controller.signal.aborted || machineSignal.aborted) return { type: 'fail' }; | |
| if (turn.stopRequested) return { type: 'fail' }; | |
| if (turn.failedStep !== undefined) return { type: 'fail' }; | |
| const consumed = this.mirrorConsumedNudges(turn); | |
| if (turn.steerController.signal.aborted) { | |
| turn.steerController = new AbortController(); | |
| } | |
| if (turn.toolStopRequested && consumed.live === 0) return { type: 'fail' }; | |
| const stepOrdinal = Math.max(this.engine?.currentStep() ?? 0, turn.steps + 1); | |
| const maxSteps = this.config.get<LoopControl>(LOOP_CONTROL_SECTION)?.maxStepsPerTurn; | |
| if ( | |
| maxSteps !== undefined && | |
| maxSteps > 0 && | |
| stepOrdinal > maxSteps && | |
| !consumed.bypass | |
| ) { | |
| turn.maxStepsError = createMaxStepsExceededError(maxSteps); | |
| return { type: 'fail' }; | |
| } | |
| turn.steps = stepOrdinal; | |
| turn.gatedSteps = stepOrdinal; | |
| const step: MachineStepState = { | |
| number: stepOrdinal, | |
| uuid: randomUUID(), | |
| signal: turn.controller.signal, | |
| contentAppended: false, | |
| entry: undefined, | |
| usage: undefined, | |
| timing: undefined, | |
| providerFinishReason: undefined, | |
| rawFinishReason: undefined, | |
| messageId: undefined, | |
| pendingToolIds: new Set(), | |
| toolCallUuids: new Map(), | |
| resolvedToolIds: new Set(), | |
| toolStopTurn: false, | |
| }; | |
| turn.current = step; | |
| turn.interruptStep = step.number; | |
| this.activeRequestTrace = undefined; | |
| this.telemetry.setContext({ trace_id: undefined }); | |
| EventEmitter.setMaxListeners(MAX_STEP_SIGNAL_LISTENERS, turn.controller.signal); | |
| try { | |
| await this.hooks.onWillBeginStep.run({ | |
| turnId: turn.id, | |
| step: stepOrdinal, | |
| firstStepOfTurn: stepOrdinal === 1, | |
| signal: step.signal, | |
| }); | |
| } catch (error) { | |
| return this.failMachineGate(turn, step, error); | |
| } | |
| if (step.signal.aborted) { | |
| return this.failMachineGate(turn, step, step.signal.reason ?? abortError('Step aborted')); | |
| } | |
| return { type: 'proceed', signal: step.signal, step: step.number }; | |
| } | |
| private failMachineGate( | |
| turn: ActiveTurn, | |
| step: MachineStepState, | |
| error: unknown, | |
| ): MachineGateDecision { | |
| if (turn.controller.signal.aborted || isAbortError(error) || step.signal.aborted) { | |
| turn.abortReason = turn.controller.signal.aborted ? turn.controller.signal.reason : error; | |
| return { type: 'fail' }; | |
| } | |
| turn.failedStep = { | |
| number: step.number, | |
| uuid: step.uuid, | |
| error, | |
| }; | |
| return { type: 'fail' }; | |
| } | |
| private bindMachineTurn(pending: { | |
| readonly id: number; | |
| readonly queueItemId?: string; | |
| readonly entry?: UserEntry; | |
| }): boolean { | |
| if (this.active !== undefined) { | |
| if (pending.queueItemId !== undefined && pending.entry !== undefined) { | |
| const waiter = this.promptWaiters.get(pending.queueItemId); | |
| if (waiter !== undefined) { | |
| this.machineEngine().submit({ | |
| message: this.gatedEntryMessage(pending.entry), | |
| meta: pending.entry.meta, | |
| }); | |
| } | |
| } | |
| return true; | |
| } | |
| if (pending.queueItemId !== undefined) { | |
| const waiter = this.promptWaiters.get(pending.queueItemId); | |
| if (waiter === undefined || pending.entry === undefined) { | |
| this.machineTurnSuppressed = true; | |
| return false; | |
| } | |
| const boundTurn = this.beginActiveTurn(waiter, pending.entry, pending.id); | |
| waiter.onMaterialize?.(); | |
| this.materializeMessage(this.gatedProjectionMessage(boundTurn.prompt)); | |
| this.settlePromptLaunched(waiter, boundTurn); | |
| return true; | |
| } | |
| const seeded = this.nudges.slice(this.nudgeCursor).find( | |
| (nudge) => !nudge.dropped && nudge.contextMessage !== undefined && nudge.contextMessage.content.length > 0, | |
| ); | |
| if (seeded === undefined) { | |
| this.machineTurnSuppressed = true; | |
| return false; | |
| } | |
| const seededMessage = seeded.contextMessage as ContextMessage; | |
| const waiter = this.createWaiter(seededMessage.id ?? newMessageId(), seededMessage.id); | |
| this.promptWaiters.set(waiter.id, waiter); | |
| const entry: UserEntry = { | |
| message: { role: 'user', content: [...seededMessage.content] }, | |
| meta: { promptId: waiter.id, origin: seededMessage.origin, tracked: false }, | |
| }; | |
| this.beginActiveTurn(waiter, entry, pending.id); | |
| return true; | |
| } | |
| private gatedProjectionMessage(prompt: ActivePrompt): ContextMessage { | |
| if (!prompt.tracked) return prompt.message; | |
| return { | |
| ...prompt.message, | |
| content: gateImageFormatParts(prompt.message.content, this.profile.getModelProviderType()), | |
| }; | |
| } | |
| private gatedEntryMessage(entry: UserEntry): UserMessage { | |
| if (entry.meta?.tracked !== true) return { role: 'user', content: [...entry.message.content] }; | |
| return { | |
| role: 'user', | |
| content: gateImageFormatParts(entry.message.content, this.profile.getModelProviderType()), | |
| }; | |
| } | |
| private beginActiveTurn(waiter: PromptWaiter, entry: UserEntry, id: number): ActiveTurn { | |
| const origin = (entry.meta?.origin as PromptOrigin | undefined) ?? { kind: 'user' }; | |
| const tracked = entry.meta?.tracked === true; | |
| const prompt: ActivePrompt = { | |
| id: waiter.id, | |
| promptId: tracked ? waiter.id : waiter.dispatchPromptId, | |
| tracked, | |
| origin, | |
| message: { | |
| role: 'user', | |
| content: [...entry.message.content], | |
| id: waiter.id, | |
| toolCalls: [], | |
| origin: entry.meta?.origin as PromptOrigin | undefined, | |
| }, | |
| userMessageId: entry.meta?.userMessageId ?? '', | |
| createdAt: entry.meta?.createdAt ?? '', | |
| }; | |
| const controller = new AbortController(); | |
| const ready = createControlledPromise<void>(); | |
| const result = createControlledPromise<TurnResult>(); | |
| void ready.catch(() => undefined); | |
| const turn: MutableTurn = { | |
| id, | |
| state: 'queued', | |
| signal: controller.signal, | |
| ready, | |
| result, | |
| cancel: (reason) => { | |
| if (this.active?.turn === turn) { | |
| return this.cancelActiveTurn(undefined, reason ?? userCancellationReason()); | |
| } | |
| return true; | |
| }, | |
| }; | |
| const active: ActiveTurn = { | |
| id, | |
| prompt, | |
| controller, | |
| steerController: new AbortController(), | |
| turn, | |
| ready, | |
| result, | |
| startedAt: Date.now(), | |
| steps: 0, | |
| gatedSteps: 0, | |
| nudgeCursor: this.nudgeCursor, | |
| current: undefined, | |
| interruptStep: undefined, | |
| failedStep: undefined, | |
| stopRequested: false, | |
| toolStopRequested: false, | |
| forcedStopReason: undefined, | |
| lastStopReason: undefined, | |
| filtered: false, | |
| maxStepsError: undefined, | |
| abortReason: undefined, | |
| retryRequested: false, | |
| afterChain: Promise.resolve(), | |
| partials: [], | |
| forceContentPartBoundary: false, | |
| readyResolved: false, | |
| mode: undefined, | |
| providerType: undefined, | |
| protocol: undefined, | |
| }; | |
| this.active = active; | |
| active.readyResolved = true; | |
| ready.resolve(); | |
| active.mode = this.telemetry.getContext().mode; | |
| const { provider_type, protocol } = this.telemetry.getContext(); | |
| active.providerType = provider_type; | |
| active.protocol = protocol; | |
| this.telemetry.setContext({ turn_id: id }); | |
| const thinkingEffort = this.llmRequester.prepareTurnConfig(id)?.thinkingEffort; | |
| this.telemetry.setContext({ thinking_effort: thinkingEffort }); | |
| void this.dispatcher.dispatch( | |
| new TurnPrompt({ | |
| agentId: this.scopeContext.agentId, | |
| input: prompt.message.content, | |
| origin: prompt.origin, | |
| promptId: prompt.promptId, | |
| turnId: id, | |
| }), | |
| ); | |
| turn.state = 'running'; | |
| void this.dispatcher.dispatch( | |
| new TurnStarted({ | |
| agentId: this.scopeContext.agentId, | |
| turnId: id, | |
| promptId: prompt.promptId, | |
| origin: prompt.origin, | |
| prompt: isDisplayablePromptOrigin(prompt.origin) | |
| ? turnPromptText(prompt.message.content, prompt.origin) | |
| : undefined, | |
| promptAttachments: turnPromptAttachments(prompt.message.content, prompt.origin), | |
| }), | |
| ); | |
| const started: TurnStartedTelemetryEvent = { | |
| turn_id: id, | |
| mode: active.mode ?? 'agent', | |
| provider_type, | |
| protocol, | |
| }; | |
| this.telemetry.track2('turn_started', started); | |
| return active; | |
| } | |
| private materializeMessage(message: ContextMessage): void { | |
| if (message.content.length === 0) return; | |
| this.context.append(message); | |
| } | |
| private consumeDrainedNudges(): { readonly live: number; readonly bypass: boolean } { | |
| const engine = this.engine; | |
| if (engine === undefined) return { live: 0, bypass: false }; | |
| const notificationCount = engine.snapshot().notificationCount; | |
| let consumed = this.nudges.length - this.nudgeCursor - notificationCount; | |
| let live = 0; | |
| let bypass = false; | |
| while (consumed > 0 && this.nudgeCursor < this.nudges.length) { | |
| const nudge = this.nudges[this.nudgeCursor]!; | |
| this.nudgeCursor += 1; | |
| consumed -= 1; | |
| if (nudge.dropped) continue; | |
| live += 1; | |
| bypass = bypass || nudge.bypassMaxSteps; | |
| nudge.consumed = true; | |
| if (nudge.contextMessage !== undefined && nudge.contextMessage.content.length > 0) { | |
| this.materializeMessage(nudge.contextMessage); | |
| } | |
| nudge.onConsume?.(); | |
| } | |
| return { live, bypass }; | |
| } | |
| private mirrorConsumedNudges(turn: ActiveTurn): { readonly live: number; readonly bypass: boolean } { | |
| const consumed = this.consumeDrainedNudges(); | |
| turn.nudgeCursor = this.nudgeCursor; | |
| return consumed; | |
| } | |
| private projectMachineEvent(event: MachineEngineEvent): void { | |
| switch (event.type) { | |
| case 'turnStarted': { | |
| this.pendingMachineTurn = { | |
| id: event.machineTurnId, | |
| queueItemId: event.queueItemId, | |
| entry: event.entry, | |
| }; | |
| this.machineTurnSuppressed = false; | |
| return; | |
| } | |
| case 'promptBlocked': { | |
| this.settleGateRejectedPrompt(event.queueItemId, event.entry, 'blocked'); | |
| return; | |
| } | |
| case 'promptGateFailed': { | |
| this.settleGateRejectedPrompt(event.queueItemId, event.entry, 'failed'); | |
| return; | |
| } | |
| case 'promptSteered': { | |
| const active = this.active; | |
| if (active === undefined) return; | |
| const children: { readonly waiter: PromptWaiter; readonly projection: SteeredPrompt }[] = []; | |
| for (const entry of event.entries) { | |
| const id = entry.meta?.promptId; | |
| if (id === undefined) continue; | |
| const waiter = this.promptWaiters.get(id); | |
| if (waiter === undefined) continue; | |
| const origin = (entry.meta?.origin as PromptOrigin | undefined) ?? { kind: 'user' }; | |
| children.push({ | |
| waiter, | |
| projection: { | |
| parentId: active.prompt.id, | |
| tracked: entry.meta?.tracked === true, | |
| origin, | |
| message: { | |
| role: 'user', | |
| content: [...entry.message.content], | |
| id, | |
| toolCalls: [], | |
| origin: entry.meta?.origin as PromptOrigin | undefined, | |
| }, | |
| userMessageId: entry.meta?.userMessageId ?? '', | |
| createdAt: entry.meta?.createdAt ?? '', | |
| }, | |
| }); | |
| } | |
| if (children.length === 0) return; | |
| for (const { waiter, projection } of children) { | |
| this.steered.set(waiter.id, projection); | |
| waiter.launched.resolve(active.turn); | |
| } | |
| active.steerController.abort(abortError('Steered by new input')); | |
| const merged = | |
| children.length === 1 | |
| ? { | |
| content: children[0]!.projection.message.content, | |
| origin: children[0]!.projection.origin, | |
| } | |
| : mergeSteerMessages( | |
| children.map((child) => ({ | |
| content: child.projection.message.content, | |
| origin: child.projection.origin, | |
| })), | |
| ); | |
| const gatedContent = gateImageFormatParts( | |
| merged.content, | |
| this.profile.getModelProviderType(), | |
| ); | |
| this.nudges.push({ | |
| contextMessage: { | |
| role: 'user', | |
| content: gatedContent, | |
| toolCalls: [], | |
| origin: merged.origin, | |
| id: newMessageId(), | |
| }, | |
| bypassMaxSteps: false, | |
| turnScoped: false, | |
| sentToMachine: true, | |
| }); | |
| void this.dispatcher.dispatch( | |
| new PromptSteered({ | |
| agentId: this.scopeContext.agentId, | |
| activePromptId: active.prompt.id, | |
| promptIds: children.map((child) => child.waiter.id), | |
| content: children.flatMap((child) => | |
| stripBundledSkillBlocks(child.projection.message), | |
| ), | |
| steeredAt: new Date().toISOString(), | |
| }), | |
| ); | |
| void this.dispatcher.dispatch( | |
| new TurnSteer({ | |
| agentId: this.scopeContext.agentId, | |
| input: gatedContent, | |
| origin: merged.origin, | |
| }), | |
| ); | |
| return; | |
| } | |
| case 'turnSettled': { | |
| const outcome = event; | |
| const active = this.active; | |
| if (this.machineTurnSuppressed) { | |
| this.machineTurnSuppressed = false; | |
| this.maybeSettle(); | |
| return; | |
| } | |
| if (this.pendingMachineTurn !== undefined) { | |
| const pending = this.pendingMachineTurn; | |
| this.pendingMachineTurn = undefined; | |
| this.machineTurnSuppressed = false; | |
| this.settleUnboundRecord(pending, outcome); | |
| this.maybeSettle(); | |
| return; | |
| } | |
| if (active === undefined) return; | |
| active.afterChain = active.afterChain.then(() => this.evaluateSettle(active, outcome)); | |
| return; | |
| } | |
| case 'stepStarted': { | |
| const turn = this.active; | |
| const step = turn?.current; | |
| if (turn === undefined || step === undefined) return; | |
| if (!turn.readyResolved) { | |
| turn.readyResolved = true; | |
| turn.ready.resolve(); | |
| } | |
| void this.dispatcher.dispatch( | |
| new TurnStepStarted({ | |
| agentId: this.scopeContext.agentId, | |
| turnId: turn.id, | |
| step: step.number, | |
| stepId: step.uuid, | |
| }), | |
| ); | |
| this.context.appendLoopEvent({ | |
| type: 'step.begin', | |
| uuid: step.uuid, | |
| turnId: String(turn.id), | |
| step: step.number, | |
| }); | |
| turn.partials = []; | |
| turn.forceContentPartBoundary = false; | |
| return; | |
| } | |
| case 'delta': { | |
| const turn = this.active; | |
| if (turn === undefined) return; | |
| const delta = event.delta; | |
| switch (delta.kind) { | |
| case 'assistant': | |
| this.accumulateMachinePart(turn, { type: 'text', text: delta.delta }); | |
| void this.dispatcher.dispatch( | |
| new AssistantDelta({ agentId: this.scopeContext.agentId, turnId: turn.id, delta: delta.delta }), | |
| ); | |
| return; | |
| case 'thinking': { | |
| const part = this.accumulateMachinePart(turn, { | |
| type: 'think', | |
| think: delta.delta, | |
| encrypted: delta.encrypted, | |
| detailsIndex: delta.detailsIndex, | |
| hidden: delta.hidden, | |
| }); | |
| if (part?.type === 'think' && part.hidden === true) return; | |
| void this.dispatcher.dispatch( | |
| new ThinkingDelta({ agentId: this.scopeContext.agentId, turnId: turn.id, delta: delta.delta }), | |
| ); | |
| return; | |
| } | |
| case 'toolCall': | |
| if (delta.started === true) turn.forceContentPartBoundary = true; | |
| void this.dispatcher.dispatch( | |
| new ToolCallDelta({ | |
| agentId: this.scopeContext.agentId, | |
| turnId: turn.id, | |
| toolCallId: delta.toolCallId, | |
| name: delta.name, | |
| argumentsPart: delta.argumentsPart, | |
| }), | |
| ); | |
| return; | |
| } | |
| return; | |
| } | |
| case 'stepCompleted': { | |
| const turn = this.active; | |
| const step = turn?.current; | |
| if (turn === undefined || step === undefined) return; | |
| step.entry = event.entry; | |
| step.usage = event.usage; | |
| step.timing = event.timing; | |
| step.providerFinishReason = event.finish?.finishReason ?? undefined; | |
| step.rawFinishReason = event.finish?.rawFinishReason ?? undefined; | |
| step.messageId = event.messageId; | |
| for (const part of event.entry.message.content) { | |
| this.context.appendLoopEvent({ | |
| type: 'content.part', | |
| uuid: randomUUID(), | |
| turnId: String(turn.id), | |
| step: step.number, | |
| stepUuid: step.uuid, | |
| part, | |
| }); | |
| } | |
| step.contentAppended = true; | |
| this.lastRequestTraceId = this.activeRequestTrace?.traceId; | |
| const toolCalls = event.entry.message.toolCalls; | |
| if (toolCalls.length === 0) { | |
| const finishReason = step.providerFinishReason ?? 'completed'; | |
| this.endOrInterruptMachineStep(turn, step, finishReason === 'tool_calls' ? 'other' : finishReason); | |
| } else { | |
| step.pendingToolIds = new Set(toolCalls.map((call) => call.id)); | |
| } | |
| return; | |
| } | |
| case 'toolStarted': { | |
| const turn = this.active; | |
| const step = turn?.current; | |
| if (turn === undefined || step === undefined) return; | |
| const callUuid = randomUUID(); | |
| step.toolCallUuids.set(event.toolCallId, callUuid); | |
| const extras = step.entry?.message.toolCalls.find((call) => call.id === event.toolCallId)?.extras; | |
| this.context.appendLoopEvent({ | |
| type: 'tool.call', | |
| uuid: callUuid, | |
| turnId: String(turn.id), | |
| step: step.number, | |
| stepUuid: step.uuid, | |
| toolCallId: event.toolCallId, | |
| name: event.name, | |
| args: event.args, | |
| extras, | |
| display: event.display, | |
| }); | |
| return; | |
| } | |
| case 'toolDone': { | |
| const turn = this.active; | |
| const step = turn?.current; | |
| if (turn === undefined || step === undefined) return; | |
| step.pendingToolIds.delete(event.toolCallId); | |
| if (this.isCannedUnknownToolResult(step, event.toolCallId, event.result)) { | |
| turn.afterChain = turn.afterChain.then(async () => { | |
| await this.executeUnknownToolCall(turn, step, event.toolCallId); | |
| if (turn.current === step && step.pendingToolIds.size === 0) { | |
| this.endOrInterruptMachineStep(turn, step, step.toolStopTurn ? 'completed' : 'tool_calls'); | |
| } | |
| }); | |
| return; | |
| } | |
| if (step.pendingToolIds.size === 0) { | |
| this.endOrInterruptMachineStep(turn, step, step.toolStopTurn ? 'completed' : 'tool_calls'); | |
| } | |
| return; | |
| } | |
| case 'toolFailed': { | |
| const turn = this.active; | |
| const step = turn?.current; | |
| if (turn === undefined || step === undefined) return; | |
| const message = event.error instanceof Error ? event.error.message : String(event.error); | |
| this.context.appendLoopEvent({ | |
| type: 'tool.result', | |
| parentUuid: step.toolCallUuids.get(event.toolCallId) ?? randomUUID(), | |
| toolCallId: event.toolCallId, | |
| result: { output: message, isError: true }, | |
| }); | |
| step.resolvedToolIds.add(event.toolCallId); | |
| step.pendingToolIds.delete(event.toolCallId); | |
| if (step.pendingToolIds.size === 0) { | |
| this.endOrInterruptMachineStep(turn, step, step.toolStopTurn ? 'completed' : 'tool_calls'); | |
| } | |
| return; | |
| } | |
| case 'toolBatchFailed': { | |
| const turn = this.active; | |
| const step = turn?.current; | |
| if (turn === undefined || step === undefined) return; | |
| if (step.signal.aborted) return; | |
| this.closeFailedMachineStep(turn, step, 'error'); | |
| turn.failedStep ??= { | |
| number: step.number, | |
| uuid: step.uuid, | |
| error: event.error, | |
| }; | |
| turn.current = undefined; | |
| this.machineEngine().abort(); | |
| return; | |
| } | |
| case 'recovering': { | |
| const turn = this.active; | |
| const step = turn?.current; | |
| if (turn === undefined) return; | |
| if (step !== undefined) { | |
| this.closeFailedMachineStep(turn, step, 'error'); | |
| } | |
| turn.current = undefined; | |
| return; | |
| } | |
| case 'retrying': { | |
| const turn = this.active; | |
| const step = turn?.current; | |
| if (turn === undefined) return; | |
| if (step !== undefined) { | |
| this.closeFailedMachineStep(turn, step, 'error'); | |
| } | |
| const fields = | |
| event.rawError !== undefined | |
| ? retryErrorFields(event.rawError) | |
| : { | |
| errorName: event.errorName, | |
| errorMessage: event.errorMessage, | |
| statusCode: event.statusCode, | |
| }; | |
| void this.dispatcher.dispatch( | |
| new TurnStepRetrying({ | |
| agentId: this.scopeContext.agentId, | |
| turnId: turn.id, | |
| step: step?.number ?? turn.gatedSteps, | |
| stepId: step?.uuid, | |
| failedAttempt: event.failedAttempt, | |
| nextAttempt: event.nextAttempt, | |
| maxAttempts: event.maxAttempts, | |
| delayMs: event.delayMs, | |
| errorName: fields.errorName, | |
| errorMessage: fields.errorMessage, | |
| statusCode: fields.statusCode, | |
| }), | |
| ); | |
| turn.current = undefined; | |
| return; | |
| } | |
| case 'stepFailed': { | |
| const turn = this.active; | |
| const step = turn?.current; | |
| if (turn === undefined || step === undefined) return; | |
| this.closeFailedMachineStep(turn, step, step.signal.aborted ? 'interrupted' : 'error'); | |
| turn.failedStep ??= { | |
| number: step.number, | |
| uuid: step.uuid, | |
| error: event.rawError ?? event.error, | |
| }; | |
| turn.current = undefined; | |
| return; | |
| } | |
| default: | |
| return; | |
| } | |
| } | |
| private isCannedUnknownToolResult( | |
| step: MachineStepState, | |
| toolCallId: string, | |
| result: { readonly content: readonly ContentPart[]; readonly isError?: boolean }, | |
| ): boolean { | |
| if (step.toolCallUuids.has(toolCallId)) return false; | |
| if (result.isError !== true || result.content.length !== 1) return false; | |
| const part = result.content[0]; | |
| const call = step.entry?.message.toolCalls.find((entry) => entry.id === toolCallId); | |
| return ( | |
| part !== undefined && | |
| part.type === 'text' && | |
| call !== undefined && | |
| part.text === `unknown tool: ${call.name}` | |
| ); | |
| } | |
| private async executeUnknownToolCall( | |
| turn: ActiveTurn, | |
| step: MachineStepState, | |
| toolCallId: string, | |
| ): Promise<void> { | |
| const call = step.entry?.message.toolCalls.find((entry) => entry.id === toolCallId); | |
| if (call === undefined) return; | |
| try { | |
| for await (const result of this.toolExecutor.execute([call], { | |
| signal: turn.controller.signal, | |
| turnId: turn.id, | |
| trace: this.activeRequestTrace, | |
| onToolCall: (payload) => { | |
| const callUuid = randomUUID(); | |
| step.toolCallUuids.set(payload.toolCallId, callUuid); | |
| const extras = step.entry?.message.toolCalls.find( | |
| (entry) => entry.id === payload.toolCallId, | |
| )?.extras; | |
| this.context.appendLoopEvent({ | |
| type: 'tool.call', | |
| uuid: callUuid, | |
| turnId: String(turn.id), | |
| step: step.number, | |
| stepUuid: step.uuid, | |
| toolCallId: payload.toolCallId, | |
| name: payload.name, | |
| args: payload.args, | |
| extras, | |
| }); | |
| }, | |
| })) { | |
| if (result.toolCallId === toolCallId) { | |
| this.appendMachineToolResult(toolCallId, result.result); | |
| } | |
| } | |
| } catch (error) { | |
| if (this.active !== turn || turn.current !== step || step.signal.aborted) return; | |
| this.closeFailedMachineStep(turn, step, 'error'); | |
| turn.failedStep ??= { | |
| number: step.number, | |
| uuid: step.uuid, | |
| error, | |
| }; | |
| turn.current = undefined; | |
| this.machineEngine().abort(); | |
| } | |
| } | |
| private accumulateMachinePart(turn: ActiveTurn, part: ContentPart): ContentPart | undefined { | |
| const last = turn.partials.at(-1); | |
| if (part.type === 'think' && last?.type === 'text' && isVacuousContentPart(part)) return undefined; | |
| if (!turn.forceContentPartBoundary && last !== undefined && mergeInPlace(last, part)) return last; | |
| turn.forceContentPartBoundary = false; | |
| turn.partials.push({ ...part }); | |
| return turn.partials.at(-1); | |
| } | |
| private appendMachineToolResult( | |
| toolCallId: string, | |
| result: { | |
| readonly output: string | ContentPart[]; | |
| readonly isError?: boolean; | |
| readonly note?: string; | |
| readonly stopTurn?: boolean; | |
| readonly stopTurnReason?: string; | |
| }, | |
| ): void { | |
| const turn = this.active; | |
| const step = turn?.current; | |
| if (turn === undefined || step === undefined) return; | |
| this.context.appendLoopEvent({ | |
| type: 'tool.result', | |
| parentUuid: step.toolCallUuids.get(toolCallId) ?? randomUUID(), | |
| toolCallId, | |
| result: { output: result.output, isError: result.isError, note: result.note }, | |
| }); | |
| step.resolvedToolIds.add(toolCallId); | |
| if (result.stopTurn === true) { | |
| step.toolStopTurn = true; | |
| turn.toolStopRequested = true; | |
| turn.forcedStopReason ??= result.stopTurnReason; | |
| } | |
| } | |
| private drainMachinePartials(turn: ActiveTurn, step: MachineStepState): void { | |
| const drained = turn.partials.splice(0).filter((entry) => !isVacuousContentPart(entry)); | |
| let lastCompleteThink = -1; | |
| for (const [index, part] of drained.entries()) { | |
| if (part.type === 'think' && part.encrypted !== undefined) { | |
| lastCompleteThink = index; | |
| } | |
| } | |
| for (const part of drained.filter( | |
| (part, index) => part.type !== 'think' || index <= lastCompleteThink, | |
| )) { | |
| this.context.appendLoopEvent({ | |
| type: 'content.part', | |
| uuid: randomUUID(), | |
| turnId: String(turn.id), | |
| step: step.number, | |
| stepUuid: step.uuid, | |
| part, | |
| }); | |
| } | |
| } | |
| private closeFailedMachineStep( | |
| turn: ActiveTurn, | |
| step: MachineStepState, | |
| finishReason: 'error' | 'interrupted', | |
| ): void { | |
| if (!step.contentAppended) this.drainMachinePartials(turn, step); | |
| this.context.appendLoopEvent({ | |
| type: 'step.end', | |
| uuid: step.uuid, | |
| turnId: String(turn.id), | |
| step: step.number, | |
| finishReason, | |
| }); | |
| } | |
| private endOrInterruptMachineStep( | |
| turn: ActiveTurn, | |
| step: MachineStepState, | |
| finishReason: FinishReason, | |
| ): void { | |
| if (turn.controller.signal.aborted) { | |
| this.context.appendLoopEvent({ | |
| type: 'step.end', | |
| uuid: step.uuid, | |
| turnId: String(turn.id), | |
| step: step.number, | |
| finishReason: 'interrupted', | |
| }); | |
| turn.current = undefined; | |
| return; | |
| } | |
| this.endMachineStep(turn, step, finishReason); | |
| } | |
| private endMachineStep(turn: ActiveTurn, step: MachineStepState, finishReason: FinishReason): void { | |
| const normalized = normalizeFinishReason(finishReason); | |
| const usage = step.usage ?? emptyUsage(); | |
| turn.lastStopReason = finishReason; | |
| turn.current = undefined; | |
| const firstStepOfTurn = step.number === 1; | |
| turn.afterChain = turn.afterChain.then(async () => { | |
| this.finishMachineStepProjection(turn, step, normalized, usage); | |
| await this.runMachineAfterStep(turn, step, firstStepOfTurn, usage, finishReason); | |
| }); | |
| } | |
| private finishMachineStepProjection( | |
| turn: ActiveTurn, | |
| step: MachineStepState, | |
| normalized: string, | |
| usage: TokenUsage, | |
| ): void { | |
| this.context.appendLoopEvent({ | |
| type: 'step.end', | |
| uuid: step.uuid, | |
| turnId: String(turn.id), | |
| step: step.number, | |
| finishReason: normalized, | |
| usage, | |
| llmFirstTokenLatencyMs: step.timing?.firstTokenLatencyMs, | |
| llmStreamDurationMs: step.timing?.streamDurationMs, | |
| llmRequestBuildMs: step.timing?.requestBuildMs, | |
| llmServerFirstTokenMs: step.timing?.serverFirstTokenMs, | |
| llmServerDecodeMs: step.timing?.serverDecodeMs, | |
| llmClientConsumeMs: step.timing?.clientConsumeMs, | |
| llmClientBlockedMs: step.timing?.clientBlockedMs, | |
| messageId: step.messageId, | |
| providerFinishReason: step.providerFinishReason, | |
| rawFinishReason: step.rawFinishReason, | |
| }); | |
| void this.dispatcher.dispatch( | |
| new TurnStepCompleted({ | |
| agentId: this.scopeContext.agentId, | |
| turnId: turn.id, | |
| step: step.number, | |
| stepId: step.uuid, | |
| usage, | |
| finishReason: normalized, | |
| llmFirstTokenLatencyMs: step.timing?.firstTokenLatencyMs, | |
| llmStreamDurationMs: step.timing?.streamDurationMs, | |
| llmRequestBuildMs: step.timing?.requestBuildMs, | |
| llmServerFirstTokenMs: step.timing?.serverFirstTokenMs, | |
| llmServerDecodeMs: step.timing?.serverDecodeMs, | |
| llmClientConsumeMs: step.timing?.clientConsumeMs, | |
| llmClientBlockedMs: step.timing?.clientBlockedMs, | |
| providerFinishReason: step.providerFinishReason, | |
| rawFinishReason: step.rawFinishReason, | |
| }), | |
| ); | |
| } | |
| private async runMachineAfterStep( | |
| turn: ActiveTurn, | |
| step: MachineStepState, | |
| firstStepOfTurn: boolean, | |
| usage: TokenUsage, | |
| finishReason: FinishReason, | |
| ): Promise<void> { | |
| const context: AfterStepContext = { | |
| turnId: turn.id, | |
| step: step.number, | |
| firstStepOfTurn, | |
| signal: step.signal, | |
| usage, | |
| finishReason, | |
| stopTurn: false, | |
| }; | |
| try { | |
| await this.hooks.onDidFinishStep.run(context); | |
| } catch (error) { | |
| if (isAbortError(error) || step.signal.aborted) { | |
| turn.abortReason = turn.controller.signal.aborted | |
| ? turn.controller.signal.reason | |
| : error; | |
| return; | |
| } | |
| } | |
| turn.interruptStep = undefined; | |
| if (context.stopTurn) turn.stopRequested = true; | |
| if (finishReason === 'filtered') turn.filtered = true; | |
| } | |
| private async evaluateSettle( | |
| turn: ActiveTurn, | |
| outcome: { readonly outcome: MachineTurnOutcome; readonly error?: unknown }, | |
| ): Promise<void> { | |
| if (this.active !== turn) return; | |
| if ( | |
| turn.failedStep !== undefined && | |
| turn.abortReason === undefined && | |
| !turn.controller.signal.aborted | |
| ) { | |
| await this.recoverOrFailMachineRun(turn); | |
| return; | |
| } | |
| if (turn.abortReason !== undefined || turn.controller.signal.aborted || outcome.outcome === 'aborted') { | |
| const reason = | |
| turn.abortReason ?? | |
| (turn.controller.signal.aborted ? turn.controller.signal.reason : undefined) ?? | |
| abortError('Turn aborted'); | |
| this.interruptMachineRunForCancel(turn, reason); | |
| await this.endTurn(turn, { type: 'cancelled', steps: turn.steps, reason }); | |
| return; | |
| } | |
| if (turn.filtered) { | |
| await this.endTurn(turn, { | |
| type: 'failed', | |
| steps: turn.steps, | |
| error: new Error2(ErrorCodes.PROVIDER_FILTERED, 'Provider safety policy blocked the response.', { | |
| name: 'ProviderFilteredError', | |
| details: { finishReason: 'filtered' }, | |
| }), | |
| }); | |
| return; | |
| } | |
| if (turn.maxStepsError !== undefined) { | |
| await this.endTurn(turn, { type: 'failed', steps: turn.steps, error: turn.maxStepsError }); | |
| return; | |
| } | |
| if (turn.stopRequested) { | |
| await this.endTurn(turn, this.machineCompletedResult(turn)); | |
| return; | |
| } | |
| if (this.hasLiveNudge()) { | |
| return; | |
| } | |
| if (turn.toolStopRequested) { | |
| await this.endTurn(turn, this.machineCompletedResult(turn)); | |
| return; | |
| } | |
| if (outcome.outcome === 'failed') { | |
| const error = outcome.error ?? new Error('Turn failed'); | |
| this.emitStepInterrupted(turn.id, turn.interruptStep, 'error', toErrorMessage(error)); | |
| await this.endTurn(turn, { type: 'failed', steps: turn.steps, error }); | |
| return; | |
| } | |
| await this.endTurn(turn, this.machineCompletedResult(turn)); | |
| } | |
| private hasLiveNudge(): boolean { | |
| return this.nudges.slice(this.nudgeCursor).some((nudge) => !nudge.dropped); | |
| } | |
| private async recoverOrFailMachineRun(turn: ActiveTurn): Promise<void> { | |
| const failure = turn.failedStep!; | |
| turn.failedStep = undefined; | |
| const context: LoopErrorContext = { | |
| turnId: turn.id, | |
| step: failure.number, | |
| stepId: failure.uuid, | |
| signal: turn.controller.signal, | |
| error: failure.error, | |
| retry: () => { | |
| turn.retryRequested = true; | |
| }, | |
| }; | |
| const handler = this.errorHandlers.find((entry) => entry.match(context)); | |
| if (handler !== undefined) { | |
| try { | |
| if (await handler.handle(context)) { | |
| turn.interruptStep = undefined; | |
| if (turn.retryRequested) { | |
| turn.retryRequested = false; | |
| await this.machineEngine().resetHistory(historyFromContext(this.context.get())); | |
| this.machineEngine().notify(createUserEntry(EMPTY_MACHINE_PROMPT)); | |
| } | |
| return; | |
| } | |
| } catch (handlerError) { | |
| if (isAbortError(handlerError) || turn.controller.signal.aborted) { | |
| const reason = turn.controller.signal.aborted ? turn.controller.signal.reason : handlerError; | |
| this.interruptMachineRunForCancel(turn, reason); | |
| await this.endTurn(turn, { type: 'cancelled', steps: turn.steps, reason }); | |
| return; | |
| } | |
| this.emitStepInterrupted(turn.id, failure.number, 'error', toErrorMessage(handlerError)); | |
| await this.endTurn(turn, { type: 'failed', steps: turn.steps, error: handlerError }); | |
| return; | |
| } | |
| } | |
| this.failMachineStep(turn, failure.number, failure.error); | |
| await this.endTurn(turn, { type: 'failed', steps: turn.steps, error: failure.error }); | |
| } | |
| private failMachineStep(turn: ActiveTurn, step: number | undefined, error: unknown): void { | |
| const reason: LoopInterruptReason = isMaxStepsExceededError(error) ? 'max_steps' : 'error'; | |
| const interruptedError = | |
| isError2(error) && error.code === ErrorCodes.INTERNAL && error.cause !== undefined ? error.cause : error; | |
| this.emitStepInterrupted(turn.id, step, reason, toErrorMessage(interruptedError)); | |
| } | |
| private backfillAbortedToolResults(step: MachineStepState, reason: unknown): void { | |
| for (const toolCallId of step.pendingToolIds) { | |
| if (step.resolvedToolIds.has(toolCallId)) continue; | |
| const name = | |
| step.entry?.message.toolCalls.find((call) => call.id === toolCallId)?.name ?? toolCallId; | |
| this.context.appendLoopEvent({ | |
| type: 'tool.result', | |
| parentUuid: step.toolCallUuids.get(toolCallId) ?? randomUUID(), | |
| toolCallId, | |
| result: { output: abortedToolOutput(name, reason), isError: true }, | |
| }); | |
| step.resolvedToolIds.add(toolCallId); | |
| } | |
| } | |
| private interruptMachineRunForCancel(turn: ActiveTurn, reason: unknown): void { | |
| const current = turn.current; | |
| if (current !== undefined) { | |
| this.backfillAbortedToolResults(current, reason); | |
| if (!current.contentAppended) this.drainMachinePartials(turn, current); | |
| this.context.appendLoopEvent({ | |
| type: 'step.end', | |
| uuid: current.uuid, | |
| turnId: String(turn.id), | |
| step: current.number, | |
| finishReason: 'interrupted', | |
| }); | |
| turn.current = undefined; | |
| } | |
| if (turn.interruptStep !== undefined) { | |
| this.emitStepInterrupted( | |
| turn.id, | |
| turn.interruptStep, | |
| 'aborted', | |
| isUserCancellation(reason) ? undefined : toErrorMessage(reason), | |
| ); | |
| turn.interruptStep = undefined; | |
| } | |
| } | |
| private machineCompletedResult(turn: ActiveTurn): LoopRunResult { | |
| const truncated = turn.lastStopReason === 'truncated'; | |
| return { | |
| type: 'completed', | |
| steps: turn.steps, | |
| truncated, | |
| stopReason: turn.forcedStopReason, | |
| }; | |
| } | |
| private async endTurn(turn: ActiveTurn, result: TurnResult): Promise<void> { | |
| if (this.active !== turn) return; | |
| this.active = undefined; | |
| await this.wire.drainPersisted().catch(() => undefined); | |
| for (const nudge of this.nudges.slice(this.nudgeCursor)) { | |
| if (nudge.turnScoped && !nudge.dropped) { | |
| nudge.dropped = true; | |
| nudge.onDrop?.(); | |
| } | |
| } | |
| turn.turn.state = result.type; | |
| if (!turn.readyResolved) { | |
| if (result.type === 'failed') { | |
| turn.ready.reject(result.error); | |
| } else if (result.type === 'cancelled') { | |
| turn.ready.reject( | |
| result.reason instanceof Error ? result.reason : abortError('Turn cancelled'), | |
| ); | |
| } else { | |
| turn.ready.reject(new Error2(ErrorCodes.INTERNAL, 'Turn ended before first step')); | |
| } | |
| } | |
| const durationMs = Date.now() - turn.startedAt; | |
| const traceId = | |
| result.type === 'completed' ? this.lastRequestTraceId : this.activeRequestTrace?.traceId; | |
| const error = result.type === 'failed' ? toKimiErrorPayload(result.error) : undefined; | |
| const interruptReason = result.type === 'completed' ? undefined : interruptReasonFor(result); | |
| void this.dispatcher.dispatch( | |
| new TurnEnded({ | |
| agentId: this.scopeContext.agentId, | |
| turnId: turn.id, | |
| reason: result.type, | |
| error, | |
| durationMs, | |
| interruptReason, | |
| stopReason: result.type === 'completed' ? result.stopReason : undefined, | |
| }), | |
| ); | |
| if (error !== undefined) { | |
| void this.dispatcher.dispatch( | |
| new AgentErrorEvent({ ...error, agentId: this.scopeContext.agentId }), | |
| ); | |
| } | |
| if (interruptReason !== undefined) { | |
| const interrupted: TurnInterruptedEvent = { | |
| turn_id: turn.id, | |
| at_step: result.steps, | |
| mode: turn.mode ?? 'agent', | |
| interrupt_reason: interruptReason, | |
| provider_type: turn.providerType, | |
| protocol: turn.protocol, | |
| trace_id: traceId, | |
| }; | |
| this.telemetry.track2('turn_interrupted', interrupted); | |
| } | |
| const ended: TurnEndedTelemetryEvent = { | |
| turn_id: turn.id, | |
| reason: result.type, | |
| duration_ms: durationMs, | |
| mode: turn.mode ?? 'agent', | |
| error_type: error?.code, | |
| provider_type: turn.providerType, | |
| protocol: turn.protocol, | |
| trace_id: traceId, | |
| }; | |
| this.telemetry.track2('turn_ended', ended); | |
| this.telemetry.setContext({ turn_id: undefined, trace_id: undefined, thinking_effort: undefined }); | |
| this.activeRequestTrace = undefined; | |
| this.lastRequestTraceId = undefined; | |
| turn.result.resolve(result); | |
| this.maybeSettle(); | |
| } | |
| private emitStepInterrupted( | |
| turnId: number, | |
| activeStep: number | undefined, | |
| reason: LoopInterruptReason, | |
| message?: string, | |
| ): void { | |
| if (activeStep === undefined) return; | |
| void this.dispatcher.dispatch( | |
| new TurnStepInterrupted({ | |
| agentId: this.scopeContext.agentId, | |
| turnId, | |
| step: activeStep, | |
| reason, | |
| message, | |
| }), | |
| ); | |
| } | |
| } | |
| type MachineGateDecision = | |
| | { readonly type: 'proceed'; readonly signal: AbortSignal; readonly step: number } | |
| | { readonly type: 'fail' }; | |
| function normalizeFinishReason(reason: FinishReason): string { | |
| if (reason === 'tool_calls') return 'tool_use'; | |
| if (reason === 'completed') return 'end_turn'; | |
| if (reason === 'truncated') return 'max_tokens'; | |
| return reason; | |
| } | |
| function machineUserMessage(message: ContextMessage | undefined): UserMessage { | |
| if (message === undefined) return EMPTY_MACHINE_PROMPT; | |
| return { role: 'user', content: [...message.content] }; | |
| } | |
| type MutableTurn = { | |
| -readonly [K in keyof Turn]: Turn[K]; | |
| }; | |
| interface PromptWaiter { | |
| readonly id: string; | |
| readonly dispatchPromptId?: string; | |
| readonly launched: ReturnType<typeof createControlledPromise<Turn | undefined>>; | |
| readonly completion: ReturnType<typeof createControlledPromise<PromptCompletion>>; | |
| readonly onMaterialize?: () => void; | |
| failedEntry?: UserEntry; | |
| } | |
| interface PromptProjection { | |
| readonly tracked: boolean; | |
| readonly origin: PromptOrigin; | |
| readonly message: ContextMessage; | |
| readonly userMessageId: string; | |
| readonly createdAt: string; | |
| } | |
| interface ActivePrompt extends PromptProjection { | |
| readonly id: string; | |
| readonly promptId?: string; | |
| } | |
| interface SteeredPrompt extends PromptProjection { | |
| readonly parentId: string; | |
| } | |
| const EMPTY_HANDLE_MESSAGE: ContextMessage = { | |
| role: 'user', | |
| content: [], | |
| toolCalls: [], | |
| }; | |
| function projectionFromEntry(entry: UserEntry): PromptProjection { | |
| const origin = (entry.meta?.origin as PromptOrigin | undefined) ?? { kind: 'user' }; | |
| return { | |
| tracked: entry.meta?.tracked === true, | |
| origin, | |
| message: { | |
| role: 'user', | |
| content: [...entry.message.content], | |
| id: entry.meta?.promptId, | |
| toolCalls: [], | |
| origin: entry.meta?.origin as PromptOrigin | undefined, | |
| }, | |
| userMessageId: entry.meta?.userMessageId ?? '', | |
| createdAt: entry.meta?.createdAt ?? '', | |
| }; | |
| } | |
| interface Nudge { | |
| readonly contextMessage?: ContextMessage; | |
| readonly bypassMaxSteps: boolean; | |
| readonly turnScoped: boolean; | |
| readonly onConsume?: () => void; | |
| readonly onDrop?: () => void; | |
| dropped?: boolean; | |
| consumed?: boolean; | |
| sentToMachine?: boolean; | |
| } | |
| type MachineStepEntry = Extract<MachineEngineEvent, { readonly type: 'stepCompleted' }>['entry']; | |
| interface MachineStepState { | |
| readonly number: number; | |
| readonly uuid: string; | |
| readonly signal: AbortSignal; | |
| contentAppended: boolean; | |
| entry: MachineStepEntry | undefined; | |
| usage: TokenUsage | undefined; | |
| timing: ModelRequestTiming | undefined; | |
| providerFinishReason: FinishReason | undefined; | |
| rawFinishReason: string | undefined; | |
| messageId: string | undefined; | |
| pendingToolIds: Set<string>; | |
| toolCallUuids: Map<string, string>; | |
| resolvedToolIds: Set<string>; | |
| toolStopTurn: boolean; | |
| } | |
| interface MachineFailedStep { | |
| readonly number: number; | |
| readonly uuid: string; | |
| readonly error: unknown; | |
| } | |
| interface ActiveTurn { | |
| readonly id: number; | |
| readonly prompt: ActivePrompt; | |
| readonly controller: AbortController; | |
| steerController: AbortController; | |
| readonly turn: MutableTurn; | |
| readonly ready: ReturnType<typeof createControlledPromise<void>>; | |
| readonly result: ReturnType<typeof createControlledPromise<TurnResult>>; | |
| readonly startedAt: number; | |
| steps: number; | |
| gatedSteps: number; | |
| nudgeCursor: number; | |
| current: MachineStepState | undefined; | |
| interruptStep: number | undefined; | |
| failedStep: MachineFailedStep | undefined; | |
| stopRequested: boolean; | |
| toolStopRequested: boolean; | |
| forcedStopReason: string | undefined; | |
| lastStopReason: FinishReason | undefined; | |
| filtered: boolean; | |
| maxStepsError: LoopError | undefined; | |
| abortReason: unknown; | |
| retryRequested: boolean; | |
| afterChain: Promise<void>; | |
| partials: ContentPart[]; | |
| forceContentPartBoundary: boolean; | |
| readyResolved: boolean; | |
| mode: 'agent' | 'plan' | undefined; | |
| providerType: string | undefined; | |
| protocol: string | undefined; | |
| } | |
| function cancelReasonFor(cancellation: unknown): 'user_cancelled' | 'aborted' { | |
| return isUserCancellation(cancellation) ? 'user_cancelled' : 'aborted'; | |
| } | |
| function interruptReasonFor( | |
| result: Extract<TurnResult, { readonly type: 'cancelled' | 'failed' }>, | |
| ): TurnInterruptReason { | |
| if (result.type === 'cancelled') { | |
| return isUserCancellation(result.reason) ? 'user_cancelled' : 'aborted'; | |
| } | |
| if (isMaxStepsExceededError(result.error)) return 'max_steps'; | |
| if (isError2(result.error) && result.error.code === ErrorCodes.PROVIDER_FILTERED) { | |
| return 'filtered'; | |
| } | |
| return 'error'; | |
| } | |
| registerScopedService( | |
| LifecycleScope.Agent, | |
| IAgentLoopService, | |
| AgentLoopService, | |
| ScopeActivation.OnScopeCreated, | |
| 'loop', | |
| ); | |