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( 'loop.lastRequestTraceId', () => undefined as string | undefined, ); export const loopDisposingKey = defineState('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(); private readonly steered = new Map(); private readonly terminalStates = new Map(); 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( @IAgentContextMemoryService private readonly context: IAgentContextMemoryService, @IAgentLLMRequesterService private readonly llmRequester: IAgentLLMRequesterService, @IAgentToolExecutorService private readonly toolExecutor: IAgentToolExecutorService, @IAgentToolRegistryService private readonly toolRegistry: IAgentToolRegistryService, @IConfigService private readonly config: IConfigService, @IEventDispatcher private readonly dispatcher: IEventDispatcher, @IAgentScopeContext private readonly scopeContext: IAgentScopeContext, @ITelemetryService private readonly telemetry: ITelemetryService, @IAgentStateService private readonly states: IAgentStateService, @IWireService private readonly wire: IWireService, @IInstantiationService private readonly instantiation: IInstantiationService, @IAgentProfileService 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(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 { 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(), completion: createControlledPromise(), 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 { 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 { 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 { 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 { 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 { 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 { if ( this.active === undefined && !this.hasPendingRequests() && this.pendingMachineTurn === undefined ) { return Promise.resolve(); } return new Promise((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 { 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(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(); const result = createControlledPromise(); 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 { 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 { 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 { 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 { 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 { 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>; readonly completion: ReturnType>; 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['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; toolCallUuids: Map; resolvedToolIds: Set; 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>; readonly result: ReturnType>; 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; 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, ): 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', );