refactor: remove PromptOrigin from IAgentTurnService.launch

Turn origin is tracked on context messages instead; drop it from the turn launch API and wire records.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
_Kerman 2026-07-04 17:03:45 +08:00
parent 0d8b9f96ef
commit 8705d502f0
9 changed files with 27 additions and 37 deletions

View file

@ -504,7 +504,7 @@ export class AgentGoalService extends Disposable implements IAgentGoalService {
origin: GOAL_CONTINUATION_ORIGIN,
});
this.context.splice(this.context.get().length, 0, [message]);
this.turnService.launch(GOAL_CONTINUATION_ORIGIN);
this.turnService.launch();
}
private normalizeAfterReplay(): void {

View file

@ -5,9 +5,7 @@ import { ErrorCodes, KimiError } from '#/errors';
import {
ensureMessageId,
IAgentContextMemoryService,
USER_PROMPT_ORIGIN,
type ContextMessage,
type PromptOrigin,
} from '#/agent/contextMemory';
import { IAgentLoopService } from '#/agent/loop';
import { IAgentRecordService } from '#/agent/record';
@ -56,7 +54,7 @@ export class AgentPromptService implements IAgentPromptService {
const stamped = ensureMessageId(message);
this.append(stamped);
if (await this.blockedByHook(stamped, false)) return undefined;
return this.launch(stamped.origin ?? USER_PROMPT_ORIGIN);
return this.launch();
}
steer(message: ContextMessage): PromptSteerHandle {
@ -82,7 +80,7 @@ export class AgentPromptService implements IAgentPromptService {
}
retry(trigger?: string): Turn | undefined {
return this.launch({ kind: 'retry', trigger });
return this.launch();
}
undo(count: number): number {
@ -134,8 +132,8 @@ export class AgentPromptService implements IAgentPromptService {
this.context.splice(this.context.get().length, 0, messages);
}
private launch(origin: PromptOrigin): Turn {
const turn = this.turnService.launch(origin);
private launch(): Turn {
const turn = this.turnService.launch();
this.observe(turn);
return turn;
}

View file

@ -1,6 +1,5 @@
import { createDecorator } from "#/_base/di";
import type { TurnResult } from '#/agent/loop';
import type { PromptOrigin } from '#/agent/contextMemory';
import type { Hooks } from '#/hooks';
export type { TurnResult } from '#/agent/loop';
@ -23,7 +22,7 @@ export interface TurnEndedContext {
export interface IAgentTurnService {
readonly _serviceBrand: undefined;
launch(origin: PromptOrigin): Turn;
launch(): Turn;
getActiveTurn(): Turn | undefined;
readonly hooks: Hooks<{

View file

@ -3,7 +3,6 @@ import { createControlledPromise } from '@antfu/utils';
import { InstantiationType } from '#/_base/di/extensions';
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
import { ErrorCodes, KimiError, toKimiErrorPayload } from '#/errors';
import type { PromptOrigin } from '#/agent/contextMemory';
import { OrderedHookSlot } from '#/hooks';
import { IAgentLoopService } from '#/agent/loop';
import { IAgentTelemetryContextService, ITelemetryService } from '#/app/telemetry';
@ -19,7 +18,6 @@ declare module '#/agent/wireRecord' {
interface WireRecordMap {
'turn.launch': {
turnId: number;
origin: PromptOrigin;
};
}
}
@ -47,7 +45,7 @@ export class AgentTurnService implements IAgentTurnService {
});
}
launch(origin: PromptOrigin): Turn {
launch(): Turn {
if (this.activeTurn !== undefined) {
throw new KimiError(
ErrorCodes.TURN_AGENT_BUSY,
@ -57,7 +55,7 @@ export class AgentTurnService implements IAgentTurnService {
}
const turnId = this.nextTurnId;
this.record.append({ type: 'turn.launch', turnId, origin });
this.record.append({ type: 'turn.launch', turnId });
this.restoreLaunch(turnId);
const abortController = new AbortController();
const ready = createControlledPromise<void>();
@ -69,7 +67,7 @@ export class AgentTurnService implements IAgentTurnService {
};
void ready.catch(() => undefined);
this.activeTurn = turn;
turn.result = this.runTurn(turn, origin, ready);
turn.result = this.runTurn(turn, ready);
void this.hooks.onLaunched.run({ turn });
return turn;
}
@ -80,7 +78,6 @@ export class AgentTurnService implements IAgentTurnService {
private async runTurn(
turn: Turn,
origin: PromptOrigin,
ready: ReturnType<typeof createControlledPromise<void>>,
): Promise<TurnResult> {
const startedAt = Date.now();
@ -91,7 +88,6 @@ export class AgentTurnService implements IAgentTurnService {
this.record.signal({
type: 'turn.started',
turnId: turn.id,
origin,
});
result = await this.loop.runTurn(turn.id, {
signal: turn.abortController.signal,

View file

@ -118,7 +118,7 @@ describe('RestGateway', () => {
it('aborts the active turn signal on cancel', async () => {
const gw = ix.get(IRestGateway);
const turn = turnService.launch({ kind: 'user' });
const turn = turnService.launch();
await gw.cancel('s1', 'main', 'bye');
expect(turn.abortController.signal.aborted).toBe(true);

View file

@ -567,7 +567,7 @@ describe('AgentGoalService core workflow hooks', () => {
status: 'active',
turnsUsed: 1,
});
expect(turnService.launches).toEqual([{ kind: 'system_trigger', name: 'goal_continuation' }]);
expect(turnService.launches).toHaveLength(1);
expect(context.get().at(-1)?.origin).toEqual({
kind: 'system_trigger',
name: 'goal_continuation',
@ -596,7 +596,7 @@ describe('AgentGoalService core workflow hooks', () => {
await goals.createGoal({ objective: 'finish the task' });
await goals.setBudgetLimits({ budgetLimits: { tokenBudget: 7 } }, 'model');
const turn = turnService.launch({ kind: 'user' });
const turn = turnService.launch();
await turnService.hooks.onLaunched.run({ turn });
expect(
@ -653,7 +653,7 @@ describe('AgentGoalService core workflow hooks', () => {
status: 'active',
turnsUsed: 0,
});
expect(turnService.launches).toEqual([{ kind: 'system_trigger', name: 'goal_continuation' }]);
expect(turnService.launches).toHaveLength(1);
});
it('requests one final outcome turn after model completion', async () => {

View file

@ -85,7 +85,7 @@ describe('AgentPromptService', () => {
it('runs submit hooks before queuing active steers', async () => {
const { context, loop, prompt, turn } = createHarness({ hasActiveTurn: true });
const activeTurn = turn.launch({ kind: 'user' });
const activeTurn = turn.launch();
const seen: Array<Pick<PromptSubmitContext, 'isSteer'> & {
readonly originKind: string | undefined;
}> = [];
@ -123,7 +123,7 @@ describe('AgentPromptService', () => {
it('does not queue active steers blocked by hooks', async () => {
const { context, loop, prompt, turn } = createHarness({ hasActiveTurn: true });
const activeTurn = turn.launch({ kind: 'user' });
const activeTurn = turn.launch();
prompt.hooks.onWillSubmitPrompt.register('block', async (ctx) => {
ctx.block = true;

View file

@ -5,7 +5,6 @@
* production tree. Import from a relative path (`./stubs` or `../turn/stubs`).
*/
import type { PromptOrigin } from '#/agent/contextMemory';
import type { IAgentLoopService } from '#/agent/loop';
import type { IAgentToolExecutorService } from '#/agent/toolExecutor';
import type { IAgentTurnService, Turn } from '#/agent/turn';
@ -20,7 +19,7 @@ export interface StubTurnOptions {
}
/**
* An `IAgentTurnService` stub that also records `launch` origins and exposes the
* An `IAgentTurnService` stub that also records `launch` calls and exposes the
* active turn. `prompts` / `steered` are retained as empty arrays for legacy
* assertions the HEAD `IAgentTurnService` has no `prompt` / `steer` methods, so
* tests that used to drive them should be rewritten against `launch` + hooks.
@ -28,7 +27,7 @@ export interface StubTurnOptions {
export type StubTurn = IAgentTurnService & {
readonly prompts: readonly string[];
readonly steered: readonly string[];
readonly launches: readonly PromptOrigin[];
readonly launches: readonly number[];
};
function makeTurn(id: number): Turn {
@ -57,16 +56,17 @@ function makeAgentLoopHookSlots(): IAgentLoopService['hooks'] {
/** A configurable `IAgentTurnService` stub backed by real `OrderedHookSlot`s. */
export function stubTurn(options: StubTurnOptions = {}): StubTurn {
const launches: PromptOrigin[] = [];
const launches: number[] = [];
let activeTurn: Turn | undefined;
let nextId = typeof options.currentId === 'number' ? options.currentId : 0;
return {
_serviceBrand: undefined,
hooks: makeHooks(),
launch(origin) {
launches.push(origin);
activeTurn = makeTurn(nextId++);
return activeTurn;
launch() {
const turn = makeTurn(nextId++);
launches.push(turn.id);
activeTurn = turn;
return turn;
},
getActiveTurn() {
return options.hasActiveTurn ? activeTurn : undefined;

View file

@ -3,7 +3,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { DisposableStore } from '#/_base/di/lifecycle';
import { createServices, type TestInstantiationService } from '#/_base/di/test';
import type { PromptOrigin } from '#/agent/contextMemory';
import { IAgentContextMemoryService } from '#/agent/contextMemory';
import { AgentLoopService, IAgentLoopService } from '#/agent/loop';
import { IAgentLLMRequesterService } from '#/agent/llmRequester';
@ -21,8 +20,6 @@ import { stubLog } from '../log/stubs';
import { recordingTelemetry } from '../telemetry/stubs';
import { stubLoopWithHooks, stubToolExecutor } from './stubs';
const SYSTEM_ORIGIN: PromptOrigin = { kind: 'system_trigger', name: 'test' };
describe('AgentTurnService ready', () => {
let disposables: DisposableStore;
let ix: TestInstantiationService;
@ -75,7 +72,7 @@ describe('AgentTurnService ready', () => {
return { reason: 'completed', steps: 1 };
};
const turn = ix.get(IAgentTurnService).launch(SYSTEM_ORIGIN);
const turn = ix.get(IAgentTurnService).launch();
void turn.ready.then(
() => {
readySettled = true;
@ -103,7 +100,7 @@ describe('AgentTurnService ready', () => {
const cause = new Error('loop failed before first step');
loop.runTurn = async () => ({ reason: 'failed', error: cause, steps: 0 });
const turn = ix.get(IAgentTurnService).launch(SYSTEM_ORIGIN);
const turn = ix.get(IAgentTurnService).launch();
let readyError: unknown;
await turn.ready.catch((error: unknown) => {
readyError = error;
@ -123,10 +120,10 @@ describe('AgentTurnService ready', () => {
};
const turnService = ix.get(IAgentTurnService);
const turn = turnService.launch(SYSTEM_ORIGIN);
const turn = turnService.launch();
let error: unknown;
try {
turnService.launch(SYSTEM_ORIGIN);
turnService.launch();
} catch (caught) {
error = caught;
} finally {