mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
Merge remote-tracking branch 'origin/kimi-code-v2' into xtr/context-ops-wire
# Conflicts: # packages/agent-core-v2/src/agent/contextMemory/contextMemory.ts # packages/agent-core-v2/src/agent/goal/goalService.ts # packages/agent-core-v2/src/agent/prompt/promptService.ts # packages/agent-core-v2/src/agent/systemReminder/systemReminder.ts
This commit is contained in:
commit
bec0b085f2
99 changed files with 313 additions and 172 deletions
|
|
@ -1266,11 +1266,12 @@ export function useWorkspaceState(rawState: ExtendedState, deps: UseWorkspaceSta
|
|||
// 1. Authoritative id captured at submit time.
|
||||
let promptId = rawState.promptIdBySession[sid];
|
||||
|
||||
// 2. Fallback to projector-derived id only when it is a real daemon prompt_id
|
||||
// (synthetic `pr_...` ids are rejected by the daemon).
|
||||
// 2. Fallback to projector-derived id only when it is a real daemon prompt_id.
|
||||
// The v1 daemon uses `prompt_...`, server-v2 legacy uses `msg_...`;
|
||||
// only local synthetic `pr_...` ids are rejected by the daemon.
|
||||
if (promptId === undefined) {
|
||||
const candidate = session?.currentPromptId;
|
||||
if (candidate?.startsWith('prompt_')) {
|
||||
if (candidate !== undefined && candidate.length > 0 && !candidate.startsWith('pr_')) {
|
||||
promptId = candidate;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,6 +236,32 @@ describe('useWorkspaceState — abortCurrentPrompt', () => {
|
|||
expect(apiMock.abortPrompt).toHaveBeenCalledWith('sess_1', 'prompt_stale');
|
||||
expect(apiMock.abortSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses a server-v2 msg prompt id recovered from session state', async () => {
|
||||
apiMock.abortPrompt.mockResolvedValue({ aborted: true });
|
||||
const state = createState();
|
||||
state.promptIdBySession = {};
|
||||
state.sessions = [{ ...state.sessions[0]!, currentPromptId: 'msg_live' }];
|
||||
const workspace = useWorkspaceState(state, createDeps());
|
||||
|
||||
await workspace.abortCurrentPrompt();
|
||||
|
||||
expect(apiMock.abortPrompt).toHaveBeenCalledWith('sess_1', 'msg_live');
|
||||
expect(apiMock.abortSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not send synthetic projector prompt ids to per-prompt abort', async () => {
|
||||
apiMock.abortSession.mockResolvedValue({ aborted: true });
|
||||
const state = createState();
|
||||
state.promptIdBySession = {};
|
||||
state.sessions = [{ ...state.sessions[0]!, currentPromptId: 'pr_synthetic' }];
|
||||
const workspace = useWorkspaceState(state, createDeps());
|
||||
|
||||
await workspace.abortCurrentPrompt();
|
||||
|
||||
expect(apiMock.abortPrompt).not.toHaveBeenCalled();
|
||||
expect(apiMock.abortSession).toHaveBeenCalledWith('sess_1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeWorkspaces', () => {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export interface AgentBlobServiceOptions {
|
|||
|
||||
export interface IAgentBlobService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
offloadParts(parts: readonly ContentPart[]): Promise<readonly ContentPart[]>;
|
||||
rehydrateParts(parts: readonly ContentPart[]): Promise<readonly ContentPart[]>;
|
||||
isBlobRef(url: string): boolean;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export type ContextInjectionProvider = (
|
|||
|
||||
export interface IAgentContextInjectorService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
register(
|
||||
variant: string,
|
||||
provider: ContextInjectionProvider,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { ContextMessage } from '#/agent/contextMemory';
|
|||
|
||||
export interface IAgentContextProjectorService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
project(messages: readonly ContextMessage[]): readonly Message[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export interface AgentTaskStopHookContext {
|
|||
|
||||
export interface IAgentExternalHooksService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/**
|
||||
* Run the blocking `SubagentStart` external hook for an agent task this
|
||||
* agent is launching (via the `Agent` tool / swarm). Called directly by the
|
||||
|
|
|
|||
|
|
@ -25,14 +25,15 @@ export interface FullCompactionDidCompactContext {
|
|||
|
||||
export interface IAgentFullCompactionService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly isCompacting: boolean;
|
||||
begin(input: CompactInput): boolean;
|
||||
cancel(): void;
|
||||
|
||||
readonly hooks: Hooks<{
|
||||
onWillCompact: FullCompactionWillCompactContext;
|
||||
onDidCompact: FullCompactionDidCompactContext;
|
||||
}>;
|
||||
readonly isCompacting: boolean;
|
||||
|
||||
begin(input: CompactInput): boolean;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export const IAgentFullCompactionService = createDecorator<IAgentFullCompactionService>('agentFullCompactionService');
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface GoalReasonInput {
|
|||
|
||||
export interface IAgentGoalService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
getGoal(): GoalToolResult;
|
||||
createGoal(input: CreateGoalInput, actor?: GoalActor): Promise<GoalSnapshot>;
|
||||
pauseGoal(input?: GoalReasonInput, actor?: GoalActor): Promise<GoalSnapshot>;
|
||||
|
|
|
|||
|
|
@ -504,7 +504,7 @@ export class AgentGoalService extends Disposable implements IAgentGoalService {
|
|||
origin: GOAL_CONTINUATION_ORIGIN,
|
||||
});
|
||||
this.contextOps.append(message);
|
||||
this.turnService.launch(GOAL_CONTINUATION_ORIGIN);
|
||||
this.turnService.launch();
|
||||
}
|
||||
|
||||
private normalizeAfterReplay(): void {
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ export interface LLMRequestOverrides {
|
|||
|
||||
export interface IAgentLLMRequesterService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
request(
|
||||
overrides?: LLMRequestOverrides,
|
||||
onPart?: LLMRequestPartHandler,
|
||||
|
|
|
|||
|
|
@ -28,10 +28,11 @@ export interface TurnErrorContext {
|
|||
retry: boolean;
|
||||
}
|
||||
|
||||
export interface RunTurnOptions {
|
||||
export interface RunOptions {
|
||||
readonly turnId: number;
|
||||
readonly signal?: AbortSignal;
|
||||
/** Fires on the first model response event for a step, or at step completion. */
|
||||
readonly onStepStarted?: (step: number) => void;
|
||||
readonly onStarted?: (step: number) => void;
|
||||
}
|
||||
|
||||
export interface TurnResult {
|
||||
|
|
@ -42,12 +43,14 @@ export interface TurnResult {
|
|||
|
||||
export interface IAgentLoopService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
run(options: RunOptions): Promise<TurnResult>;
|
||||
|
||||
readonly hooks: Hooks<{
|
||||
beforeStep: TurnBeforeStepContext;
|
||||
afterStep: TurnAfterStepContext;
|
||||
onError: TurnErrorContext;
|
||||
}>;
|
||||
runTurn(turnId: number, options?: RunTurnOptions): Promise<TurnResult>;
|
||||
}
|
||||
|
||||
export const IAgentLoopService = createDecorator<IAgentLoopService>('agentLoopService');
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import {
|
|||
} from './errors';
|
||||
import {
|
||||
IAgentLoopService,
|
||||
type RunTurnOptions,
|
||||
type RunOptions,
|
||||
type TurnAfterStepContext,
|
||||
type TurnResult,
|
||||
} from './loop';
|
||||
|
|
@ -58,10 +58,8 @@ export class AgentLoopService implements IAgentLoopService {
|
|||
@IConfigService private readonly config: IConfigService,
|
||||
) { }
|
||||
|
||||
async runTurn(
|
||||
turnId: number,
|
||||
options: RunTurnOptions = {},
|
||||
): Promise<TurnResult> {
|
||||
async run(options: RunOptions): Promise<TurnResult> {
|
||||
const { turnId } = options;
|
||||
const signal = options.signal ?? new AbortController().signal;
|
||||
|
||||
let steps = 0;
|
||||
|
|
@ -83,7 +81,7 @@ export class AgentLoopService implements IAgentLoopService {
|
|||
turnId,
|
||||
signal,
|
||||
steps,
|
||||
options.onStepStarted,
|
||||
options.onStarted,
|
||||
);
|
||||
activeStep = undefined;
|
||||
|
||||
|
|
@ -135,7 +133,7 @@ export class AgentLoopService implements IAgentLoopService {
|
|||
turnId: number,
|
||||
signal: AbortSignal,
|
||||
currentStep: number,
|
||||
onStepStarted: ((step: number) => void) | undefined,
|
||||
onStarted: ((step: number) => void) | undefined,
|
||||
): Promise<{
|
||||
readonly stopReason: FinishReason;
|
||||
readonly continue: boolean;
|
||||
|
|
@ -151,7 +149,7 @@ export class AgentLoopService implements IAgentLoopService {
|
|||
const markStepStarted = (): void => {
|
||||
if (stepStarted) return;
|
||||
stepStarted = true;
|
||||
onStepStarted?.(currentStep);
|
||||
onStarted?.(currentStep);
|
||||
};
|
||||
const emitStreamPart = this.createStreamPartHandler(turnId, markStepStarted);
|
||||
const response = await this.llmRequester.request(
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ export interface McpResolvedServer {
|
|||
|
||||
export interface IAgentMcpService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly oauthService: McpOAuthService | undefined;
|
||||
|
||||
readonly oauthService: McpOAuthService | undefined;
|
||||
waitForInitialLoad(signal?: AbortSignal): Promise<void>;
|
||||
initialLoadDurationMs(): number;
|
||||
list(): readonly McpServerEntry[];
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export interface MicroCompactionEffect {
|
|||
|
||||
export interface IAgentMicroCompactionService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
compact(messages: readonly ContextMessage[]): readonly ContextMessage[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,14 +44,16 @@ export type PermissionApprovalResultContext =
|
|||
|
||||
export interface IAgentPermissionGate {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly hooks: Hooks<{
|
||||
onDidRequestApproval: PermissionApprovalRequestContext;
|
||||
onDidResolveApproval: PermissionApprovalResultContext;
|
||||
}>;
|
||||
|
||||
data(): PermissionData;
|
||||
authorize(
|
||||
context: ResolvedToolExecutionHookContext,
|
||||
): Promise<AuthorizeToolExecutionResult | undefined>;
|
||||
|
||||
readonly hooks: Hooks<{
|
||||
onDidRequestApproval: PermissionApprovalRequestContext;
|
||||
onDidResolveApproval: PermissionApprovalResultContext;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const IAgentPermissionGate =
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export interface PermissionModeChangedContext {
|
|||
|
||||
export interface IAgentPermissionModeService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly mode: PermissionMode;
|
||||
setMode(mode: PermissionMode): void;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export interface PermissionPolicyEvaluation {
|
|||
|
||||
export interface IAgentPermissionPolicyService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
evaluate(
|
||||
context: ResolvedToolExecutionHookContext,
|
||||
): Promise<PermissionPolicyEvaluation | undefined>;
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ export interface PermissionRule {
|
|||
|
||||
export interface IAgentPermissionRulesService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly rules: readonly PermissionRule[];
|
||||
readonly sessionApprovalRulePatterns: readonly string[];
|
||||
|
||||
addRules(rules: readonly PermissionRule[]): void;
|
||||
recordApprovalResult(record: PermissionApprovalResultRecord): void;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export type PlanFilePath = string | null;
|
|||
|
||||
export interface IAgentPlanService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
enter(id?: string, createFile?: boolean): Promise<void>;
|
||||
cancel(id?: string): void;
|
||||
clear(): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ export interface BindAgentInput {
|
|||
|
||||
export interface IAgentProfileService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
configure(options: ProfileServiceOptions): void;
|
||||
update(changed: ProfileUpdateData): void;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export interface PromptSteerHandle {
|
|||
|
||||
export interface IAgentPromptService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
prompt(message: ContextMessage): Promise<Turn | undefined>;
|
||||
steer(message: ContextMessage): PromptSteerHandle;
|
||||
retry(trigger?: string): Turn | undefined;
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@ import { ErrorCodes, KimiError } from '#/errors';
|
|||
import {
|
||||
ensureMessageId,
|
||||
IAgentContextMemoryService,
|
||||
USER_PROMPT_ORIGIN,
|
||||
type ContextMessage,
|
||||
type ContextOperation,
|
||||
type PromptOrigin,
|
||||
} from '#/agent/contextMemory';
|
||||
import {
|
||||
IAgentContextOpsService,
|
||||
|
|
@ -78,7 +76,7 @@ export class AgentPromptService implements IAgentPromptService {
|
|||
const stamped = ensureMessageId(message);
|
||||
this.contextOps.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 {
|
||||
|
|
@ -104,7 +102,7 @@ export class AgentPromptService implements IAgentPromptService {
|
|||
}
|
||||
|
||||
retry(trigger?: string): Turn | undefined {
|
||||
return this.launch({ kind: 'retry', trigger });
|
||||
return this.launch();
|
||||
}
|
||||
|
||||
undo(count: number): number {
|
||||
|
|
@ -154,8 +152,8 @@ export class AgentPromptService implements IAgentPromptService {
|
|||
this.contextOps.clear();
|
||||
}
|
||||
|
||||
private launch(origin: PromptOrigin): Turn {
|
||||
const turn = this.turnService.launch(origin);
|
||||
private launch(): Turn {
|
||||
const turn = this.turnService.launch();
|
||||
this.observe(turn);
|
||||
return turn;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiatio
|
|||
|
||||
export interface IAgentPromptLegacyService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
list(): PromptListResponse;
|
||||
submit(body: PromptSubmission): Promise<PromptSubmitResult>;
|
||||
steer(promptIds: readonly string[]): Promise<PromptSteerResult>;
|
||||
|
|
|
|||
|
|
@ -97,7 +97,6 @@ export interface IAgentRecordService {
|
|||
* Returns a disposable that unregisters the facets (and the resumer).
|
||||
*/
|
||||
define<K extends keyof AgentRecordMap>(type: K, facets: RecordFacets<K>): IDisposable;
|
||||
|
||||
/**
|
||||
* Append a record to the replay read model directly. Used when the projected
|
||||
* data is computed inside a domain handler rather than derived from a single
|
||||
|
|
@ -125,9 +124,9 @@ export interface IAgentRecordService {
|
|||
buildReplay(): readonly AgentReplayRecord[];
|
||||
/** When true, live `append` calls also feed the replay read model. */
|
||||
captureLiveRecords: boolean;
|
||||
|
||||
readonly restoring: WireRecordRestoringContext | null;
|
||||
readonly postRestoring: boolean;
|
||||
|
||||
readonly hooks: IAgentWireRecordService['hooks'];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import type {
|
|||
import type { PromisableMethods } from "#/_base/utils/types";
|
||||
|
||||
export interface IAgentRPCService extends PromisableMethods<AgentAPI> {
|
||||
readonly _serviceBrand: undefined;}
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export interface ISessionRPCService extends PromisableMethods<SessionAPI> {
|
||||
readonly _serviceBrand: undefined;}
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const IAgentRPCService =
|
||||
createDecorator<IAgentRPCService>('agentRPCService');
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiatio
|
|||
|
||||
export interface IAgentScopeContext {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly agentId: string;
|
||||
|
||||
readonly agentId: string;
|
||||
/**
|
||||
* Persistence scope rooted at this agent. `scope()` returns the agent
|
||||
* scope itself; `scope(subKey)` returns `${agentScope}/${subKey}` (e.g.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ export interface IAgentSkillService {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
activate(input: SkillActivationInput): Promise<Turn>;
|
||||
|
||||
/**
|
||||
* Records a model-tool skill activation (an inline skill loaded through the
|
||||
* `Skill` tool) without opening a new turn — the tool builds and steers its
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export type SwarmModeTrigger = 'manual' | 'task' | 'tool';
|
|||
|
||||
export interface IAgentSwarmService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly isActive: boolean;
|
||||
enter(trigger: SwarmModeTrigger): void;
|
||||
exit(): void;
|
||||
|
|
|
|||
|
|
@ -98,16 +98,10 @@ export interface AgentTaskNotificationContext {
|
|||
export interface IAgentTaskService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly hooks: Hooks<{
|
||||
onDidNotify: AgentTaskNotificationContext;
|
||||
}>;
|
||||
|
||||
/** Track a `ITaskHandle` (from `taskService.run()`). */
|
||||
track(handle: ITaskHandle, options: AgentTaskTrackOptions): IAgentTaskEntry;
|
||||
|
||||
/** @deprecated Use `taskService.run()` + `track()` instead. */
|
||||
registerTask(task: AgentTask, options?: RegisterAgentTaskOptions): string;
|
||||
|
||||
getTask(taskId: string): AgentTaskInfo | undefined;
|
||||
list(activeOnly?: boolean, limit?: number): readonly AgentTaskInfo[];
|
||||
persistOutput(taskId: string): void;
|
||||
|
|
@ -124,6 +118,10 @@ export interface IAgentTaskService {
|
|||
waitForForegroundRelease(
|
||||
taskId: string,
|
||||
): Promise<ForegroundTaskReleaseReason | undefined>;
|
||||
|
||||
readonly hooks: Hooks<{
|
||||
onDidNotify: AgentTaskNotificationContext;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const IAgentTaskService =
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export type ToolDedupResult = ToolDedupSuccessResult | ToolDedupErrorResult;
|
|||
|
||||
export interface IAgentToolDedupeService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly currentStreak: number;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export interface ToolRegistrationOptions {
|
|||
|
||||
export interface IAgentToolRegistryService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
register(tool: ExecutableTool, options?: ToolRegistrationOptions): IDisposable;
|
||||
list(): readonly ToolInfo[];
|
||||
resolve(name: string): ExecutableTool | undefined;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export interface ToolStoreUpdate<K extends ToolStoreKey = ToolStoreKey> {
|
|||
|
||||
export interface IAgentToolState extends ToolStore {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
data(): Readonly<Partial<ToolStoreData>>;
|
||||
|
||||
readonly hooks: Hooks<{
|
||||
|
|
|
|||
|
|
@ -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,8 @@ export interface TurnEndedContext {
|
|||
|
||||
export interface IAgentTurnService {
|
||||
readonly _serviceBrand: undefined;
|
||||
launch(origin: PromptOrigin): Turn;
|
||||
|
||||
launch(): Turn;
|
||||
getActiveTurn(): Turn | undefined;
|
||||
|
||||
readonly hooks: Hooks<{
|
||||
|
|
|
|||
|
|
@ -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,11 +88,11 @@ export class AgentTurnService implements IAgentTurnService {
|
|||
this.record.signal({
|
||||
type: 'turn.started',
|
||||
turnId: turn.id,
|
||||
origin,
|
||||
});
|
||||
result = await this.loop.runTurn(turn.id, {
|
||||
result = await this.loop.run({
|
||||
turnId: turn.id,
|
||||
signal: turn.abortController.signal,
|
||||
onStepStarted: () => ready.resolve(),
|
||||
onStarted: () => ready.resolve(),
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface UsageStatus {
|
|||
|
||||
export interface IAgentUsageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
record(model: string, usage: TokenUsage, source?: LLMRequestSource): void;
|
||||
status(): UsageStatus;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,9 +63,9 @@ export interface WireRecordServiceOptions {
|
|||
|
||||
export interface IAgentWireRecordService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly restoring: WireRecordRestoringContext | null;
|
||||
readonly postRestoring: boolean;
|
||||
|
||||
append(record: WireRecord): void;
|
||||
/**
|
||||
* Snapshot of every record currently held in memory (live-appended and
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export interface AgentProfile {
|
|||
|
||||
export interface IAgentProfileCatalogService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/** Return the profile with the given name, or `undefined` when unknown. */
|
||||
get(name: string): AgentProfile | undefined;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export interface AuthStatus {
|
|||
|
||||
export interface IOAuthService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
startLogin(provider?: string): Promise<OAuthFlowStart>;
|
||||
getFlow(provider?: string): OAuthFlowSnapshot | undefined;
|
||||
cancelLogin(provider?: string): Promise<OAuthLoginCancelResponse>;
|
||||
|
|
@ -51,6 +52,7 @@ export const IOAuthService: ServiceIdentifier<IOAuthService> =
|
|||
|
||||
export interface IOAuthToolkit {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
login(providerName?: string, options?: KimiOAuthLoginOptions): Promise<KimiOAuthLoginResult>;
|
||||
logout(providerName?: string, oauthRef?: KimiOAuthTokenRef): Promise<KimiOAuthLogoutResult>;
|
||||
getCachedAccessToken(
|
||||
|
|
@ -65,6 +67,7 @@ export const IOAuthToolkit: ServiceIdentifier<IOAuthToolkit> =
|
|||
|
||||
export interface IAuthSummaryService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
summarize(): Promise<readonly AuthStatus[]>;
|
||||
ensureReady(provider?: string): Promise<void>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export interface WebSearchProviderOptions {
|
|||
|
||||
export interface IWebSearchProviderService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
getWebSearchProvider(): WebSearchProvider | undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ export interface IBootstrapService {
|
|||
readonly arch: string;
|
||||
readonly cwd: string;
|
||||
readonly osHomeDir: string;
|
||||
|
||||
readonly homeDir: string;
|
||||
readonly configPath: string;
|
||||
readonly sessionsDir: string;
|
||||
|
|
@ -72,40 +71,33 @@ export interface IBootstrapService {
|
|||
readonly storeDir: string;
|
||||
readonly cacheDir: string;
|
||||
readonly logsDir: string;
|
||||
|
||||
getEnv(name: string): string | undefined;
|
||||
|
||||
/**
|
||||
* Scope string for a well-known top-level persistence area. Business code
|
||||
* passes this to `IFileSystemStorageService` / `IAtomicDocumentStore` / `IAppendLogStore`
|
||||
* — the backend layer converts it to concrete addressing.
|
||||
*/
|
||||
scope(name: PersistenceScopeName): string;
|
||||
|
||||
/**
|
||||
* Scope string for a session's persistence root.
|
||||
* Equivalent to `${scope('sessions')}/${workspaceId}/${sessionId}`.
|
||||
*/
|
||||
sessionScope(workspaceId: string, sessionId: string): string;
|
||||
|
||||
/**
|
||||
* Scope string for a specific agent's persistence root under a session.
|
||||
* Equivalent to `${sessionScope(wsId, sId)}/agents/${agentId}`.
|
||||
*/
|
||||
agentScope(workspaceId: string, sessionId: string, agentId: string): string;
|
||||
|
||||
/**
|
||||
* File-only: absolute on-disk directory for a session.
|
||||
* Prefer `sessionScope(...)` — this exists for legacy APIs (session logs,
|
||||
* task output files). Non-file bootstraps may throw.
|
||||
*/
|
||||
sessionDir(workspaceId: string, sessionId: string): string;
|
||||
|
||||
/**
|
||||
* File-only: absolute on-disk directory for a specific agent. Same caveat.
|
||||
*/
|
||||
agentHomedir(workspaceId: string, sessionId: string, agentId: string): string;
|
||||
|
||||
/** Key of the config document under `scope('config')` (file: `'config.toml'`). */
|
||||
readonly configKey: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ export interface IConfigRegistry {
|
|||
|
||||
readonly onDidRegisterSection: Event<ConfigSectionRegisteredEvent>;
|
||||
readonly onDidRegisterOverlay: Event<ConfigOverlayRegisteredEvent>;
|
||||
|
||||
registerSection<T>(domain: string, schema: ConfigSchema<T>, options?: RegisterSectionOptions<T>): void;
|
||||
getSection(domain: string): ConfigSection | undefined;
|
||||
listSections(): readonly ConfigSection[];
|
||||
|
|
@ -147,10 +146,10 @@ export interface ConfigInspectValue<T = unknown> {
|
|||
|
||||
export interface IConfigService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly ready: Promise<void>;
|
||||
readonly onDidChangeConfiguration: Event<ConfigChangedEvent>;
|
||||
readonly onDidSectionChange: Event<ConfigSectionChangedEvent>;
|
||||
|
||||
get<T = unknown>(domain: string): T;
|
||||
inspect<T = unknown>(domain: string): ConfigInspectValue<T>;
|
||||
getAll(): ResolvedConfig;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export interface CronTaskQuery {
|
|||
|
||||
export interface ICronTaskPersistence {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
get(workspaceId: string, taskId: string): Promise<CronTask | undefined>;
|
||||
list(query: CronTaskQuery): Promise<readonly CronTask[]>;
|
||||
save(workspaceId: string, task: CronTask): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export interface DomainEvent {
|
|||
|
||||
export interface IEventService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly onDidPublish: Event<DomainEvent>;
|
||||
publish(event: DomainEvent): void;
|
||||
subscribe(handler: (event: DomainEvent) => void): IDisposable;
|
||||
|
|
|
|||
|
|
@ -35,9 +35,7 @@ export interface IFileService {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
save(source: Readable, filename: string, options?: SaveOptions): Promise<FileMeta>;
|
||||
|
||||
get(fileId: string): Promise<GetResult>;
|
||||
|
||||
delete(fileId: string): Promise<void>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export interface ExperimentalFeatureState {
|
|||
|
||||
export interface IFlagService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly registry: IFlagRegistry;
|
||||
enabled(id: FlagId): boolean;
|
||||
snapshot(): ExperimentalFlagMap;
|
||||
|
|
|
|||
|
|
@ -38,9 +38,7 @@ export interface IFlagRegistry {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
register(definition: FlagDefinitionInput): IDisposable;
|
||||
|
||||
get(id: FlagId): FlagDefinitionInput | undefined;
|
||||
|
||||
list(): readonly FlagDefinitionInput[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiatio
|
|||
|
||||
export interface IRestGateway {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
prompt(
|
||||
sessionId: string,
|
||||
agentId: string,
|
||||
|
|
@ -32,6 +33,7 @@ export const IRestGateway: ServiceIdentifier<IRestGateway> =
|
|||
|
||||
export interface IWSGateway {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
connect(connectionId: string): void;
|
||||
broadcast(sessionId: string, event: unknown): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ export interface IGitService {
|
|||
* tree or git itself fails.
|
||||
*/
|
||||
status(cwd: string, pathFilter?: ReadonlySet<string>): Promise<FsGitStatusResponse>;
|
||||
|
||||
/**
|
||||
* `git diff HEAD -- <relPath>` for the repo at `cwd`. `relPath` is the
|
||||
* repo-relative posix path passed to git; `absPath` is the confined absolute
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ export interface IGlobalSkillCatalog {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly catalog: SkillCatalog;
|
||||
|
||||
load(): Promise<void>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ export interface ISkillDiscovery {
|
|||
workDir: string,
|
||||
extraRoots?: readonly SkillRoot[],
|
||||
): Promise<SkillDiscoveryResult>;
|
||||
|
||||
discoverUser(homeDir: string, osHomeDir: string): Promise<SkillDiscoveryResult>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ export interface IHostFolderBrowser {
|
|||
* target.
|
||||
*/
|
||||
browse(absPath?: string): Promise<FsBrowseResponse>;
|
||||
|
||||
/** `$HOME` plus the most recently opened workspace roots. */
|
||||
home(): Promise<FsHomeResponse>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface LogEntry {
|
|||
|
||||
export interface ILogWriterService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
write(entry: LogEntry): void;
|
||||
flush?(): Promise<void>;
|
||||
close?(): Promise<void>;
|
||||
|
|
@ -49,6 +50,7 @@ export interface ILogger {
|
|||
|
||||
export interface ILogService extends ILogger {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly level: LogLevel;
|
||||
setLevel(level: LogLevel): void;
|
||||
flush(): Promise<void>;
|
||||
|
|
@ -72,6 +74,7 @@ export function levelEnabled(level: LogLevel, configured: LogLevel): boolean {
|
|||
|
||||
export interface ISessionLogService extends ILogger {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
flush(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ export interface IMessageLegacyService {
|
|||
* Throws `session.not_found` when `sid` is unknown.
|
||||
*/
|
||||
list(sessionId: string, query: MessageListQuery): Promise<PageResponse<Message>>;
|
||||
|
||||
/**
|
||||
* `GET /sessions/{sid}/messages/{mid}` — single message by id.
|
||||
* Throws `session.not_found` when `sid` is unknown, `message.not_found` when
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ export interface ModelsChangedEvent {
|
|||
|
||||
export interface IModelService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly onDidChangeModels: Event<ModelsChangedEvent>;
|
||||
get(id: string): ModelConfig | undefined;
|
||||
list(): Readonly<Record<string, ModelConfig>>;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import type { Model } from './modelInstance';
|
|||
|
||||
export interface IModelResolver {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/** Resolve a Model id into a runnable god-object Model instance. */
|
||||
resolve(id: string): Model;
|
||||
/** All Model ids whose `name` or `aliases` match the given routing key. */
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ export interface PlatformsChangedEvent {
|
|||
|
||||
export interface IPlatformService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly onDidChangePlatforms: Event<PlatformsChangedEvent>;
|
||||
get(name: string): PlatformConfig | undefined;
|
||||
list(): Readonly<Record<string, PlatformConfig>>;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export interface GetPluginInfoInput {
|
|||
|
||||
export interface IPluginService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
listPlugins(): Promise<readonly PluginSummary[]>;
|
||||
installPlugin(input: InstallPluginInput): Promise<PluginSummary>;
|
||||
setPluginEnabled(input: SetPluginEnabledInput): Promise<void>;
|
||||
|
|
@ -47,7 +48,6 @@ export interface IPluginService {
|
|||
getPluginInfo(input: GetPluginInfoInput): Promise<PluginInfo | undefined>;
|
||||
listPluginCommands(): Promise<readonly PluginCommandDef[]>;
|
||||
checkUpdates(): Promise<readonly PluginUpdateStatus[]>;
|
||||
|
||||
// --- consumption plane (loaded from enabled, error-free plugins) ---------
|
||||
|
||||
/** Skill roots contributed by enabled plugins (fed into skill discovery). */
|
||||
|
|
@ -58,7 +58,6 @@ export interface IPluginService {
|
|||
enabledMcpServers(): Promise<Record<string, McpServerConfig>>;
|
||||
/** Hooks contributed by enabled plugins (cwd + env already resolved). */
|
||||
enabledHooks(): Promise<readonly HookDef[]>;
|
||||
|
||||
/** Fires after a successful `reloadPlugins()` with the reload summary. */
|
||||
readonly onDidReload: Event<ReloadSummary>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export interface ProtocolAdapterConfig {
|
|||
|
||||
export interface IProtocolAdapterRegistry {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/** Protocols this registry can build adapters for. */
|
||||
supportedProtocols(): readonly Protocol[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ export interface ProvidersChangedEvent {
|
|||
|
||||
export interface IProviderService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly onDidChangeProviders: Event<ProvidersChangedEvent>;
|
||||
get(name: string): ProviderConfig | undefined;
|
||||
list(): Readonly<Record<string, ProviderConfig>>;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export interface SessionListQuery {
|
|||
|
||||
export interface ISessionIndex {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/** List persisted sessions, optionally filtered by workspace. */
|
||||
list(query: SessionListQuery): Promise<Page<SessionSummary>>;
|
||||
/** Fetch a single persisted session by id. */
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export interface SessionChildrenPage {
|
|||
|
||||
export interface ISessionLegacyService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
fork(sessionId: string, body: ForkSessionRequest): Promise<SessionWireFields>;
|
||||
createChild(sessionId: string, body: CreateSessionChildRequest): Promise<SessionWireFields>;
|
||||
listChildren(sessionId: string, query: SessionChildrenQuery): Promise<SessionChildrenPage>;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export interface SessionForkedEvent {
|
|||
|
||||
export interface ISessionLifecycleService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly onDidCreateSession: Event<SessionCreatedEvent>;
|
||||
readonly onDidCloseSession: Event<SessionClosedEvent>;
|
||||
readonly onDidArchiveSession: Event<SessionArchivedEvent>;
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ export interface ITaskService {
|
|||
* State: pending → running → completed | failed | cancelled.
|
||||
*/
|
||||
run<T>(fn: (signal: AbortSignal, output: (data: string) => void) => Promise<T>): ITaskHandle<T>;
|
||||
|
||||
/**
|
||||
* Create a passive task whose settlement is controlled by the caller
|
||||
* through the returned `resolve` / `reject` methods.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import type { TelemetryProperties } from './telemetry';
|
|||
|
||||
export interface IAgentTelemetryContextService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/** Current ambient telemetry properties for this agent. */
|
||||
get(): TelemetryProperties;
|
||||
/** Merge a patch into the ambient telemetry context. */
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export interface TelemetryServiceOptions {
|
|||
|
||||
export interface ITelemetryService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
track(event: string, properties?: TelemetryProperties): void;
|
||||
withContext(patch: TelemetryContextPatch): ITelemetryService;
|
||||
setContext(patch: TelemetryContextPatch): void;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export interface WebFetchServiceOptions {
|
|||
|
||||
export interface IWebFetchService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
getUrlFetcher(): UrlFetcher;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ export interface IWorkspacePersistence {
|
|||
* - `Workspace[]` (possibly empty) → a materialized catalog; do not rebuild.
|
||||
*/
|
||||
load(): Promise<Workspace[] | undefined>;
|
||||
|
||||
/** Atomically replace the persisted catalog. */
|
||||
save(workspaces: readonly Workspace[]): Promise<void>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ export interface IHostEnvironment {
|
|||
readonly pathClass: PathClass;
|
||||
/** Absolute path of the current user's home directory (`os.homedir()`). */
|
||||
readonly homeDir: string;
|
||||
|
||||
/**
|
||||
* Resolves once the probe has completed. Every field above is populated by
|
||||
* the time this promise settles. The composition root awaits this before
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ export interface IHostProcess {
|
|||
readonly stdin: Writable;
|
||||
readonly stdout: Readable;
|
||||
readonly stderr: Readable;
|
||||
|
||||
/** Wait for the process to exit and return its exit code. */
|
||||
wait(): Promise<number>;
|
||||
/** Kill the process tree (not just the direct child) with the given signal. */
|
||||
|
|
|
|||
|
|
@ -34,15 +34,10 @@ export interface IAppendLogStore {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
append<R>(scope: string, key: string, record: R, options?: AppendLogOptions): void;
|
||||
|
||||
read<R>(scope: string, key: string): AsyncIterable<R>;
|
||||
|
||||
rewrite<R>(scope: string, key: string, records: readonly R[]): Promise<void>;
|
||||
|
||||
flush(): Promise<void>;
|
||||
|
||||
close(): Promise<void>;
|
||||
|
||||
acquire(scope: string, key: string): IDisposable;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,15 +22,10 @@ export interface IAtomicDocumentStore {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
get<T>(scope: string, key: string): Promise<T | undefined>;
|
||||
|
||||
set<T>(scope: string, key: string, value: T): Promise<void>;
|
||||
|
||||
delete(scope: string, key: string): Promise<void>;
|
||||
|
||||
list(scope: string, prefix?: string): Promise<readonly string[]>;
|
||||
|
||||
watch(scope: string, key: string): Event<void>;
|
||||
|
||||
acquire(scope: string, key: string): IDisposable;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,15 +15,10 @@ export interface IBlobStore {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
put(scope: string, key: string, data: Uint8Array): Promise<void>;
|
||||
|
||||
get(scope: string, key: string): Promise<Uint8Array | undefined>;
|
||||
|
||||
getStream(scope: string, key: string): AsyncIterable<Uint8Array>;
|
||||
|
||||
has(scope: string, key: string): Promise<boolean>;
|
||||
|
||||
delete(scope: string, key: string): Promise<void>;
|
||||
|
||||
list(scope: string, prefix?: string): Promise<readonly string[]>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,21 +83,13 @@ export interface IQueryStore {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
put<T>(collection: string, key: string, value: T): Promise<void>;
|
||||
|
||||
batch(ops: readonly WriteOp[]): Promise<void>;
|
||||
|
||||
delete(collection: string, key: string): Promise<void>;
|
||||
|
||||
get<T>(collection: string, key: string): Promise<T | undefined>;
|
||||
|
||||
query<T>(collection: string): IQuery<T>;
|
||||
|
||||
ensureIndex(collection: string, def: IndexDef): Promise<void>;
|
||||
|
||||
getCheckpoint(source: string): Promise<Checkpoint | undefined>;
|
||||
|
||||
setCheckpoint(source: string, checkpoint: Checkpoint): Promise<void>;
|
||||
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,21 +41,13 @@ export interface IFileSystemStorageService {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
read(scope: string, key: string): Promise<Uint8Array | undefined>;
|
||||
|
||||
readStream(scope: string, key: string): AsyncIterable<Uint8Array>;
|
||||
|
||||
write(scope: string, key: string, data: Uint8Array, options?: StorageWriteOptions): Promise<void>;
|
||||
|
||||
append(scope: string, key: string, data: Uint8Array, options?: StorageAppendOptions): Promise<void>;
|
||||
|
||||
list(scope: string, prefix?: string): Promise<readonly string[]>;
|
||||
|
||||
delete(scope: string, key: string): Promise<void>;
|
||||
|
||||
watch?(scope: string, key: string): Event<void>;
|
||||
|
||||
flush(): Promise<void>;
|
||||
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ export interface AgentListFilter {
|
|||
|
||||
export interface IAgentLifecycleService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/** Fires after an agent is created and registered, with its scope handle. */
|
||||
readonly onDidCreate: Event<IAgentScopeHandle>;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export interface ApprovalResponse {
|
|||
|
||||
export interface ISessionApprovalService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
request(req: ApprovalRequest): Promise<ApprovalResponse>;
|
||||
/**
|
||||
* Submit an approval request without blocking on the decision. Returns the
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ IMPORTANT:
|
|||
|
||||
export interface ISessionBtwService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/**
|
||||
* Fork the main agent into a side-question child agent (tools disabled,
|
||||
* side-channel reminder appended) and return the child's id.
|
||||
|
|
|
|||
|
|
@ -20,18 +20,16 @@ export interface CronLoadOptions {
|
|||
|
||||
export interface ISessionCronService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly isEnabled: boolean;
|
||||
|
||||
readonly isEnabled: boolean;
|
||||
addTask(init: CronTaskInit): CronTask;
|
||||
removeTasks(ids: readonly string[]): readonly string[];
|
||||
getTask(id: string): CronTask | undefined;
|
||||
list(): readonly CronTask[];
|
||||
|
||||
now(): number;
|
||||
isStale(task: CronTask): boolean;
|
||||
getNextFireTime(): number | null;
|
||||
getNextFireForTask(taskId: string): number | null;
|
||||
|
||||
loadFromStore(options?: CronLoadOptions): Promise<void>;
|
||||
start(): void;
|
||||
stop(): Promise<void>;
|
||||
|
|
@ -41,7 +39,6 @@ export interface ISessionCronService {
|
|||
tasks: readonly CronTask[],
|
||||
renderMissedNotification: (tasks: readonly CronTask[]) => readonly ContentPart[],
|
||||
): Turn | undefined;
|
||||
|
||||
emitScheduled(task: CronTask): void;
|
||||
emitDeleted(taskId: string): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,15 +26,12 @@ export interface IExecContext {
|
|||
|
||||
/** Absolute path to the session's working directory. */
|
||||
readonly cwd: string;
|
||||
|
||||
/** Ordered list of env overlays applied on top of `process.env` when
|
||||
* spawning a process. Later layers win. */
|
||||
readonly envLayers: readonly Record<string, string>[];
|
||||
|
||||
/** Return a new `IExecContext` rooted at `cwd`, keeping the same env
|
||||
* layers. Does not mutate this context. */
|
||||
withCwd(cwd: string): IExecContext;
|
||||
|
||||
/** Return a new `IExecContext` with `env` appended to `envLayers`. Does
|
||||
* not mutate this context. */
|
||||
withEnv(env: Record<string, string>): IExecContext;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export interface InteractionPendingChangedEvent {
|
|||
|
||||
export interface ISessionInteractionService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
request<TPayload, TResponse>(req: InteractionRequest<TPayload>): Promise<TResponse>;
|
||||
/**
|
||||
* Park a request without blocking on its response. Returns the created
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export interface QuestionRequest {
|
|||
|
||||
export interface ISessionQuestionService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
request(req: QuestionRequest): Promise<QuestionResult>;
|
||||
/**
|
||||
* Post a question without blocking on the answer. Returns the request with
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export type { SessionWarning };
|
|||
|
||||
export interface ISessionWarningService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/**
|
||||
* Compute the current session-level warnings. Recomputes the AGENTS.md size
|
||||
* warning on demand (preferring the main agent's cached value when the agent
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export type SessionStatus = 'running' | 'idle' | 'awaiting_approval' | 'awaiting
|
|||
|
||||
export interface ISessionActivity {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
status(): SessionStatus;
|
||||
isIdle(): boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import type { ScopeSeed } from '#/_base/di/scope';
|
|||
|
||||
export interface ISessionContext {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly sessionId: string;
|
||||
readonly workspaceId: string;
|
||||
readonly sessionDir: string;
|
||||
readonly metaScope: string;
|
||||
|
||||
/**
|
||||
* Persistence scope rooted at this session. `scope()` returns the session
|
||||
* scope itself; `scope(subKey)` returns `${sessionScope}/${subKey}`. The
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export interface SessionMetadataChangedEvent {
|
|||
|
||||
export interface ISessionMetadata {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly ready: Promise<void>;
|
||||
readonly onDidChangeMetadata: Event<SessionMetadataChangedEvent>;
|
||||
read(): Promise<SessionMeta>;
|
||||
|
|
|
|||
|
|
@ -15,11 +15,8 @@ export interface ISessionSkillCatalog {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly catalog: SkillCatalog;
|
||||
|
||||
readonly ready: Promise<void>;
|
||||
|
||||
load(): Promise<void>;
|
||||
|
||||
reload(): Promise<void>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export interface SessionSwarmRunResult<T = unknown> {
|
|||
|
||||
export interface ISessionSwarmService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
run<T>(args: SessionSwarmRunArgs<T>): Promise<readonly SessionSwarmRunResult<T>[]>;
|
||||
cancel(args: { readonly callerAgentId: string }): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,25 +46,17 @@ export interface ISessionTerminalService {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
create(input: CreateTerminalRequest): Promise<Terminal>;
|
||||
|
||||
list(): Promise<readonly Terminal[]>;
|
||||
|
||||
get(terminalId: string): Promise<Terminal>;
|
||||
|
||||
attach(
|
||||
terminalId: string,
|
||||
sink: TerminalAttachSink,
|
||||
options?: TerminalAttachOptions,
|
||||
): Promise<{ replayed: number }>;
|
||||
|
||||
detach(terminalId: string, sinkId: string): void;
|
||||
|
||||
detachAllForSink(sinkId: string): void;
|
||||
|
||||
write(terminalId: string, data: string): Promise<void>;
|
||||
|
||||
resize(terminalId: string, cols: number, rows: number): Promise<void>;
|
||||
|
||||
close(terminalId: string): Promise<{ closed: true }>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ export interface ISessionWorkspaceContext {
|
|||
|
||||
readonly workDir: string;
|
||||
readonly additionalDirs: readonly string[];
|
||||
|
||||
setWorkDir(workDir: string): void;
|
||||
resolve(rel: string): string;
|
||||
isWithin(absPath: string): boolean;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ describe('Agent loop', () => {
|
|||
const controller = new AbortController();
|
||||
controller.abort(new Error('stop'));
|
||||
|
||||
const result = await loop.runTurn(0, { signal: controller.signal });
|
||||
const result = await loop.run({ turnId: 0, signal: controller.signal });
|
||||
|
||||
expect(result.reason).toBe('cancelled');
|
||||
expect(called).toBe(false);
|
||||
|
|
@ -150,7 +150,7 @@ describe('Agent loop', () => {
|
|||
throw recoveryError;
|
||||
});
|
||||
|
||||
const result = await loop.runTurn(0);
|
||||
const result = await loop.run({ turnId: 0 });
|
||||
|
||||
expect(result.reason).toBe('failed');
|
||||
if (result.reason === 'failed') {
|
||||
|
|
|
|||
|
|
@ -87,7 +87,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;
|
||||
}> = [];
|
||||
|
|
@ -125,7 +125,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;
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ describe('task notification → main agent (real Agent instance)', () => {
|
|||
|
||||
it('RACE: bg completion fires AFTER LLM returns but BEFORE activeTurn is cleared', async () => {
|
||||
// We're hunting a window: shouldContinueAfterStop reads an empty
|
||||
// steerBuffer → returns { continue: false } → runTurn unwinds →
|
||||
// steerBuffer → returns { continue: false } → loop.run unwinds →
|
||||
// finally block hasn't yet set activeTurn = null. If a steer()
|
||||
// lands in this window, it gets buffered, then activeTurn=null
|
||||
// and the buffer is never flushed until the next user prompt.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -99,8 +99,8 @@ export function stubLoopWithHooks(): IAgentLoopService {
|
|||
return {
|
||||
_serviceBrand: undefined,
|
||||
hooks,
|
||||
runTurn: async (_turnId, options) => {
|
||||
options?.onStepStarted?.(1);
|
||||
run: async (options) => {
|
||||
options.onStarted?.(1);
|
||||
return { reason: 'completed', steps: 0 };
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 { IAgentContextOpsService, AgentContextOpsService } from '#/agent/contextOps';
|
||||
import { AgentLoopService, IAgentLoopService } from '#/agent/loop';
|
||||
|
|
@ -22,8 +21,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;
|
||||
|
|
@ -65,19 +62,19 @@ describe('AgentTurnService ready', () => {
|
|||
events.push('after');
|
||||
beforeStepDone.resolve();
|
||||
});
|
||||
loop.runTurn = async (turnId, options) => {
|
||||
loop.run = async (options) => {
|
||||
events.push('loop');
|
||||
const signal = options?.signal ?? new AbortController().signal;
|
||||
const { turnId, signal = new AbortController().signal } = options;
|
||||
await loop.hooks.beforeStep.run({ turnId, step: 1, signal });
|
||||
await responseEvent;
|
||||
events.push('response');
|
||||
options?.onStepStarted?.(1);
|
||||
options.onStarted?.(1);
|
||||
await release;
|
||||
events.push('done');
|
||||
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,9 +100,9 @@ describe('AgentTurnService ready', () => {
|
|||
|
||||
it('rejects with an Error when the turn ends before the first step starts', async () => {
|
||||
const cause = new Error('loop failed before first step');
|
||||
loop.runTurn = async () => ({ reason: 'failed', error: cause, steps: 0 });
|
||||
loop.run = 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;
|
||||
|
|
@ -119,16 +116,16 @@ describe('AgentTurnService ready', () => {
|
|||
|
||||
it('throws a KimiError when launching while a turn is active', async () => {
|
||||
const release = createControlledPromise<void>();
|
||||
loop.runTurn = async () => {
|
||||
loop.run = async () => {
|
||||
await release;
|
||||
return { reason: 'completed', steps: 1 };
|
||||
};
|
||||
|
||||
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 {
|
||||
|
|
@ -144,7 +141,7 @@ describe('AgentTurnService ready', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('AgentLoopService onStepStarted', () => {
|
||||
describe('AgentLoopService onStarted', () => {
|
||||
let disposables: DisposableStore;
|
||||
let ix: TestInstantiationService;
|
||||
|
||||
|
|
@ -195,8 +192,9 @@ describe('AgentLoopService onStepStarted', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const result = ix.get(IAgentLoopService).runTurn(1, {
|
||||
onStepStarted: (step) => {
|
||||
const result = ix.get(IAgentLoopService).run({
|
||||
turnId: 1,
|
||||
onStarted: (step) => {
|
||||
expect(step).toBe(1);
|
||||
started = true;
|
||||
stepStarted.resolve();
|
||||
|
|
@ -238,8 +236,9 @@ describe('AgentLoopService onStepStarted', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const result = ix.get(IAgentLoopService).runTurn(1, {
|
||||
onStepStarted: (step) => {
|
||||
const result = ix.get(IAgentLoopService).run({
|
||||
turnId: 1,
|
||||
onStarted: (step) => {
|
||||
expect(step).toBe(1);
|
||||
started = true;
|
||||
stepStarted.resolve();
|
||||
|
|
@ -268,8 +267,9 @@ describe('AgentLoopService onStepStarted', () => {
|
|||
});
|
||||
|
||||
await expect(
|
||||
ix.get(IAgentLoopService).runTurn(1, {
|
||||
onStepStarted: () => {
|
||||
ix.get(IAgentLoopService).run({
|
||||
turnId: 1,
|
||||
onStarted: () => {
|
||||
started = true;
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -469,7 +469,6 @@ export interface WarningEvent {
|
|||
export interface TurnStartedEvent {
|
||||
readonly type: 'turn.started';
|
||||
readonly turnId: number;
|
||||
readonly origin: PromptOrigin;
|
||||
}
|
||||
|
||||
export interface TurnEndedEvent {
|
||||
|
|
|
|||
|
|
@ -16,15 +16,22 @@
|
|||
import {
|
||||
IAgentContextMemoryService,
|
||||
IAgentLifecycleService,
|
||||
IAgentPromptLegacyService,
|
||||
ISessionInteractionService,
|
||||
ISessionContext,
|
||||
ISessionLifecycleService,
|
||||
ISessionMetadata,
|
||||
IWorkspaceRegistry,
|
||||
toProtocolMessage,
|
||||
type IAgentScopeHandle,
|
||||
type Scope,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
import { ErrorCode, sessionSnapshotResponseSchema, type Message } from '@moonshot-ai/protocol';
|
||||
import {
|
||||
ErrorCode,
|
||||
sessionSnapshotResponseSchema,
|
||||
type InFlightTurn,
|
||||
type Message,
|
||||
} from '@moonshot-ai/protocol';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { errEnvelope, okEnvelope } from '../envelope';
|
||||
|
|
@ -111,6 +118,14 @@ export function registerSnapshotRoutes(app: SnapshotRouteHost, deps: SnapshotRou
|
|||
const offset = history.length - page.length;
|
||||
items = page.map((msg, i) => toProtocolMessage(session_id, offset + i, msg, meta.createdAt));
|
||||
}
|
||||
const currentPromptId =
|
||||
snapState.inFlightTurn === null
|
||||
? undefined
|
||||
: readCurrentPromptId(main);
|
||||
const inFlightTurn = attachCurrentPromptIdToInFlight(
|
||||
snapState.inFlightTurn,
|
||||
currentPromptId,
|
||||
);
|
||||
|
||||
// Pending approvals / questions.
|
||||
const interaction = handle.accessor.get(ISessionInteractionService);
|
||||
|
|
@ -128,7 +143,7 @@ export function registerSnapshotRoutes(app: SnapshotRouteHost, deps: SnapshotRou
|
|||
epoch: snapState.epoch,
|
||||
session,
|
||||
messages: { items, has_more: hasMore },
|
||||
in_flight_turn: snapState.inFlightTurn,
|
||||
in_flight_turn: inFlightTurn,
|
||||
pending_approvals: pendingApprovals,
|
||||
pending_questions: pendingQuestions,
|
||||
},
|
||||
|
|
@ -139,3 +154,21 @@ export function registerSnapshotRoutes(app: SnapshotRouteHost, deps: SnapshotRou
|
|||
);
|
||||
app.get(route.path, route.options, route.handler as Parameters<SnapshotRouteHost['get']>[2]);
|
||||
}
|
||||
|
||||
function readCurrentPromptId(main: IAgentScopeHandle | undefined): string | undefined {
|
||||
if (main === undefined) return undefined;
|
||||
try {
|
||||
return main.accessor.get(IAgentPromptLegacyService).list().active?.prompt_id;
|
||||
} catch {
|
||||
// Auxiliary reconnect metadata must not make the whole snapshot fail.
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function attachCurrentPromptIdToInFlight(
|
||||
inFlightTurn: InFlightTurn | null,
|
||||
currentPromptId: string | undefined,
|
||||
): InFlightTurn | null {
|
||||
if (inFlightTurn === null || currentPromptId === undefined) return inFlightTurn;
|
||||
return { ...inFlightTurn, current_prompt_id: currentPromptId };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,15 +11,121 @@ import {
|
|||
IAgentContextMemoryService,
|
||||
IAgentEventSinkService,
|
||||
IAgentLifecycleService,
|
||||
IAgentPromptLegacyService,
|
||||
ISessionInteractionService,
|
||||
ISessionContext,
|
||||
ISessionLifecycleService,
|
||||
ISessionMetadata,
|
||||
IWorkspaceRegistry,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
import { sessionSnapshotResponseSchema, type AgentEvent } from '@moonshot-ai/protocol';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { registerSnapshotRoutes } from '../src/routes/snapshot';
|
||||
import { type RunningServer, startServer } from '../src/start';
|
||||
import { authHeaders } from './helpers/auth';
|
||||
|
||||
function fakeAccessor(entries: ReadonlyArray<readonly [unknown, unknown]>) {
|
||||
const services = new Map<unknown, unknown>(entries);
|
||||
return {
|
||||
get<T>(id: unknown): T {
|
||||
if (!services.has(id)) {
|
||||
throw new Error(`unexpected service request: ${String(id)}`);
|
||||
}
|
||||
return services.get(id) as T;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('server-v2 snapshot route enrichment', () => {
|
||||
it('attaches current_prompt_id to an in-flight turn from promptLegacy active state', async () => {
|
||||
const sessionId = 'sess_snapshot';
|
||||
const promptId = 'msg_snapshot_prompt';
|
||||
const workspaceId = 'wd_snapshot_012345abcdef';
|
||||
const now = Date.parse('2026-01-01T00:00:00.000Z');
|
||||
const main = {
|
||||
accessor: fakeAccessor([
|
||||
[IAgentContextMemoryService, { get: () => [] }],
|
||||
[
|
||||
IAgentPromptLegacyService,
|
||||
{ list: () => ({ active: { prompt_id: promptId }, queued: [] }) },
|
||||
],
|
||||
]),
|
||||
};
|
||||
const session = {
|
||||
accessor: fakeAccessor([
|
||||
[ISessionContext, { workspaceId }],
|
||||
[
|
||||
ISessionMetadata,
|
||||
{
|
||||
read: async () => ({
|
||||
id: sessionId,
|
||||
title: 'Snapshot',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
archived: false,
|
||||
}),
|
||||
},
|
||||
],
|
||||
[IAgentLifecycleService, { getHandle: () => main }],
|
||||
[ISessionInteractionService, { listPending: () => [] }],
|
||||
]),
|
||||
};
|
||||
const core = {
|
||||
accessor: fakeAccessor([
|
||||
[ISessionLifecycleService, { resume: async () => session }],
|
||||
[IWorkspaceRegistry, { get: async () => ({ root: '/workspace' }) }],
|
||||
]),
|
||||
};
|
||||
const broadcaster = {
|
||||
getSnapshotState: async () => ({
|
||||
seq: 1,
|
||||
epoch: 'ep_snapshot',
|
||||
inFlightTurn: {
|
||||
turn_id: 7,
|
||||
assistant_text: 'Hello',
|
||||
thinking_text: '',
|
||||
running_tools: [],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
let routeHandler:
|
||||
| ((
|
||||
req: { id: string; params: { session_id: string } },
|
||||
reply: { send(payload: unknown): unknown },
|
||||
) => Promise<void> | void)
|
||||
| undefined;
|
||||
registerSnapshotRoutes(
|
||||
{
|
||||
get: (_path, _options, handler) => {
|
||||
routeHandler = handler;
|
||||
},
|
||||
},
|
||||
{ core: core as never, broadcaster: broadcaster as never },
|
||||
);
|
||||
|
||||
let payload: unknown;
|
||||
await routeHandler?.(
|
||||
{ id: 'req_snapshot', params: { session_id: sessionId } },
|
||||
{
|
||||
send: (value) => {
|
||||
payload = value;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const body = payload as { code: number; data: unknown };
|
||||
expect(body.code).toBe(0);
|
||||
const snap = sessionSnapshotResponseSchema.parse(body.data);
|
||||
expect(snap.in_flight_turn).toMatchObject({
|
||||
turn_id: 7,
|
||||
assistant_text: 'Hello',
|
||||
current_prompt_id: promptId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('server-v2 GET /api/v1/sessions/:id/snapshot', () => {
|
||||
let server: RunningServer | undefined;
|
||||
let home: string | undefined;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue