diff --git a/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts b/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts index 4578316f2..df88503e8 100644 --- a/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts +++ b/packages/agent-core-v2/src/agent/externalHooks/externalHooksService.ts @@ -3,10 +3,11 @@ * hook commands. * * Listens to hook slots owned by the agent behavior/lifecycle domains - * (`toolExecutor`, `permissionGate`, `turn`, `loop`, `fullCompaction`, and + * (`toolExecutor`, `permissionGate`, `prompt`, `turn`, `loop`, `fullCompaction`, and * `task`) and translates those minimal contexts into the configured external - * HookEngine events. Appends Stop hook continuation prompts through - * `contextMemory`. The `SubagentStart` / `SubagentStop` pair is the one + * HookEngine events. Appends UserPromptSubmit hook results and Stop hook + * continuation prompts through `contextMemory`. The `SubagentStart` / + * `SubagentStop` pair is the one * exception: the `agentLifecycle` tool wrapper has no hook service of its own, * so `mirrorAgentRun` invokes `runAgentTaskStart` / `notifyAgentTaskStop` on * this service directly. @@ -18,7 +19,7 @@ import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; import { isUserCancellation } from '#/_base/utils/abort'; import { isPlainRecord } from '#/_base/utils/canonical-args'; import { IAgentTaskService, type AgentTaskNotificationContext } from '#/agent/task'; -import { IAgentContextMemoryService } from '#/agent/contextMemory'; +import { IAgentContextMemoryService, USER_PROMPT_ORIGIN } from '#/agent/contextMemory'; import { IAgentFullCompactionService, type FullCompactionDidCompactContext, @@ -29,6 +30,11 @@ import { IAgentPermissionGate, type PermissionApprovalResultContext, } from '#/agent/permissionGate'; +import { + IAgentPromptService, + type PromptSubmitContext, +} from '#/agent/prompt'; +import { IAgentRecordService } from '#/agent/record'; import type { ExecutableToolResult, ToolDidExecuteContext, @@ -38,8 +44,6 @@ import { IAgentToolExecutorService } from '#/agent/toolExecutor'; import { IAgentTurnService, type TurnEndedContext, - type TurnUserPromptDecision, - type TurnUserPromptSubmitContext, } from '#/agent/turn'; import { IBootstrapService } from '#/app/bootstrap'; import { IConfigService } from '#/app/config'; @@ -84,6 +88,7 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter constructor( private readonly options: ExternalHooksServiceOptions = {}, @IAgentContextMemoryService private readonly context: IAgentContextMemoryService, + @IAgentRecordService private readonly record: IAgentRecordService, @IInstantiationService private readonly instantiation: IInstantiationService, @IConfigService private readonly config: IConfigService, @IBootstrapService private readonly bootstrap: IBootstrapService, @@ -115,6 +120,10 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter this.instantiation.invokeFunction((accessor) => accessor.get(IAgentPermissionGate)), ); + this.registerPromptHooks( + this.instantiation.invokeFunction((accessor) => accessor.get(IAgentPromptService)), + ); + this.registerTurnHooks( this.instantiation.invokeFunction((accessor) => accessor.get(IAgentTurnService)), ); @@ -180,17 +189,19 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter ); } - private registerTurnHooks(turn: IAgentTurnService): void { + private registerPromptHooks(prompt: IAgentPromptService): void { this._register( - turn.hooks.onWillSubmitUserPrompt.register('externalHooks', async (ctx, next) => { - const decision = await this.runUserPromptSubmit(ctx); - if (decision !== undefined) { - ctx.decision = decision; - if (decision.action === 'block') return; + prompt.hooks.onWillSubmitPrompt.register('externalHooks', async (ctx, next) => { + if (await this.runPromptSubmitHook(ctx)) { + ctx.block = true; + return; } await next(); }), ); + } + + private registerTurnHooks(turn: IAgentTurnService): void { this._register( turn.hooks.onEnded.register('externalHooks', async (ctx, next) => { this.notifyTurnEnded(ctx); @@ -293,24 +304,53 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter ); } - private async runUserPromptSubmit( - ctx: TurnUserPromptSubmitContext, - ): Promise { - const signal = ctx.turn.abortController.signal; + private async runPromptSubmitHook( + ctx: PromptSubmitContext, + ): Promise { + if ((ctx.promptMessage.origin ?? USER_PROMPT_ORIGIN).kind !== 'user') return false; + + const signal = new AbortController().signal; const input = ctx.promptMessage.content; signal.throwIfAborted(); const results = await this.engine()?.trigger('UserPromptSubmit', { matcherValue: input, signal, - inputData: { prompt: input }, + inputData: { prompt: input, isSteer: ctx.isSteer }, }); signal.throwIfAborted(); const block = renderUserPromptHookBlockResult(results); - if (block !== undefined) return { action: 'block', ...block }; + if (block !== undefined) { + this.context.splice(this.context.get().length, 0, [{ + role: 'assistant', + content: [{ type: 'text', text: block.text }], + toolCalls: [], + origin: { kind: 'hook_result', event: block.event, blocked: true }, + }]); + this.record.signal({ + type: 'hook.result', + hookEvent: block.event, + content: block.message, + blocked: true, + }); + return true; + } const append = renderUserPromptHookResult(results); - return append === undefined ? undefined : { action: 'append', ...append }; + if (append !== undefined) { + this.context.splice(this.context.get().length, 0, [{ + role: 'user', + content: [{ type: 'text', text: append.text }], + toolCalls: [], + origin: { kind: 'hook_result', event: append.event }, + }]); + this.record.signal({ + type: 'hook.result', + hookEvent: append.event, + content: append.message, + }); + } + return false; } private notifyTurnEnded(ctx: TurnEndedContext): void { diff --git a/packages/agent-core-v2/src/agent/prompt/prompt.ts b/packages/agent-core-v2/src/agent/prompt/prompt.ts index fdd8f711e..83ad01b49 100644 --- a/packages/agent-core-v2/src/agent/prompt/prompt.ts +++ b/packages/agent-core-v2/src/agent/prompt/prompt.ts @@ -1,15 +1,25 @@ import { createDecorator } from "#/_base/di"; import type { ContextMessage } from "#/agent/contextMemory"; import type { Turn } from "#/agent/turn"; +import type { Hooks } from '#/hooks'; +export interface PromptSubmitContext { + readonly promptMessage: ContextMessage; + readonly isSteer: boolean; + block: boolean; +} export interface IAgentPromptService { readonly _serviceBrand: undefined; - prompt(message: ContextMessage): Turn | undefined; - steer(message: ContextMessage): Turn | undefined; + prompt(message: ContextMessage): Promise; + steer(message: ContextMessage): Promise; retry(trigger?: string): Turn | undefined; undo(count: number): number; clear(): void; + + readonly hooks: Hooks<{ + onWillSubmitPrompt: PromptSubmitContext; + }>; } export const IAgentPromptService = createDecorator('agentPromptService'); diff --git a/packages/agent-core-v2/src/agent/prompt/promptService.ts b/packages/agent-core-v2/src/agent/prompt/promptService.ts index b4275221a..008b9d4a0 100644 --- a/packages/agent-core-v2/src/agent/prompt/promptService.ts +++ b/packages/agent-core-v2/src/agent/prompt/promptService.ts @@ -11,13 +11,21 @@ import { import { IAgentLoopService } from '#/agent/loop'; import { IAgentRecordService } from '#/agent/record'; import { IAgentTurnService, type Turn } from '#/agent/turn'; -import { IAgentPromptService } from './prompt'; +import { OrderedHookSlot } from '#/hooks'; +import { + IAgentPromptService, + type PromptSubmitContext, +} from './prompt'; export class AgentPromptService implements IAgentPromptService { declare readonly _serviceBrand: undefined; private readonly steerQueue: ContextMessage[] = []; private observedTurn: Turn | undefined; + readonly hooks = { + onWillSubmitPrompt: new OrderedHookSlot(), + }; + constructor( @IAgentContextMemoryService private readonly context: IAgentContextMemoryService, @IAgentTurnService private readonly turnService: IAgentTurnService, @@ -36,25 +44,27 @@ export class AgentPromptService implements IAgentPromptService { }); } - prompt(message: ContextMessage): Turn | undefined { + async prompt(message: ContextMessage): Promise { if (this.emitBusyIfActive()) return undefined; const stamped = ensureMessageId(message); this.append(stamped); + if (await this.applyPromptHook(stamped, false)) return undefined; const turn = this.turnService.launch(stamped.origin ?? USER_PROMPT_ORIGIN, stamped.id); this.observe(turn); return turn; } - steer(message: ContextMessage): Turn | undefined { + async steer(message: ContextMessage): Promise { + const stamped = ensureMessageId(message); const activeTurn = this.turnService.getActiveTurn(); if (activeTurn !== undefined) { - this.steerQueue.push(ensureMessageId(message)); + this.steerQueue.push(stamped); this.observe(activeTurn); return undefined; } - const stamped = ensureMessageId(message); this.append(stamped); + if (await this.applyPromptHook(stamped, true)) return undefined; const turn = this.turnService.launch(stamped.origin ?? USER_PROMPT_ORIGIN, stamped.id); this.observe(turn); return turn; @@ -118,6 +128,16 @@ export class AgentPromptService implements IAgentPromptService { this.context.splice(this.context.get().length, 0, messages); } + private async applyPromptHook(promptMessage: ContextMessage, isSteer: boolean): Promise { + const hookContext: PromptSubmitContext = { + promptMessage, + isSteer, + block: false, + }; + await this.hooks.onWillSubmitPrompt.run(hookContext); + return hookContext.block; + } + private observe(turn: Turn): void { if (this.observedTurn === turn) return; this.observedTurn = turn; diff --git a/packages/agent-core-v2/src/agent/promptLegacy/promptLegacyService.ts b/packages/agent-core-v2/src/agent/promptLegacy/promptLegacyService.ts index 80835ea2c..a98e5eab9 100644 --- a/packages/agent-core-v2/src/agent/promptLegacy/promptLegacyService.ts +++ b/packages/agent-core-v2/src/agent/promptLegacy/promptLegacyService.ts @@ -21,6 +21,7 @@ import type { PromptAbortResponse, PromptItem, PromptListResponse, + PromptStatus, PromptSteerResult, PromptSubmission, PromptSubmitResult, @@ -69,8 +70,8 @@ export class AgentPromptLegacyService implements IAgentPromptLegacyService { this.queued.push(record); return toItem(record, 'queued'); } - const turn = this.launch(record); - return toItem(record, turn === undefined ? 'queued' : 'running'); + const status = await this.launch(record); + return toItem(record, status); } async steer(promptIds: readonly string[]): Promise { @@ -96,7 +97,7 @@ export class AgentPromptLegacyService implements IAgentPromptLegacyService { selected.reverse(); const content = selected.flatMap((record) => contentToCoreParts(record.body.content)); - this.prompt.steer({ + await this.prompt.steer({ role: 'user', content, toolCalls: [], @@ -138,12 +139,12 @@ export class AgentPromptLegacyService implements IAgentPromptLegacyService { }; } - private launch(record: PromptRecord): Turn | undefined { + private async launch(record: PromptRecord): Promise { const parts = contentToCoreParts(record.body.content); if (parts.length === 0) { throw new KimiError(ErrorCodes.REQUEST_INVALID, 'prompt content has no supported parts'); } - const turn = this.prompt.prompt({ + const turn = await this.prompt.prompt({ id: record.promptId, role: 'user', content: parts, @@ -151,14 +152,17 @@ export class AgentPromptLegacyService implements IAgentPromptLegacyService { origin: { kind: 'user' }, }); if (turn === undefined) { - // Busy with a turn started outside the legacy service (e.g. via /api/v2); - // keep the record queued so it runs once the agent is idle. - this.queued.unshift(record); - return undefined; + if (this.turnService.getActiveTurn() !== undefined) { + // Busy with a turn started outside the legacy service (e.g. via /api/v2); + // keep the record queued so it runs once the agent is idle. + this.queued.unshift(record); + return 'queued'; + } + return 'blocked'; } this.active = { ...record, turn }; void turn.result.then((result) => this.onTurnSettled(record.promptId, result)); - return turn; + return 'running'; } private onTurnSettled(promptId: string, result: TurnResult): void { @@ -173,7 +177,7 @@ export class AgentPromptLegacyService implements IAgentPromptLegacyService { if (this.active !== undefined) return; const next = this.queued.shift(); if (next === undefined) return; - this.launch(next); + void this.launch(next); } private async applyOverrides(body: PromptSubmission): Promise { @@ -189,7 +193,7 @@ export class AgentPromptLegacyService implements IAgentPromptLegacyService { } } -function toItem(record: PromptRecord, status: 'running' | 'queued'): PromptItem { +function toItem(record: PromptRecord, status: PromptStatus): PromptItem { return { prompt_id: record.promptId, user_message_id: record.userMessageId, diff --git a/packages/agent-core-v2/src/agent/rpc/core-api.ts b/packages/agent-core-v2/src/agent/rpc/core-api.ts index 1d5d109ac..4330dbb95 100644 --- a/packages/agent-core-v2/src/agent/rpc/core-api.ts +++ b/packages/agent-core-v2/src/agent/rpc/core-api.ts @@ -369,7 +369,8 @@ export interface RemoveKimiProviderPayload { * Result returned when a prompt/steer submission is accepted. The turn is the * submission's identity and lifecycle (`turn.started` / `turn.ended` carry the * rest over the event stream), so the handle is just the turn id. `undefined` - * means the submission was not accepted (e.g. the agent was busy). + * means no turn was launched (e.g. the agent was busy, or a prompt hook blocked + * before launch). */ export interface PromptLaunchResult { readonly turn_id: number; diff --git a/packages/agent-core-v2/src/agent/rpc/rpcService.ts b/packages/agent-core-v2/src/agent/rpc/rpcService.ts index cd2194ded..7f854f634 100644 --- a/packages/agent-core-v2/src/agent/rpc/rpcService.ts +++ b/packages/agent-core-v2/src/agent/rpc/rpcService.ts @@ -91,8 +91,8 @@ export class AgentRPCService implements IAgentRPCService { @ISessionMetadata private readonly metadata: ISessionMetadata, ) { } - prompt(payload: PromptPayload): PromptLaunchResult | undefined { - const turn = this.promptService.prompt({ + async prompt(payload: PromptPayload): Promise { + const turn = await this.promptService.prompt({ role: 'user', content: [...payload.input], toolCalls: [], @@ -169,9 +169,9 @@ export class AgentRPCService implements IAgentRPCService { this.shellCommandControllers.get(payload.commandId)?.abort(userCancellationReason()); } - steer(payload: SteerPayload): PromptLaunchResult | undefined { + async steer(payload: SteerPayload): Promise { this.telemetry.track('input_steer', { parts: payload.input.length }); - const turn = this.promptService.steer({ + const turn = await this.promptService.steer({ role: 'user', content: [...payload.input], toolCalls: [], @@ -312,7 +312,7 @@ export class AgentRPCService implements IAgentRPCService { commandArgs: origin.commandArgs, trigger: origin.trigger, }); - this.promptService.prompt({ + await this.promptService.prompt({ role: 'user', content: [{ type: 'text', text: expanded }], toolCalls: [], diff --git a/packages/agent-core-v2/src/agent/skill/skillService.ts b/packages/agent-core-v2/src/agent/skill/skillService.ts index c1c7fdb3c..6d5bd0608 100644 --- a/packages/agent-core-v2/src/agent/skill/skillService.ts +++ b/packages/agent-core-v2/src/agent/skill/skillService.ts @@ -71,7 +71,7 @@ export class AgentSkillService extends Disposable implements IAgentSkillService }, ]; - return this.recordActivation( + const turn = await this.recordActivation( { kind: 'skill_activation', activationId: randomUUID(), @@ -83,17 +83,24 @@ export class AgentSkillService extends Disposable implements IAgentSkillService skillArgs: input.args, }, content, - )!; + ); + if (turn === undefined) { + throw new KimiError( + ErrorCodes.TURN_AGENT_BUSY, + 'Cannot activate skill while another turn is active', + ); + } + return turn; } recordModelToolActivation(origin: SkillActivationOrigin): void { - this.recordActivation(origin); + void this.recordActivation(origin); } - private recordActivation( + private async recordActivation( origin: SkillActivationOrigin, input?: readonly ContentPart[], - ): Turn | undefined { + ): Promise { this.records.append({ type: 'skill.activate', origin }); this.publishActivation(origin); diff --git a/packages/agent-core-v2/src/agent/skill/tools/skill.ts b/packages/agent-core-v2/src/agent/skill/tools/skill.ts index 66a0a65f3..86592838e 100644 --- a/packages/agent-core-v2/src/agent/skill/tools/skill.ts +++ b/packages/agent-core-v2/src/agent/skill/tools/skill.ts @@ -173,7 +173,7 @@ export async function executeModelSkill( origin, }; skillService.recordModelToolActivation(origin); - prompt.steer(message); + await prompt.steer(message); return { output: `Skill "${skill.name}" loaded inline. Follow its instructions.`, }; diff --git a/packages/agent-core-v2/src/agent/task/taskService.ts b/packages/agent-core-v2/src/agent/task/taskService.ts index 097f5941f..4b79b0242 100644 --- a/packages/agent-core-v2/src/agent/task/taskService.ts +++ b/packages/agent-core-v2/src/agent/task/taskService.ts @@ -813,7 +813,7 @@ export class AgentTaskService extends Disposable implements IAgentTaskService { private async notifyAgentTask(info: AgentTaskInfo): Promise { const context = await this.buildAgentTaskNotificationContext(info); if (context === undefined) return; - this.prompt.steer({ + await this.prompt.steer({ role: 'user', content: [...context.content], toolCalls: [], diff --git a/packages/agent-core-v2/src/agent/turn/turn.ts b/packages/agent-core-v2/src/agent/turn/turn.ts index 44665c5f6..47362e685 100644 --- a/packages/agent-core-v2/src/agent/turn/turn.ts +++ b/packages/agent-core-v2/src/agent/turn/turn.ts @@ -1,6 +1,6 @@ import { createDecorator } from "#/_base/di"; import type { TurnResult } from '#/agent/loop'; -import type { ContextMessage, PromptOrigin } from '#/agent/contextMemory'; +import type { PromptOrigin } from '#/agent/contextMemory'; import type { Hooks } from '#/hooks'; export type { TurnResult } from '#/agent/loop'; @@ -18,33 +18,6 @@ export interface Turn { readonly result: Promise; } -export interface TurnRunContext { - readonly turn: Turn; - readonly origin: PromptOrigin; - readonly promptMessage?: ContextMessage; - result?: TurnResult; -} - -export type TurnUserPromptDecision = - | { - readonly action: 'append'; - readonly event: string; - readonly message: string; - readonly text: string; - } - | { - readonly action: 'block'; - readonly event: string; - readonly message: string; - readonly text: string; - }; - -export interface TurnUserPromptSubmitContext { - readonly turn: Turn; - readonly promptMessage: ContextMessage; - decision?: TurnUserPromptDecision; -} - export interface TurnEndedContext { readonly turn: Turn; readonly result: TurnResult; @@ -63,7 +36,6 @@ export interface IAgentTurnService { readonly hooks: Hooks<{ onLaunched: { turn: Turn }; - onWillSubmitUserPrompt: TurnUserPromptSubmitContext; onEnded: TurnEndedContext; }>; } diff --git a/packages/agent-core-v2/src/agent/turn/turnService.ts b/packages/agent-core-v2/src/agent/turn/turnService.ts index 037ac30b8..8056b306c 100644 --- a/packages/agent-core-v2/src/agent/turn/turnService.ts +++ b/packages/agent-core-v2/src/agent/turn/turnService.ts @@ -2,9 +2,8 @@ import { createControlledPromise } from '@antfu/utils'; import { InstantiationType } from '#/_base/di/extensions'; import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; -import { toKimiErrorPayload, type KimiErrorPayload } from '#/errors'; -import type { ContextMessage, PromptOrigin } from '#/agent/contextMemory'; -import { IAgentContextMemoryService, USER_PROMPT_ORIGIN } from '#/agent/contextMemory'; +import { toKimiErrorPayload } from '#/errors'; +import type { PromptOrigin } from '#/agent/contextMemory'; import { OrderedHookSlot } from '#/hooks'; import { IAgentLoopService } from '#/agent/loop'; import { IAgentTelemetryContextService, ITelemetryService } from '#/app/telemetry'; @@ -12,7 +11,6 @@ import { IAgentRecordService } from '#/agent/record'; import type { Turn, TurnEndedContext, - TurnUserPromptSubmitContext, TurnResult, } from './turn'; import { IAgentTurnService } from './turn'; @@ -35,14 +33,12 @@ export class AgentTurnService implements IAgentTurnService { readonly hooks = { onLaunched: new OrderedHookSlot<{ turn: Turn }>(), - onWillSubmitUserPrompt: new OrderedHookSlot(), onEnded: new OrderedHookSlot(), }; constructor( @IAgentLoopService private readonly loop: IAgentLoopService, @IAgentRecordService private readonly record: IAgentRecordService, - @IAgentContextMemoryService private readonly context: IAgentContextMemoryService, @ITelemetryService private readonly telemetry: ITelemetryService, @IAgentTelemetryContextService private readonly telemetryContext: IAgentTelemetryContextService, ) { @@ -105,11 +101,6 @@ export class AgentTurnService implements IAgentTurnService { origin, promptMessageId: turn.promptMessageId, }); - const promptHookResult = await this.applyUserPromptHook(turn, origin); - if (promptHookResult !== undefined) { - result = promptHookResult; - return result; - } result = await this.loop.runTurn(turn.id, { signal: turn.abortController.signal, onStepStarted: () => ready.resolve(), @@ -123,19 +114,25 @@ export class AgentTurnService implements IAgentTurnService { result = { reason: 'failed', error }; return result; } finally { - ready.reject(createTurnReadyError(result)); + ready.reject(new Error('Turn ended before first step', { cause: result?.error })); if (this.activeTurn === turn) { this.activeTurn = undefined; } if (result !== undefined) { this.lastEndedReasonValue = result.reason; - const ended = toTurnEndedEvent(turn, result, Date.now() - startedAt); - this.record.signal(ended); - if (ended.error !== undefined) { - this.record.signal({ type: 'error', ...ended.error }); + const error = result.error !== undefined ? toKimiErrorPayload(result.error) : undefined; + this.record.signal({ + type: 'turn.ended', + turnId: turn.id, + reason: result.reason, + error, + durationMs: Date.now() - startedAt, + }); + if (error !== undefined) { + this.record.signal({ type: 'error', ...error }); } - if (ended.reason !== 'completed') { - turnTelemetry.track('turn_interrupted', { at_step: result.steps ?? 0 }); + if (result.reason !== 'completed') { + turnTelemetry.track('turn_interrupted', { at_step: result.steps ?? null }); } } if (result !== undefined) { @@ -149,117 +146,12 @@ export class AgentTurnService implements IAgentTurnService { this.nextTurnId = turnId + 1; } } - - private async applyUserPromptHook( - turn: Turn, - origin: PromptOrigin, - ): Promise { - if (origin.kind !== 'user') return undefined; - const promptMessage = this.context.get().at(-1); - if (!shouldRunUserPromptHook(promptMessage)) return undefined; - - const hookContext: TurnUserPromptSubmitContext = { turn, promptMessage }; - await this.hooks.onWillSubmitUserPrompt.run(hookContext); - const hookResult = hookContext.decision; - if (hookResult?.action === 'block') { - this.append({ - role: 'assistant', - content: [{ type: 'text', text: hookResult.text }], - toolCalls: [], - origin: { kind: 'hook_result', event: hookResult.event, blocked: true }, - }); - this.record.signal({ - type: 'hook.result', - turnId: turn.id, - hookEvent: hookResult.event, - content: hookResult.message, - blocked: true, - }); - return { reason: 'blocked' }; - } - - if (hookResult?.action === 'append') { - this.append({ - role: 'user', - content: [{ type: 'text', text: hookResult.text }], - toolCalls: [], - origin: { kind: 'hook_result', event: hookResult.event }, - }); - this.record.signal({ - type: 'hook.result', - turnId: turn.id, - hookEvent: hookResult.event, - content: hookResult.message, - }); - } - return undefined; - } - - private append(...messages: ContextMessage[]): void { - if (messages.length === 0) return; - this.context.splice(this.context.get().length, 0, messages); - } - -} - -function shouldRunUserPromptHook(message: ContextMessage | undefined): message is ContextMessage { - if (message === undefined || message.role !== 'user') return false; - return (message.origin ?? USER_PROMPT_ORIGIN).kind === 'user'; -} - -function toTurnEndedEvent( - turn: Turn, - result: TurnResult, - durationMs: number, -): { - type: 'turn.ended'; - turnId: number; - reason: TurnResult['reason']; - error?: KimiErrorPayload; - durationMs: number; -} { - if (result.reason !== 'failed' || result.error === undefined) { - return { type: 'turn.ended', turnId: turn.id, reason: result.reason, durationMs }; - } - return { - type: 'turn.ended', - turnId: turn.id, - reason: result.reason, - error: summarizeTurnError(result.error, turn.id), - durationMs, - }; -} - -const LLM_NOT_SET_MESSAGE = 'LLM not set, send "/login" to login'; - -function summarizeTurnError(error: unknown, turnId: number): KimiErrorPayload { - const payload = toKimiErrorPayload(error); - const details = { ...payload.details, turnId }; - // Substitute a friendlier, login-aware message for model-not-configured. The - // raw "Model not set" / "Provider not set" text is not actionable. - if (payload.code === 'model.not_configured') { - return { ...payload, message: LLM_NOT_SET_MESSAGE, details }; - } - return { ...payload, details }; } type MutableTurn = { -readonly [K in keyof Turn]: Turn[K]; }; -function createTurnReadyError(result: TurnResult | undefined): Error { - const cause = turnReadyFailureCause(result); - return cause === undefined - ? new Error('Turn ended before first step') - : new Error('Turn ended before first step', { cause }); -} - -function turnReadyFailureCause(result: TurnResult | undefined): unknown { - if (result === undefined) return undefined; - if ('error' in result && result.error !== undefined) return result.error; - return result.reason; -} - registerScopedService( LifecycleScope.Agent, IAgentTurnService, diff --git a/packages/agent-core-v2/src/app/gateway/gatewayService.ts b/packages/agent-core-v2/src/app/gateway/gatewayService.ts index e5ed77ddc..e24e782c8 100644 --- a/packages/agent-core-v2/src/app/gateway/gatewayService.ts +++ b/packages/agent-core-v2/src/app/gateway/gatewayService.ts @@ -38,33 +38,33 @@ export class RestGateway implements IRestGateway { return agent; } - prompt( + async prompt( sessionId: string, agentId: string, input: string, ): Promise<{ readonly turn_id: number } | undefined> { - const turn = this.agent(sessionId, agentId).accessor.get(IAgentPromptService).prompt({ + const turn = await this.agent(sessionId, agentId).accessor.get(IAgentPromptService).prompt({ role: 'user', content: [{ type: 'text', text: input }], toolCalls: [], origin: { kind: 'user' }, }); - return Promise.resolve(turn === undefined ? undefined : { turn_id: turn.id }); + return turn === undefined ? undefined : { turn_id: turn.id }; } - steer( + async steer( sessionId: string, agentId: string, content: string, ): Promise<{ readonly turn_id: number } | undefined> { const agent = this.agent(sessionId, agentId); - const turn = agent.accessor.get(IAgentPromptService).steer({ + const turn = await agent.accessor.get(IAgentPromptService).steer({ role: 'user', content: [{ type: 'text', text: content }], toolCalls: [], origin: { kind: 'user' }, }); const id = turn?.id ?? agent.accessor.get(IAgentTurnService).getActiveTurn()?.id; - return Promise.resolve(id === undefined ? undefined : { turn_id: id }); + return id === undefined ? undefined : { turn_id: id }; } cancel(sessionId: string, agentId: string, reason?: string): Promise { const activeTurn = this.agent(sessionId, agentId).accessor.get(IAgentTurnService).getActiveTurn(); diff --git a/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycle.ts b/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycle.ts index 87eca1b3e..61c313261 100644 --- a/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycle.ts +++ b/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycle.ts @@ -119,7 +119,7 @@ export interface IAgentLifecycleService { * surface this run (the `Agent` tool, the swarm) mirrors it itself. Throws * when the agent does not exist or a turn cannot be started (busy / no head). */ - run(agentId: string, request: AgentRunRequest, opts: RunAgentOptions): AgentRunHandle; + run(agentId: string, request: AgentRunRequest, opts: RunAgentOptions): Promise; getHandle(agentId: string): IAgentScopeHandle | undefined; list(filter?: AgentListFilter): readonly IAgentScopeHandle[]; remove(agentId: string): Promise; diff --git a/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts b/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts index abedb7f15..5e37dd13d 100644 --- a/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts +++ b/packages/agent-core-v2/src/session/agentLifecycle/agentLifecycleService.ts @@ -200,7 +200,7 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle return child; } - run(agentId: string, request: AgentRunRequest, opts: RunAgentOptions): AgentRunHandle { + run(agentId: string, request: AgentRunRequest, opts: RunAgentOptions): Promise { const handle = this.handles.get(agentId); if (handle === undefined) throw new Error(`Agent "${agentId}" does not exist`); return runAgentTurn(handle, request, { diff --git a/packages/agent-core-v2/src/session/agentLifecycle/runAgentTurn.ts b/packages/agent-core-v2/src/session/agentLifecycle/runAgentTurn.ts index 981c355b9..54124d9c5 100644 --- a/packages/agent-core-v2/src/session/agentLifecycle/runAgentTurn.ts +++ b/packages/agent-core-v2/src/session/agentLifecycle/runAgentTurn.ts @@ -58,20 +58,20 @@ export interface RunAgentTurnOptions { } /** - * Submit a prompt (or a retry) to `target` and return the running `Turn` plus - * a promise of the distilled summary/usage. Throws when the underlying + * Submit a prompt (or a retry) to `target` and resolve to the running `Turn` + * plus a promise of the distilled summary/usage. Throws when the underlying * `IAgentPromptService.prompt/retry` refuses to launch a turn (busy / no head). */ -export function runAgentTurn( +export async function runAgentTurn( target: IAgentScopeHandle, request: AgentRunRequest, options: RunAgentTurnOptions, -): AgentRunHandle { +): Promise { options.signal.throwIfAborted(); const promptService = target.accessor.get(IAgentPromptService); const turn = request.kind === 'prompt' - ? promptService.prompt({ + ? await promptService.prompt({ role: 'user', content: [{ type: 'text', text: request.prompt }], toolCalls: [], @@ -140,7 +140,7 @@ async function distillSummary( const promptService = target.accessor.get(IAgentPromptService); for (let attempt = 0; attempt < policy.retries; attempt++) { - const turn = promptService.prompt({ + const turn = await promptService.prompt({ role: 'user', content: [{ type: 'text', text: policy.continuationPrompt }], toolCalls: [], diff --git a/packages/agent-core-v2/src/session/agentLifecycle/tools/agent.ts b/packages/agent-core-v2/src/session/agentLifecycle/tools/agent.ts index 17bb2fb6b..ee91c013b 100644 --- a/packages/agent-core-v2/src/session/agentLifecycle/tools/agent.ts +++ b/packages/agent-core-v2/src/session/agentLifecycle/tools/agent.ts @@ -270,7 +270,7 @@ export class AgentTool implements BuiltinTool { runInBackground, }); - const run = this.lifecycle.run( + const run = await this.lifecycle.run( agentId, { kind: 'prompt', prompt: promptText }, { signal: controller.signal }, diff --git a/packages/agent-core-v2/src/session/cron/sessionCronServiceImpl.ts b/packages/agent-core-v2/src/session/cron/sessionCronServiceImpl.ts index 1ab865678..44f3e50e1 100644 --- a/packages/agent-core-v2/src/session/cron/sessionCronServiceImpl.ts +++ b/packages/agent-core-v2/src/session/cron/sessionCronServiceImpl.ts @@ -392,9 +392,9 @@ export class SessionCronServiceImpl extends Disposable implements ISessionCronSe toolCalls: [], origin, }; - const turn = promptService.steer(message); + void promptService.steer(message); this.telemetry.track(CRON_MISSED, { count: tasks.length }); - return turn; + return undefined; } emitScheduled(task: CronTask): void { @@ -448,14 +448,15 @@ export class SessionCronServiceImpl extends Disposable implements ISessionCronSe origin, }; this.signalRecord({ type: 'cron.fired', origin, prompt: task.prompt }); - const turn = promptService.steer(message); + const buffered = mainHandle.accessor.get(IAgentTurnService).getActiveTurn() !== undefined; + void promptService.steer(message); this.telemetry.track(CRON_FIRED, { recurring: task.recurring !== false, coalesced_count: ctx.coalescedCount, stale: origin.stale, - buffered: turn === undefined, + buffered, }); - return turn; + return undefined; } private advanceCursor(id: string, lastFiredAt: number): void { diff --git a/packages/agent-core-v2/src/session/swarm/sessionSwarmService.ts b/packages/agent-core-v2/src/session/swarm/sessionSwarmService.ts index d904744bf..b975368f9 100644 --- a/packages/agent-core-v2/src/session/swarm/sessionSwarmService.ts +++ b/packages/agent-core-v2/src/session/swarm/sessionSwarmService.ts @@ -172,14 +172,14 @@ export class SessionSwarmService implements ISessionSwarmService { return this.observe(caller, child.id, profileName, request, options); } - private observe( + private async observe( caller: IAgentScopeHandle, agentId: string, profileName: string, request: { kind: 'prompt'; prompt: string } | { kind: 'retry' }, options: AgentRunAttemptOptions, - ): AgentRunAttemptHandle { - const run = this.lifecycle.run(agentId, request, { + ): Promise { + const run = await this.lifecycle.run(agentId, request, { signal: options.signal, onReady: options.onReady, }); diff --git a/packages/agent-core-v2/test/cron/cron.e2e.test.ts b/packages/agent-core-v2/test/cron/cron.e2e.test.ts index 28833d3f2..972ecfe53 100644 --- a/packages/agent-core-v2/test/cron/cron.e2e.test.ts +++ b/packages/agent-core-v2/test/cron/cron.e2e.test.ts @@ -82,12 +82,12 @@ describe('Cron — session E2E (P1.9)', () => { }> = []; vi.spyOn(prompt, 'steer').mockImplementation((message: ContextMessage) => { steerCalls.push({ content: message.content, origin: message.origin }); - return { + return Promise.resolve({ id: 1, abortController: new AbortController(), ready: Promise.resolve(), result: Promise.resolve({ reason: 'completed' as const }), - }; + }); }); // Schedule via the full tool surface — the scheduling path goes diff --git a/packages/agent-core-v2/test/cron/cron.test.ts b/packages/agent-core-v2/test/cron/cron.test.ts index 06d3c5e8b..2d9f99861 100644 --- a/packages/agent-core-v2/test/cron/cron.test.ts +++ b/packages/agent-core-v2/test/cron/cron.test.ts @@ -75,10 +75,10 @@ describe('SessionCronService', () => { }; ix.stub(IAgentPromptService, { - prompt: () => undefined, + prompt: () => Promise.resolve(undefined), steer: (message) => { steered.push(message); - return fakeTurn(); + return Promise.resolve(fakeTurn()); }, retry: () => undefined, undo: () => 0, diff --git a/packages/agent-core-v2/test/cron/manager.test.ts b/packages/agent-core-v2/test/cron/manager.test.ts index 65b46f210..4a42293b8 100644 --- a/packages/agent-core-v2/test/cron/manager.test.ts +++ b/packages/agent-core-v2/test/cron/manager.test.ts @@ -61,7 +61,7 @@ function createSteerSpy( const calls: Array<{ content: readonly ContentPart[]; origin: PromptOrigin }> = []; vi.spyOn(prompt, 'steer').mockImplementation((message: ContextMessage) => { calls.push({ content: message.content, origin: message.origin as PromptOrigin }); - return returnValue; + return Promise.resolve(returnValue); }); return calls; } @@ -610,7 +610,7 @@ describe('SessionCronService', () => { vi.spyOn(prompt, 'steer').mockImplementation((message: ContextMessage) => { if (shouldThrow) throw new Error('steer boom'); delivered.push(message); - return undefined; + return Promise.resolve(undefined); }); cron.addTask({ cron: '*/5 * * * *', prompt: 'retry me' }); harness.advance(6 * 60_000); diff --git a/packages/agent-core-v2/test/cron/manual-tick.test.ts b/packages/agent-core-v2/test/cron/manual-tick.test.ts index 5b74102a3..aec2924f5 100644 --- a/packages/agent-core-v2/test/cron/manual-tick.test.ts +++ b/packages/agent-core-v2/test/cron/manual-tick.test.ts @@ -31,7 +31,7 @@ function createClocks(initial: number = WALL_ANCHOR): ClockHarness { } function spySteer(prompt: IAgentPromptService) { - return vi.spyOn(prompt, 'steer').mockImplementation((_message: ContextMessage) => ({ + return vi.spyOn(prompt, 'steer').mockImplementation((_message: ContextMessage) => Promise.resolve({ id: 1, abortController: new AbortController(), ready: Promise.resolve(), diff --git a/packages/agent-core-v2/test/cron/resume.test.ts b/packages/agent-core-v2/test/cron/resume.test.ts index c6c9d9f9f..d66227cb4 100644 --- a/packages/agent-core-v2/test/cron/resume.test.ts +++ b/packages/agent-core-v2/test/cron/resume.test.ts @@ -64,7 +64,7 @@ function captureSteer(prompt: IAgentPromptService): SteerCall[] { const calls: SteerCall[] = []; prompt.steer = (message: ContextMessage) => { calls.push({ content: message.content, origin: message.origin as PromptOrigin }); - return undefined; + return Promise.resolve(undefined); }; return calls; } diff --git a/packages/agent-core-v2/test/externalHooks/integration.test.ts b/packages/agent-core-v2/test/externalHooks/integration.test.ts index fdde0217b..044d657f1 100644 --- a/packages/agent-core-v2/test/externalHooks/integration.test.ts +++ b/packages/agent-core-v2/test/externalHooks/integration.test.ts @@ -23,6 +23,8 @@ import { import { IAgentFullCompactionService } from '#/agent/fullCompaction'; import { IAgentLoopService, type TurnAfterStepContext } from '#/agent/loop'; import { IAgentPermissionGate } from '#/agent/permissionGate'; +import { IAgentPromptService } from '#/agent/prompt'; +import { IAgentRecordService } from '#/agent/record'; import { IAgentToolExecutorService } from '#/agent/toolExecutor'; import { IAgentTurnService, type Turn } from '#/agent/turn'; import { IBootstrapService } from '#/app/bootstrap'; @@ -31,6 +33,7 @@ import { IPluginService } from '#/app/plugin'; import { createHooks } from '#/hooks'; import { stubBootstrap } from '../bootstrap/stubs'; +import { stubRecord } from '../contextMemory/stubs'; import { stubLoopWithHooks, stubToolExecutor, stubTurnWithHooks } from '../turn/stubs'; function nodeCommand(source: string): string { @@ -156,7 +159,11 @@ describe('HookEngine integration', () => { reg.definePartialInstance(IConfigService, {}); reg.definePartialInstance(IPluginService, {}); reg.defineInstance(IAgentContextMemoryService, context); + reg.defineInstance(IAgentRecordService, stubRecord()); reg.defineInstance(IAgentLoopService, loop); + reg.definePartialInstance(IAgentPromptService, { + hooks: createHooks(['onWillSubmitPrompt']), + }); reg.defineInstance(IAgentTurnService, turnService); reg.defineInstance(IAgentToolExecutorService, stubToolExecutor()); reg.definePartialInstance(IAgentPermissionGate, { diff --git a/packages/agent-core-v2/test/gateway/gateway.test.ts b/packages/agent-core-v2/test/gateway/gateway.test.ts index 0187532a2..c016be59c 100644 --- a/packages/agent-core-v2/test/gateway/gateway.test.ts +++ b/packages/agent-core-v2/test/gateway/gateway.test.ts @@ -13,6 +13,7 @@ import { ILogService } from '#/app/log'; import { IAgentPromptService } from '#/agent/prompt'; import { ISessionLifecycleService } from '#/app/sessionLifecycle'; import { IAgentTurnService } from '#/agent/turn'; +import { createHooks } from '#/hooks'; import { stubLog } from '../log/stubs'; import { stubTurn } from '../turn/stubs'; @@ -51,12 +52,13 @@ describe('RestGateway', () => { _serviceBrand: undefined, prompt: (message) => { promptCalls.push(message); - return undefined; + return Promise.resolve(undefined); }, - steer: () => undefined, + steer: () => Promise.resolve(undefined), retry: () => undefined, undo: () => 0, clear: () => {}, + hooks: createHooks(['onWillSubmitPrompt']) as IAgentPromptService['hooks'], }; const agentHandle: IAgentScopeHandle = { diff --git a/packages/agent-core-v2/test/prompt/promptService.test.ts b/packages/agent-core-v2/test/prompt/promptService.test.ts new file mode 100644 index 000000000..889fd3af0 --- /dev/null +++ b/packages/agent-core-v2/test/prompt/promptService.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it, onTestFinished } from 'vitest'; + +import { DisposableStore } from '#/_base/di/lifecycle'; +import { createServices } from '#/_base/di/test'; +import { IAgentLoopService } from '#/agent/loop'; +import { AgentPromptService, IAgentPromptService } from '#/agent/prompt'; +import type { PromptSubmitContext } from '#/agent/prompt'; +import { IAgentContextMemoryService, type ContextMessage } from '#/agent/contextMemory'; +import { IAgentRecordService } from '#/agent/record'; +import { IAgentTurnService } from '#/agent/turn'; + +import { stubContextMemory, stubRecord } from '../contextMemory/stubs'; +import { stubLoopWithHooks, stubTurn } from '../turn/stubs'; + +function userMessage(text: string, origin: ContextMessage['origin']): ContextMessage { + return { + role: 'user', + content: [{ type: 'text', text }], + toolCalls: [], + origin, + }; +} + +function createHarness() { + const disposables = new DisposableStore(); + onTestFinished(() => disposables.dispose()); + + const context = stubContextMemory(); + const turn = stubTurn(); + const ix = createServices(disposables, { + strict: true, + additionalServices: (reg) => { + reg.defineInstance(IAgentContextMemoryService, context); + reg.defineInstance(IAgentTurnService, turn); + reg.defineInstance(IAgentRecordService, stubRecord()); + reg.defineInstance(IAgentLoopService, stubLoopWithHooks()); + reg.define(IAgentPromptService, AgentPromptService); + }, + }); + + return { + context, + prompt: ix.get(IAgentPromptService), + turn, + }; +} + +describe('AgentPromptService', () => { + it('runs submit hooks for any prompt and steer origin', async () => { + const { prompt, turn } = createHarness(); + const seen: Array & { + readonly originKind: string | undefined; + }> = []; + + prompt.hooks.onWillSubmitPrompt.register('capture', async (ctx, next) => { + seen.push({ isSteer: ctx.isSteer, originKind: ctx.promptMessage.origin?.kind }); + await next(); + }); + + await prompt.prompt(userMessage('from prompt', { kind: 'system_trigger', name: 'test_prompt' })); + await prompt.steer(userMessage('from steer', { kind: 'system_trigger', name: 'test_steer' })); + + expect(seen).toEqual([ + { isSteer: false, originKind: 'system_trigger' }, + { isSteer: true, originKind: 'system_trigger' }, + ]); + expect(turn.launches).toHaveLength(2); + }); + + it('blocks launch when the hook sets block', async () => { + const { context, prompt, turn } = createHarness(); + + prompt.hooks.onWillSubmitPrompt.register('block', async (ctx) => { + ctx.block = true; + }); + + const result = await prompt.prompt( + userMessage('blocked', { kind: 'system_trigger', name: 'test_block' }), + ); + + expect(result).toBeUndefined(); + expect(turn.launches).toEqual([]); + expect(context.messages).toHaveLength(1); + }); +}); diff --git a/packages/agent-core-v2/test/promptLegacy/promptLegacyService.test.ts b/packages/agent-core-v2/test/promptLegacy/promptLegacyService.test.ts index a344cc8fb..e6b543544 100644 --- a/packages/agent-core-v2/test/promptLegacy/promptLegacyService.test.ts +++ b/packages/agent-core-v2/test/promptLegacy/promptLegacyService.test.ts @@ -6,6 +6,7 @@ import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMo import { IAgentProfileService } from '#/agent/profile/profile'; import { IAgentPromptService } from '#/agent/prompt/prompt'; import { IAgentTurnService, type Turn, type TurnResult } from '#/agent/turn/turn'; +import { createHooks } from '#/hooks'; import type { PromptSubmission } from '@moonshot-ai/protocol'; import { IAgentPromptLegacyService, AgentPromptLegacyService } from '#/agent/promptLegacy'; @@ -40,7 +41,7 @@ interface Harness { readonly steered: string[]; } -function createHarness(): Harness { +function createHarness(options: { readonly blockPrompt?: boolean } = {}): Harness { const disposables = new DisposableStore(); onTestFinished(() => disposables.dispose()); @@ -53,7 +54,8 @@ function createHarness(): Harness { const prompt: IAgentPromptService = { _serviceBrand: undefined, prompt: () => { - if (activeTurn !== undefined) return undefined; + if (options.blockPrompt === true) return Promise.resolve(undefined); + if (activeTurn !== undefined) return Promise.resolve(undefined); const { turn, settle } = controlledTurn(nextTurnId++); activeTurn = turn; activeSettle = settle; @@ -64,17 +66,18 @@ function createHarness(): Harness { activeSettle = undefined; } }); - return turn; + return Promise.resolve(turn); }, steer: (message) => { for (const part of message.content) { if (part.type === 'text') steered.push(part.text); } - return undefined; + return Promise.resolve(undefined); }, retry: () => undefined, undo: () => 0, clear: () => {}, + hooks: createHooks(['onWillSubmitPrompt']) as IAgentPromptService['hooks'], }; const turnService: IAgentTurnService = { @@ -136,12 +139,21 @@ describe('AgentPromptLegacyService', () => { expect(list.queued.map((q) => q.prompt_id)).toEqual([second.prompt_id]); }); + it('reports blocked without queueing when no turn is launched', async () => { + const { service, turns } = createHarness({ blockPrompt: true }); + const result = await service.submit(textBody('blocked')); + expect(result.status).toBe('blocked'); + expect(turns).toHaveLength(0); + expect(service.list()).toEqual({ active: null, queued: [] }); + }); + it('auto-launches the next queued prompt when the active turn settles', async () => { const { service, turns, settleActive } = createHarness(); await service.submit(textBody('first')); const second = await service.submit(textBody('second')); settleActive({ reason: 'completed' }); await Promise.resolve(); + await Promise.resolve(); expect(turns).toHaveLength(2); expect(service.list().active?.prompt_id).toBe(second.prompt_id); }); @@ -154,6 +166,7 @@ describe('AgentPromptLegacyService', () => { expect(aborted.aborted).toBe(true); settleActive({ reason: 'cancelled' }); await Promise.resolve(); + await Promise.resolve(); expect(turns).toHaveLength(2); expect(service.list().active?.prompt_id).toBe(second.prompt_id); }); diff --git a/packages/agent-core-v2/test/skill/skill.test.ts b/packages/agent-core-v2/test/skill/skill.test.ts index 1d2db0cc1..5b5c9947c 100644 --- a/packages/agent-core-v2/test/skill/skill.test.ts +++ b/packages/agent-core-v2/test/skill/skill.test.ts @@ -55,11 +55,11 @@ describe('AgentSkillService', () => { reg.definePartialInstance(IAgentPromptService, { prompt: (message) => { prompted.push(message); - return fakeTurn(); + return Promise.resolve(fakeTurn()); }, steer: (message) => { prompted.push(message); - return undefined; + return Promise.resolve(undefined); }, retry: () => undefined, undo: () => 0, @@ -152,11 +152,11 @@ describe('SkillTool', () => { reg.definePartialInstance(IAgentPromptService, { prompt: (message: ContextMessage) => { prompted.push(message); - return fakeTurn(); + return Promise.resolve(fakeTurn()); }, steer: (message: ContextMessage) => { prompted.push(message); - return undefined; + return Promise.resolve(undefined); }, retry: () => undefined, undo: () => 0, diff --git a/packages/agent-core-v2/test/task/rpc-events.test.ts b/packages/agent-core-v2/test/task/rpc-events.test.ts index dfcb50c1a..6d55db0b8 100644 --- a/packages/agent-core-v2/test/task/rpc-events.test.ts +++ b/packages/agent-core-v2/test/task/rpc-events.test.ts @@ -205,7 +205,9 @@ function createAgentTaskService(options: { emittedEvents.push(event as { type: string; info?: unknown }); }); - const steerSpy = vi.spyOn(ctx.get(IAgentPromptService), 'steer').mockReturnValue(undefined); + const steerSpy = vi + .spyOn(ctx.get(IAgentPromptService), 'steer') + .mockResolvedValue(undefined); const context = ctx.get(IAgentContextMemoryService); const spliceHistorySpy = vi.spyOn(context, 'splice'); diff --git a/packages/agent-core-v2/test/task/service.test.ts b/packages/agent-core-v2/test/task/service.test.ts index e4bfd9acc..83563bf4b 100644 --- a/packages/agent-core-v2/test/task/service.test.ts +++ b/packages/agent-core-v2/test/task/service.test.ts @@ -49,7 +49,7 @@ describe('AgentTaskService', () => { ix.stub(IAgentToolRegistryService, { register: () => toDisposable(() => {}), }); - ix.stub(IAgentPromptService, { steer: () => undefined }); + ix.stub(IAgentPromptService, { steer: () => Promise.resolve(undefined) }); ix.stub(IConfigRegistry, { registerSection: () => {} }); ix.stub(IConfigService, { get: (() => undefined) as IConfigService['get'], diff --git a/packages/agent-core-v2/test/turn/stubs.ts b/packages/agent-core-v2/test/turn/stubs.ts index 458dd6c81..02d529ce7 100644 --- a/packages/agent-core-v2/test/turn/stubs.ts +++ b/packages/agent-core-v2/test/turn/stubs.ts @@ -43,7 +43,6 @@ function makeTurn(id: number): Turn { function makeHooks(): IAgentTurnService['hooks'] { return createHooks([ 'onLaunched', - 'onWillSubmitUserPrompt', 'onEnded', ]) as IAgentTurnService['hooks']; } diff --git a/packages/agent-core-v2/test/turn/turn.test.ts b/packages/agent-core-v2/test/turn/turn.test.ts index d2c3e55d8..635c337ee 100644 --- a/packages/agent-core-v2/test/turn/turn.test.ts +++ b/packages/agent-core-v2/test/turn/turn.test.ts @@ -619,7 +619,7 @@ describe('Agent turn flow', () => { ); }); - it('emits a friendly model.not_configured error when no model is configured', async () => { + it('emits a model.not_configured error when no model is configured', async () => { const ctx = testAgent(configServices(() => ({ providers: {} })), { autoConfigure: false }); await ctx.rpc.prompt({ input: [{ type: 'text', text: 'Hello without login' }] }); @@ -628,10 +628,10 @@ describe('Agent turn flow', () => { [wire] context.splice { "start": 0, "deleteCount": 0, "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Hello without login" } ], "toolCalls": [], "id": "" } ], "time": "