mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
Merge branch 'kimi-code-v2' of https://github.com/MoonshotAI/kimi-code into kimi-code-v2
This commit is contained in:
commit
87faa7897b
92 changed files with 1322 additions and 1109 deletions
|
|
@ -102,7 +102,6 @@ package "Agent scope (per agent)" #FDF5E6 {
|
|||
rectangle "<b>rpc</b>\n<size:9><i>Agent</i></size>\n IAgentRPCService" as rpc #FDEBD0
|
||||
rectangle "<b>fileTools</b>\n<size:9><i>Agent</i></size>\n IAgentFileToolsService" as fileTools #FDEBD0
|
||||
rectangle "<b>shellTools</b>\n<size:9><i>Agent</i></size>\n IAgentShellToolsService" as shellTools #FDEBD0
|
||||
rectangle "<b>agentTool</b>\n<size:9><i>Agent</i></size>\n IAgentToolService" as agentTool #FDEBD0
|
||||
rectangle "<b>scopeContext</b>\n<size:9><i>Agent</i></size>\n IAgentScopeContext (seed)" as scopeContext #FDEBD0
|
||||
}
|
||||
|
||||
|
|
@ -367,7 +366,7 @@ externalHooks ..> turn #16A085 : prompt/end hooks
|
|||
externalHooks ..> loop #16A085 : stop hook
|
||||
externalHooks ..> fullCompaction #16A085 : compaction hooks
|
||||
externalHooks ..> task #16A085 : notification hook
|
||||
externalHooks ..> agentTool #16A085 : subagent hooks
|
||||
agent_lifecycle ..> externalHooks #16A085 : SubagentStart/Stop (mirrorAgentRun)
|
||||
toolStore ..> wireRecord #16A085 : tools.update_store
|
||||
usage ..> wireRecord #16A085 : usage.record
|
||||
contextSize ..> contextMemory #16A085 : hooks.onSpliced
|
||||
|
|
|
|||
|
|
@ -147,7 +147,6 @@ const DOMAIN_LAYER = new Map([
|
|||
['microCompaction', 4],
|
||||
['loop', 4],
|
||||
['media', 4],
|
||||
['shellTools', 4],
|
||||
['llmRequester', 4],
|
||||
['profile', 4],
|
||||
['prompt', 4],
|
||||
|
|
@ -158,8 +157,6 @@ const DOMAIN_LAYER = new Map([
|
|||
['agentTask', 5],
|
||||
['mcp', 5],
|
||||
['cron', 5],
|
||||
['cronPersistence', 5],
|
||||
['agentTool', 5],
|
||||
['externalHooks', 5],
|
||||
// `btw` forks a single side-question sub-agent via `agentLifecycle`,
|
||||
// parallel to how the `Agent` tool spawns child agents. Agent-scope, L5.
|
||||
|
|
@ -302,7 +299,6 @@ const ALLOWED_EXCEPTIONS = new Set([
|
|||
'replayBuilder>agentTask',
|
||||
'replayBuilder>rpc',
|
||||
'replayBuilder>sessionMetadata',
|
||||
'shellTools>agentTask',
|
||||
'skill>contextMemory',
|
||||
'skill>prompt',
|
||||
'swarm>sessionMetadata',
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
/**
|
||||
* `agentTool` domain (L5) — `IAgentToolService` implementation.
|
||||
*
|
||||
* Owns the Agent-scoped hook slots for child-agent start/stop lifecycle events.
|
||||
* The actual `Agent` tool is registered by `agentLifecycle` as a builtin tool;
|
||||
* this service remains as the observer boundary consumed by `externalHooks`.
|
||||
* Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { Disposable } from '#/_base/di';
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { OrderedHookSlot } from '#/hooks';
|
||||
|
||||
import {
|
||||
IAgentToolService,
|
||||
type AgentToolDidRunSubagentContext,
|
||||
type AgentToolWillRunSubagentContext,
|
||||
} from './agentToolServiceToken';
|
||||
|
||||
export class AgentToolService extends Disposable implements IAgentToolService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
readonly hooks: IAgentToolService['hooks'] = {
|
||||
onWillRunSubagent: new OrderedHookSlot<AgentToolWillRunSubagentContext>(),
|
||||
onDidRunSubagent: new OrderedHookSlot<AgentToolDidRunSubagentContext>(),
|
||||
};
|
||||
}
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Agent,
|
||||
IAgentToolService,
|
||||
AgentToolService,
|
||||
InstantiationType.Eager,
|
||||
'agentTool',
|
||||
);
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
/**
|
||||
* `agentTool` domain (L5) — hook service for the `Agent` collaboration tool.
|
||||
*
|
||||
* Exposes the Agent-scoped subagent lifecycle hooks that the `Agent` tool and
|
||||
* swarm scheduler emit, and that observer services such as `externalHooks`
|
||||
* consume. Bound at Agent scope.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
import type { Hooks } from '#/hooks';
|
||||
|
||||
export interface AgentToolWillRunSubagentContext {
|
||||
readonly agentName: string;
|
||||
readonly prompt: string;
|
||||
readonly signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface AgentToolDidRunSubagentContext {
|
||||
readonly agentName: string;
|
||||
readonly response: string;
|
||||
}
|
||||
|
||||
export interface IAgentToolService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly hooks: Hooks<{
|
||||
onWillRunSubagent: AgentToolWillRunSubagentContext;
|
||||
onDidRunSubagent: AgentToolDidRunSubagentContext;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const IAgentToolService: ServiceIdentifier<IAgentToolService> =
|
||||
createDecorator<IAgentToolService>('agentToolService');
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
/**
|
||||
* `agentTool` domain barrel — re-exports the subagent lifecycle hook service
|
||||
* contract (`agentToolServiceToken`) and its scoped service
|
||||
* (`agentToolService`). Importing this barrel registers the
|
||||
* `IAgentToolService` binding into the scope registry.
|
||||
*/
|
||||
|
||||
export * from './agentToolServiceToken';
|
||||
export * from './agentToolService';
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* consumers that detect chat-history injections.
|
||||
*
|
||||
* `agent_id` is emitted only for task notifications whose
|
||||
* source task is an agent subagent — surfacing it structurally lets the
|
||||
* source task is an agent run — surfacing it structurally lets the
|
||||
* LLM identify the correct id to pass to `Agent(resume=...)` without
|
||||
* having to grep the body or the original spawn-success ToolResult.
|
||||
* It is intentionally a separate attribute from `source_id`: the two
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
/**
|
||||
* `cron` domain barrel — re-exports cron utilities (expression parser, jitter,
|
||||
* format, clock, config) and registers the three cron tools (`CronCreate` /
|
||||
* `CronList` / `CronDelete`) via side-effect imports. The cron task record
|
||||
* type lives in `app/cronPersistence`; the scheduling engine lives in `session/cron`.
|
||||
*/
|
||||
|
||||
import './configSection';
|
||||
import './tools/cron-create';
|
||||
import './tools/cron-delete';
|
||||
import './tools/cron-list';
|
||||
|
||||
export * from './cron-expr';
|
||||
export * from './format';
|
||||
export * from './jitter';
|
||||
export * from './clock';
|
||||
export { CRON_SECTION, type CronConfig, DEFAULT_CRON_CONFIG } from './configSection';
|
||||
|
|
@ -22,8 +22,30 @@ export interface ExternalHooksServiceOptions {
|
|||
| undefined;
|
||||
}
|
||||
|
||||
export interface AgentTaskStartHookContext {
|
||||
readonly agentName: string;
|
||||
readonly prompt: string;
|
||||
readonly signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface AgentTaskStopHookContext {
|
||||
readonly agentName: string;
|
||||
readonly response: string;
|
||||
}
|
||||
|
||||
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
|
||||
* `agentLifecycle` tool wrapper (`mirrorAgentRun`) — the wrapper is a thin
|
||||
* layer over the lifecycle with no hook service of its own, so this is the
|
||||
* one context invoked explicitly rather than observed. Throws when a
|
||||
* configured hook blocks.
|
||||
*/
|
||||
runAgentTaskStart(ctx: AgentTaskStartHookContext): Promise<void>;
|
||||
/** Fire-and-forget `SubagentStop` external hook counterpart. */
|
||||
notifyAgentTaskStop(ctx: AgentTaskStopHookContext): void;
|
||||
}
|
||||
|
||||
export const IAgentExternalHooksService =
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@
|
|||
* hook commands.
|
||||
*
|
||||
* Listens to hook slots owned by the agent behavior/lifecycle domains
|
||||
* (`toolExecutor`, `permissionGate`, `turn`, `loop`, `fullCompaction`,
|
||||
* `task`, and `agentTool`) and translates those minimal contexts into
|
||||
* the configured external HookEngine events.
|
||||
* (`toolExecutor`, `permissionGate`, `turn`, `loop`, `fullCompaction`, and
|
||||
* `task`) and translates those minimal contexts into the configured external
|
||||
* HookEngine events. 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.
|
||||
*/
|
||||
|
||||
import { Disposable, IInstantiationService } from '#/_base/di';
|
||||
|
|
@ -13,11 +16,6 @@ import { InstantiationType } from '#/_base/di/extensions';
|
|||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { isUserCancellation } from '#/_base/utils/abort';
|
||||
import { isPlainRecord } from '#/_base/utils/canonical-args';
|
||||
import type {
|
||||
AgentToolDidRunSubagentContext,
|
||||
AgentToolWillRunSubagentContext,
|
||||
} from '#/agent/agentTool';
|
||||
import { IAgentToolService } from '#/agent/agentTool';
|
||||
import { IAgentTaskService, type AgentTaskNotificationContext } from '#/agent/task';
|
||||
import {
|
||||
IAgentFullCompactionService,
|
||||
|
|
@ -50,6 +48,8 @@ import { HOOKS_SECTION, type HookDefConfig } from './configSection';
|
|||
import { HookEngine } from './engine';
|
||||
import {
|
||||
IAgentExternalHooksService,
|
||||
type AgentTaskStartHookContext,
|
||||
type AgentTaskStopHookContext,
|
||||
type ExternalHooksServiceOptions,
|
||||
} from './externalHooks';
|
||||
import {
|
||||
|
|
@ -128,9 +128,6 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter
|
|||
this.instantiation.invokeFunction((accessor) => accessor.get(IAgentTaskService)),
|
||||
);
|
||||
|
||||
this.registerAgentToolHooks(
|
||||
this.instantiation.invokeFunction((accessor) => accessor.get(IAgentToolService)),
|
||||
);
|
||||
}
|
||||
|
||||
private registerToolHooks(toolExecutor: IAgentToolExecutorService): void {
|
||||
|
|
@ -237,21 +234,6 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter
|
|||
);
|
||||
}
|
||||
|
||||
private registerAgentToolHooks(agentTool: IAgentToolService): void {
|
||||
this._register(
|
||||
agentTool.hooks.onWillRunSubagent.register('externalHooks', async (ctx, next) => {
|
||||
await this.runSubagentStart(ctx);
|
||||
await next();
|
||||
}),
|
||||
);
|
||||
this._register(
|
||||
agentTool.hooks.onDidRunSubagent.register('externalHooks', async (ctx, next) => {
|
||||
this.notifySubagentStop(ctx);
|
||||
await next();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async loadDynamicHooks(): Promise<void> {
|
||||
await this.config.ready;
|
||||
const configured = this.config.get(HOOKS_SECTION) as readonly HookDefConfig[] | undefined;
|
||||
|
|
@ -390,7 +372,7 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter
|
|||
);
|
||||
}
|
||||
|
||||
private async runSubagentStart(ctx: AgentToolWillRunSubagentContext): Promise<void> {
|
||||
async runAgentTaskStart(ctx: AgentTaskStartHookContext): Promise<void> {
|
||||
ctx.signal.throwIfAborted();
|
||||
await this.engine()?.trigger('SubagentStart', {
|
||||
matcherValue: ctx.agentName,
|
||||
|
|
@ -403,7 +385,7 @@ export class AgentExternalHooksService extends Disposable implements IAgentExter
|
|||
ctx.signal.throwIfAborted();
|
||||
}
|
||||
|
||||
private notifySubagentStop(ctx: AgentToolDidRunSubagentContext): void {
|
||||
notifyAgentTaskStop(ctx: AgentTaskStopHookContext): void {
|
||||
void this.engine()?.fireAndForgetTrigger('SubagentStop', {
|
||||
matcherValue: ctx.agentName,
|
||||
inputData: {
|
||||
|
|
|
|||
|
|
@ -258,20 +258,25 @@ export class AgentPermissionGate extends Disposable implements IAgentPermissionG
|
|||
result.decision === 'cancelled'
|
||||
? `Tool "${toolName}" was not run because the approval request was cancelled.`
|
||||
: `Tool "${toolName}" was not run because the user rejected the approval request.`;
|
||||
if (this.isSubagent()) {
|
||||
if (this.usesWorkerRejectionGuidance()) {
|
||||
return `${prefix}${suffix} Try a different approach — don't retry the same call, don't attempt to bypass the restriction.`;
|
||||
}
|
||||
return `${prefix}${suffix}`;
|
||||
}
|
||||
|
||||
private formatDenyMessage(message: string): string {
|
||||
if (this.isSubagent()) {
|
||||
if (this.usesWorkerRejectionGuidance()) {
|
||||
return `${message} Try a different approach — don't retry the same call, don't attempt to bypass the restriction.`;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
private isSubagent(): boolean {
|
||||
/**
|
||||
* Rejection messages for agents driven by another agent (no user in the
|
||||
* loop) carry extra "don't retry / don't bypass" guidance. Heuristic: any
|
||||
* agent other than `main` is treated as worker-driven.
|
||||
*/
|
||||
private usesWorkerRejectionGuidance(): boolean {
|
||||
return this.options.agentId !== undefined && this.options.agentId !== 'main';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { AgentProfile, AgentProfileContext } from '#/app/agentProfileCatalog';
|
||||
import type { ModelCapability, ThinkingEffort } from '#/app/llmProtocol';
|
||||
import type { Model } from '#/app/model';
|
||||
|
||||
|
|
@ -28,27 +29,26 @@ export type AgentConfigUpdateData = Partial<{
|
|||
systemPrompt: string;
|
||||
}>;
|
||||
|
||||
export interface SystemPromptContext {
|
||||
readonly cwd?: string;
|
||||
/** 2-level tree listing of the working directory, for LLM orientation. */
|
||||
readonly cwdListing?: string;
|
||||
/** Concatenated AGENTS.md instruction hierarchy (user-level + project-level). */
|
||||
readonly agentsMd?: string;
|
||||
/** Rendered listings of additional workspace directories. */
|
||||
readonly additionalDirsInfo?: string;
|
||||
/**
|
||||
* Runtime context supplied to a profile's system-prompt renderer. Extends the
|
||||
* catalog's {@link AgentProfileContext} (host OS/shell, cwd, AGENTS.md, skills,
|
||||
* …) with the AGENTS.md size warning produced by `prepareSystemPromptContext`.
|
||||
*/
|
||||
export interface SystemPromptContext extends AgentProfileContext {
|
||||
/**
|
||||
* Present when the combined AGENTS.md content exceeds the recommended soft
|
||||
* budget. Surfaced through `getSessionWarnings` instead of truncating.
|
||||
*/
|
||||
readonly agentsMdWarning?: string;
|
||||
readonly [key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ResolvedAgentProfile {
|
||||
readonly name: string;
|
||||
readonly tools: readonly string[];
|
||||
systemPrompt(context: SystemPromptContext): string;
|
||||
}
|
||||
/**
|
||||
* Resolved profile consumed by {@link IAgentProfileService.useProfile} /
|
||||
* {@link IAgentProfileService.applyProfile}. Alias of the catalog's
|
||||
* {@link AgentProfile} — a profile is self-contained (full system prompt +
|
||||
* tools), so the per-agent binding and the profile catalog share one type.
|
||||
*/
|
||||
export type ResolvedAgentProfile = AgentProfile;
|
||||
|
||||
export interface ProfileData extends AgentConfigData {
|
||||
readonly activeToolNames?: readonly string[];
|
||||
|
|
@ -92,11 +92,39 @@ export interface ProfileSetModelResult {
|
|||
readonly providerName?: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomic binding input: a named Profile plus a Model id/alias. Binding the two
|
||||
* (with optional run config) is what makes an Agent runnable — `Profile +
|
||||
* Model ⇒ Agent`. `profile` defaults to the catalog's default profile when the
|
||||
* caller only supplies a model (see {@link IAgentProfileService.setModel}).
|
||||
*/
|
||||
export interface BindAgentInput {
|
||||
/** Profile name from `IAgentProfileCatalogService` (e.g. 'agent', 'explore'). */
|
||||
readonly profile: string;
|
||||
/** Model id or routing alias resolved through `IModelResolver`. */
|
||||
readonly model: string;
|
||||
readonly thinking?: string;
|
||||
readonly cwd?: string;
|
||||
}
|
||||
|
||||
export interface IAgentProfileService {
|
||||
readonly _serviceBrand: undefined;
|
||||
configure(options: ProfileServiceOptions): void;
|
||||
update(changed: ProfileUpdateData): void;
|
||||
setModel(model: string): ProfileSetModelResult;
|
||||
/**
|
||||
* Atomically bind a Profile + Model (plus optional run config) to this agent,
|
||||
* rendering the profile's system prompt and activating its tool set. This is
|
||||
* the production entry point that turns an agent scope into a runnable Agent.
|
||||
* Throws `PROFILE_NOT_FOUND` / `MODEL_NOT_CONFIGURED` on unknown inputs.
|
||||
*/
|
||||
bind(input: BindAgentInput): Promise<void>;
|
||||
/**
|
||||
* Bind (or switch) the active Model. When no Profile is bound yet, the
|
||||
* catalog's default profile is bound first (rendering its system prompt and
|
||||
* tool set), so a fresh agent becomes runnable on its first `setModel`.
|
||||
* Subsequent calls swap the model while keeping the existing profile.
|
||||
*/
|
||||
setModel(model: string): Promise<ProfileSetModelResult>;
|
||||
setThinking(level: string): void;
|
||||
getModel(): string;
|
||||
useProfile(profile: ResolvedAgentProfile, context: SystemPromptContext): void;
|
||||
|
|
@ -135,6 +163,8 @@ export interface IAgentProfileService {
|
|||
getModelCapabilities(): ModelCapability;
|
||||
getMaxOutputSize(): number | undefined;
|
||||
hasModel(): boolean;
|
||||
/** True when both a Profile and a Model are bound — i.e. the agent can run a turn. */
|
||||
isRunnable(): boolean;
|
||||
hasProvider(): boolean;
|
||||
getSystemPrompt(): string;
|
||||
getActiveToolNames(): readonly string[] | undefined;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ import {
|
|||
type ModelCapability,
|
||||
type ThinkingEffort,
|
||||
} from '#/app/llmProtocol';
|
||||
import {
|
||||
DEFAULT_AGENT_PROFILE_NAME,
|
||||
IAgentProfileCatalogService,
|
||||
} from '#/app/agentProfileCatalog';
|
||||
import { IModelResolver, type KimiModelOverrides, type Model } from '#/app/model';
|
||||
import picomatch from 'picomatch';
|
||||
|
||||
|
|
@ -28,6 +32,7 @@ import { ISessionAgentFileSystem } from '#/session/agentFs';
|
|||
import { IExecContext } from '#/session/execContext';
|
||||
import { isMcpToolName } from '#/agent/tool';
|
||||
import { ISessionWorkspaceContext } from '#/session/workspaceContext';
|
||||
import { ISessionSkillCatalog } from '#/session/sessionSkillCatalog';
|
||||
import type { ResolvedAgentProfile, SystemPromptContext } from '#/agent/profile';
|
||||
|
||||
import { IAgentRecordService, type AgentRecord } from '#/agent/record';
|
||||
|
|
@ -36,6 +41,7 @@ import type { ToolSource } from '#/agent/tool';
|
|||
import { prepareSystemPromptContext } from './context';
|
||||
import type {
|
||||
ApplyProfileOptions,
|
||||
BindAgentInput,
|
||||
ProfileData,
|
||||
ProfileModelContext,
|
||||
ProfileServiceOptions,
|
||||
|
|
@ -79,6 +85,8 @@ export class AgentProfileService implements IAgentProfileService {
|
|||
@IExecContext private readonly execCtx: IExecContext,
|
||||
@IBootstrapService private readonly bootstrap: IBootstrapService,
|
||||
@ISessionWorkspaceContext private readonly workspace: ISessionWorkspaceContext,
|
||||
@IAgentProfileCatalogService private readonly catalog: IAgentProfileCatalogService,
|
||||
@ISessionSkillCatalog private readonly skillCatalog: ISessionSkillCatalog,
|
||||
) {
|
||||
this.configure({});
|
||||
record.define('config.update', {
|
||||
|
|
@ -116,9 +124,44 @@ export class AgentProfileService implements IAgentProfileService {
|
|||
}
|
||||
}
|
||||
|
||||
setModel(alias: string): ProfileSetModelResult {
|
||||
async bind(input: BindAgentInput): Promise<void> {
|
||||
const profile = this.catalog.get(input.profile);
|
||||
if (profile === undefined) {
|
||||
throw new Error(`Unknown agent profile: "${input.profile}"`);
|
||||
}
|
||||
// Resolve eagerly so an unknown model id fails the bind here rather than on
|
||||
// the first turn.
|
||||
this.modelFactory.resolve(input.model);
|
||||
|
||||
const context = await this.buildSystemPromptContext(input.cwd);
|
||||
const systemPrompt = profile.systemPrompt(context);
|
||||
const { agentsMdWarning } = context;
|
||||
this.agentsMdWarning = agentsMdWarning;
|
||||
|
||||
this.update({
|
||||
cwd: input.cwd,
|
||||
profileName: profile.name,
|
||||
modelAlias: input.model,
|
||||
thinkingLevel: input.thinking,
|
||||
systemPrompt,
|
||||
activeToolNames: profile.tools,
|
||||
});
|
||||
|
||||
if (agentsMdWarning !== undefined) {
|
||||
this.record.signal({
|
||||
type: 'warning',
|
||||
message: agentsMdWarning,
|
||||
code: 'agents-md-oversized',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async setModel(alias: string): Promise<ProfileSetModelResult> {
|
||||
const model = this.modelFactory.resolve(alias);
|
||||
if (this.modelAlias !== alias) {
|
||||
if (this.profileName === undefined) {
|
||||
await this.bind({ profile: DEFAULT_AGENT_PROFILE_NAME, model: alias });
|
||||
this.telemetry.track('model_switch', { model: alias });
|
||||
} else if (this.modelAlias !== alias) {
|
||||
this.update({ modelAlias: alias });
|
||||
this.telemetry.track('model_switch', { model: alias });
|
||||
}
|
||||
|
|
@ -240,6 +283,10 @@ export class AgentProfileService implements IAgentProfileService {
|
|||
return this.modelAlias !== undefined;
|
||||
}
|
||||
|
||||
isRunnable(): boolean {
|
||||
return this.profileName !== undefined && this.hasModel();
|
||||
}
|
||||
|
||||
hasProvider(): boolean {
|
||||
return this.tryResolveRawModel() !== undefined;
|
||||
}
|
||||
|
|
@ -353,6 +400,35 @@ export class AgentProfileService implements IAgentProfileService {
|
|||
}
|
||||
}
|
||||
|
||||
private async buildSystemPromptContext(cwd?: string): Promise<SystemPromptContext> {
|
||||
const effectiveCwd = cwd ?? this.execCtx.cwd;
|
||||
const base = await prepareSystemPromptContext(
|
||||
{ fs: this.fs, homeDir: this.env.homeDir },
|
||||
effectiveCwd,
|
||||
this.bootstrap.homeDir,
|
||||
{ additionalDirs: this.workspace.additionalDirs },
|
||||
);
|
||||
const skills = await this.resolveSkillListing();
|
||||
return {
|
||||
...base,
|
||||
cwd: effectiveCwd,
|
||||
osKind: this.env.osKind,
|
||||
shellName: this.env.shellName,
|
||||
shellPath: this.env.shellPath,
|
||||
now: new Date().toISOString(),
|
||||
skills,
|
||||
};
|
||||
}
|
||||
|
||||
private async resolveSkillListing(): Promise<string> {
|
||||
try {
|
||||
await this.skillCatalog.ready;
|
||||
return this.skillCatalog.catalog.getModelSkillListing();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private readConfiguredCwd(): string | undefined {
|
||||
const cwd = this.optionsValue.cwd;
|
||||
return typeof cwd === 'function' ? cwd() : cwd;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@ import type { SessionSummary } from '#/agent/rpc/core-api';
|
|||
import type { UsageStatus } from '@moonshot-ai/protocol';
|
||||
import type { SessionMeta } from '#/session/sessionMetadata';
|
||||
|
||||
/**
|
||||
* Wire projection of the agent's role in the resume DTO: `'main'` when
|
||||
* `agentId === 'main'`, `'sub'` otherwise. Wire values kept for node-sdk
|
||||
* compatibility; not a business concept.
|
||||
*/
|
||||
type AgentType = 'main' | 'sub';
|
||||
|
||||
export type AgentReplayRecordPayload =
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
/**
|
||||
* `shellTools` domain barrel — re-exports the built-in Bash tool, its process
|
||||
* task adapter, and the shared output `ToolResultBuilder`. The Bash tool self-registers via
|
||||
* `registerTool(BashTool)` at module load, so importing this barrel is what
|
||||
* wires it into every Agent-scope tool registry.
|
||||
*/
|
||||
|
||||
export * from '#/agent/shellTools/tools/bash';
|
||||
export * from '#/agent/shellTools/tools/process-task';
|
||||
export * from '#/agent/shellTools/tools/result-builder';
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
export {
|
||||
ToolResultBuilder,
|
||||
type ExecutableToolResultBuilderResult,
|
||||
type ToolResultBuilderOptions,
|
||||
} from '#/agent/tool/result-builder';
|
||||
|
|
@ -1,19 +1,25 @@
|
|||
/**
|
||||
* `agentProfileCatalog` domain (L3) — App-scope registry of named agent profiles
|
||||
* that a parent Agent can invoke a child Agent under.
|
||||
* `agentProfileCatalog` domain (L3) — App-scope registry of named agent
|
||||
* profiles.
|
||||
*
|
||||
* A profile is "how an Agent runs": which tools are active, what system prompt
|
||||
* overlay is applied on top of the caller's prompt, an optional per-invocation
|
||||
* prompt prefix (e.g. explore's git-context block), and an optional summary
|
||||
* distillation policy (min chars + continuation prompt) applied when a caller
|
||||
* awaits the child's turn output.
|
||||
* A profile is "how an Agent runs": the full system prompt it renders for a
|
||||
* given context, the tool set it may use, plus optional per-invocation and
|
||||
* summary-distillation behavior for child agents. A profile is model-agnostic:
|
||||
* the same profile can be bound to any Model. Together with a bound Model, a
|
||||
* profile uniquely determines an Agent's behavior (`Profile + Model ⇒ Agent`).
|
||||
*
|
||||
* Every profile is self-contained: `systemPrompt(context)` returns the complete
|
||||
* prompt (base + role overlay are merged at definition time, not at spawn
|
||||
* time). The builtin {@link DEFAULT_AGENT_PROFILE_NAME} (`agent`) is the default
|
||||
* profile used when an Agent is bound to a Model without naming a profile.
|
||||
*
|
||||
* Profiles are contributed at module load via `registerAgentProfile(...)`, the
|
||||
* same "import = register" pattern used by `registerTool` and
|
||||
* `registerConfigSection`. `AgentProfileCatalogService` consumes the accumulated
|
||||
* contributions on construction and exposes `get(name)` / `list()` to callers
|
||||
* (currently the `Agent` tool). Contributions are keyed by `name`; a
|
||||
* later-registered profile with the same name overrides an earlier one.
|
||||
* contributions on construction and exposes `get(name)` / `getDefault()` /
|
||||
* `list()` to callers (the `Agent` tool, the swarm scheduler, and the per-agent
|
||||
* profile binding). Contributions are keyed by `name`; a later-registered
|
||||
* profile with the same name overrides an earlier one.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
|
@ -21,6 +27,9 @@ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiatio
|
|||
import type { ILogger } from '#/app/log';
|
||||
import type { ISessionProcessRunner } from '#/session/process';
|
||||
|
||||
/** Name of the builtin default profile (the top-level interactive agent). */
|
||||
export const DEFAULT_AGENT_PROFILE_NAME = 'agent';
|
||||
|
||||
export interface AgentProfilePromptPrefixContext {
|
||||
readonly cwd: string;
|
||||
readonly runner: ISessionProcessRunner;
|
||||
|
|
@ -38,24 +47,45 @@ export interface AgentProfileSummaryPolicy {
|
|||
readonly retries: number;
|
||||
}
|
||||
|
||||
export interface AgentProfileDefinition {
|
||||
/**
|
||||
* Runtime context supplied to a profile's system-prompt renderer. Captures
|
||||
* everything determined at render time (working dir, AGENTS.md, host OS/shell,
|
||||
* skills, …). Assembled by the `profile` domain and passed into
|
||||
* {@link AgentProfile.systemPrompt}.
|
||||
*/
|
||||
export interface AgentProfileContext {
|
||||
readonly cwd?: string;
|
||||
/** 2-level tree listing of the working directory, for LLM orientation. */
|
||||
readonly cwdListing?: string;
|
||||
/** Concatenated AGENTS.md instruction hierarchy (user-level + project-level). */
|
||||
readonly agentsMd?: string;
|
||||
/** Rendered listings of additional workspace directories. */
|
||||
readonly additionalDirsInfo?: string;
|
||||
/** Host OS family (`macOS` / `Linux` / `Windows` / raw platform). */
|
||||
readonly osKind?: string;
|
||||
readonly shellName?: string;
|
||||
readonly shellPath?: string;
|
||||
/** ISO timestamp captured at render time. */
|
||||
readonly now?: string;
|
||||
/** Rendered model-facing listing of available skills. */
|
||||
readonly skills?: string;
|
||||
readonly [key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AgentProfile {
|
||||
/** Stable identifier; must be unique across contributions. */
|
||||
readonly name: string;
|
||||
/** Short human-readable label; surfaced to the caller (LLM) as "Available agent types". */
|
||||
readonly description?: string;
|
||||
/** When-to-use hint appended to `description` in the caller's tool spec. */
|
||||
readonly whenToUse?: string;
|
||||
/** Tool names (and MCP glob patterns) the agent may use under this profile. */
|
||||
readonly tools: readonly string[];
|
||||
/**
|
||||
* Text appended to the parent's system prompt when a child agent is spawned
|
||||
* under this profile. Undefined = use parent's system prompt verbatim.
|
||||
* Render the complete system prompt for this profile given the runtime
|
||||
* context. Self-contained — includes the base prompt and any role overlay.
|
||||
*/
|
||||
readonly systemPromptOverlay?: string;
|
||||
/**
|
||||
* Tool names the child agent may use. Undefined = inherit the parent's
|
||||
* active tool set (`coder` behaves this way for the special case where the
|
||||
* parent is also a `coder`).
|
||||
*/
|
||||
readonly activeToolNames?: readonly string[];
|
||||
systemPrompt(context: AgentProfileContext): string;
|
||||
/**
|
||||
* Optional per-invocation prompt prefix produced from the caller's context
|
||||
* (e.g. `explore`'s `<git-context>` block). Prepended to the caller-supplied
|
||||
|
|
@ -73,9 +103,15 @@ export interface AgentProfileDefinition {
|
|||
export interface IAgentProfileCatalogService {
|
||||
readonly _serviceBrand: undefined;
|
||||
/** Return the profile with the given name, or `undefined` when unknown. */
|
||||
get(name: string): AgentProfileDefinition | undefined;
|
||||
get(name: string): AgentProfile | undefined;
|
||||
/**
|
||||
* Return the builtin default profile ({@link DEFAULT_AGENT_PROFILE_NAME}).
|
||||
* Throws when no default profile is registered (a programming-time invariant
|
||||
* violation, not a request failure).
|
||||
*/
|
||||
getDefault(): AgentProfile;
|
||||
/** Enumerate every registered profile. Stable order (insertion order). */
|
||||
list(): readonly AgentProfileDefinition[];
|
||||
list(): readonly AgentProfile[];
|
||||
}
|
||||
|
||||
export const IAgentProfileCatalogService: ServiceIdentifier<IAgentProfileCatalogService> =
|
||||
|
|
|
|||
|
|
@ -4,21 +4,24 @@
|
|||
* Snapshots the module-level contributions on construction. Register-after-
|
||||
* construction is not supported: like `IAgentToolRegistryService`, the
|
||||
* expectation is that contributions accumulate at import time before the
|
||||
* container resolves the service.
|
||||
* container resolves the service. `getDefault()` throws a plain `Error` when
|
||||
* the builtin default profile is missing — that is a programming-time
|
||||
* invariant violation, not a request failure, so it does not warrant a wire
|
||||
* error code.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
|
||||
import type { AgentProfileDefinition } from './agentProfileCatalog';
|
||||
import { IAgentProfileCatalogService } from './agentProfileCatalog';
|
||||
import type { AgentProfile } from './agentProfileCatalog';
|
||||
import { DEFAULT_AGENT_PROFILE_NAME, IAgentProfileCatalogService } from './agentProfileCatalog';
|
||||
import { getAgentProfileContributions } from './contribution';
|
||||
|
||||
export class AgentProfileCatalogService implements IAgentProfileCatalogService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly byName: Map<string, AgentProfileDefinition>;
|
||||
private readonly ordered: readonly AgentProfileDefinition[];
|
||||
private readonly byName: Map<string, AgentProfile>;
|
||||
private readonly ordered: readonly AgentProfile[];
|
||||
|
||||
constructor() {
|
||||
const contributions = getAgentProfileContributions();
|
||||
|
|
@ -26,11 +29,21 @@ export class AgentProfileCatalogService implements IAgentProfileCatalogService {
|
|||
this.byName = new Map(this.ordered.map((def) => [def.name, def]));
|
||||
}
|
||||
|
||||
get(name: string): AgentProfileDefinition | undefined {
|
||||
get(name: string): AgentProfile | undefined {
|
||||
return this.byName.get(name);
|
||||
}
|
||||
|
||||
list(): readonly AgentProfileDefinition[] {
|
||||
getDefault(): AgentProfile {
|
||||
const profile = this.byName.get(DEFAULT_AGENT_PROFILE_NAME);
|
||||
if (profile === undefined) {
|
||||
throw new Error(
|
||||
`Default agent profile "${DEFAULT_AGENT_PROFILE_NAME}" is not registered`,
|
||||
);
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
list(): readonly AgentProfile[] {
|
||||
return this.ordered;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@
|
|||
* can override built-ins by re-registering.
|
||||
*/
|
||||
|
||||
import type { AgentProfileDefinition } from './agentProfileCatalog';
|
||||
import type { AgentProfile } from './agentProfileCatalog';
|
||||
|
||||
const _profileContributions: AgentProfileDefinition[] = [];
|
||||
const _profileContributions: AgentProfile[] = [];
|
||||
|
||||
export function registerAgentProfile(definition: AgentProfileDefinition): void {
|
||||
export function registerAgentProfile(definition: AgentProfile): void {
|
||||
const existingIndex = _profileContributions.findIndex((d) => d.name === definition.name);
|
||||
if (existingIndex >= 0) {
|
||||
_profileContributions.splice(existingIndex, 1);
|
||||
|
|
@ -21,7 +21,7 @@ export function registerAgentProfile(definition: AgentProfileDefinition): void {
|
|||
_profileContributions.push(definition);
|
||||
}
|
||||
|
||||
export function getAgentProfileContributions(): readonly AgentProfileDefinition[] {
|
||||
export function getAgentProfileContributions(): readonly AgentProfile[] {
|
||||
return _profileContributions;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
export * from './agentProfileCatalog';
|
||||
export * from './agentProfileCatalogService';
|
||||
export * from './profile-shared';
|
||||
export * from './promptPrefix';
|
||||
export {
|
||||
registerAgentProfile,
|
||||
getAgentProfileContributions,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* `agentProfileCatalog` domain (L3) — profile prompt-prefix helper.
|
||||
*
|
||||
* Applies a profile's optional per-invocation `promptPrefix` (e.g. `explore`'s
|
||||
* `<git-context>` block) to a caller-supplied prompt. Best-effort: a thrown
|
||||
* error or empty prefix leaves the prompt unchanged. Shared by every launcher
|
||||
* that instantiates an agent from a profile (the `Agent` tool, the swarm
|
||||
* scheduler).
|
||||
*/
|
||||
|
||||
import type {
|
||||
AgentProfile,
|
||||
AgentProfilePromptPrefixContext,
|
||||
} from './agentProfileCatalog';
|
||||
|
||||
export async function applyProfilePromptPrefix(
|
||||
profile: AgentProfile,
|
||||
prompt: string,
|
||||
ctx: AgentProfilePromptPrefixContext,
|
||||
): Promise<string> {
|
||||
if (profile.promptPrefix === undefined) return prompt;
|
||||
try {
|
||||
const prefix = await profile.promptPrefix(ctx);
|
||||
return prefix.length > 0 ? `${prefix}\n\n${prompt}` : prompt;
|
||||
} catch {
|
||||
return prompt;
|
||||
}
|
||||
}
|
||||
19
packages/agent-core-v2/src/app/cron/index.ts
Normal file
19
packages/agent-core-v2/src/app/cron/index.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* `cron` domain barrel — re-exports the cron task record (`cronTask`), the
|
||||
* `ICronTaskPersistence` contract and its App-scoped implementation
|
||||
* (`cronTaskPersistenceService`), the dependency-free cron algorithm library
|
||||
* (`cron-expr`, `jitter`, `clock`, `format`), and registers the `cron` config
|
||||
* section into `config`. Importing this barrel registers the
|
||||
* `ICronTaskPersistence` binding and the cron config section.
|
||||
*/
|
||||
|
||||
import './configSection';
|
||||
|
||||
export * from './cronTask';
|
||||
export * from './cronTaskPersistence';
|
||||
export * from './cronTaskPersistenceService';
|
||||
export * from './cron-expr';
|
||||
export * from './format';
|
||||
export * from './jitter';
|
||||
export * from './clock';
|
||||
export * from './configSection';
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
/**
|
||||
* `cron` domain barrel — re-exports the cron task data record, the
|
||||
* `ICronTaskPersistence` contract, and registers the App-scoped persistence service.
|
||||
*/
|
||||
|
||||
export * from './cronTask';
|
||||
export * from './cronTaskPersistence';
|
||||
export * from './cronTaskPersistenceService';
|
||||
|
|
@ -15,7 +15,7 @@ import type { Message, PageResponse } from '@moonshot-ai/protocol';
|
|||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { type IAgentScopeHandle, LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentLifecycleService } from '#/session/agentLifecycle';
|
||||
import { ensureMainAgent } from '#/session/agentLifecycle';
|
||||
import {
|
||||
IAgentContextMemoryService,
|
||||
toProtocolMessage,
|
||||
|
|
@ -28,7 +28,6 @@ import { ISessionLifecycleService } from '#/app/sessionLifecycle';
|
|||
|
||||
import { IMessageLegacyService, type MessageListQuery } from './messageLegacy';
|
||||
|
||||
const MAIN_AGENT_ID = 'main';
|
||||
const DEFAULT_PAGE_SIZE = 50;
|
||||
const MAX_PAGE_SIZE = 100;
|
||||
|
||||
|
|
@ -128,13 +127,10 @@ export class MessageLegacyService implements IMessageLegacyService {
|
|||
private async resolveMainAgent(sessionId: string): Promise<IAgentScopeHandle | undefined> {
|
||||
const session = await this.lifecycle.resume(sessionId);
|
||||
if (session === undefined) return undefined;
|
||||
const agents = session.accessor.get(IAgentLifecycleService);
|
||||
const existing = agents.getHandle(MAIN_AGENT_ID);
|
||||
if (existing !== undefined) return existing;
|
||||
// Live session whose main agent has not been materialized yet: create it
|
||||
// fresh. No restore here — the session was already live, so any persisted
|
||||
// wire is already reflected in memory; re-restoring would re-apply splices.
|
||||
return agents.createMain();
|
||||
return ensureMainAgent(session);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { type IAgentScopeHandle, LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IAgentLifecycleService } from '#/session/agentLifecycle';
|
||||
import { ensureMainAgent } from '#/session/agentLifecycle';
|
||||
import { IAgentContextMemoryService, toProtocolMessage, type ContextMessage } from '#/agent/contextMemory';
|
||||
import { IAgentContextSizeService } from '#/agent/contextSize';
|
||||
import { ErrorCodes, isKimiError, KimiError } from '#/errors';
|
||||
|
|
@ -47,8 +47,6 @@ import {
|
|||
type SessionWireFields,
|
||||
} from './sessionLegacy';
|
||||
|
||||
const MAIN_AGENT_ID = 'main';
|
||||
|
||||
/**
|
||||
* v1 `child_session_kind` marker (`packages/agent-core/.../sessionService.ts`).
|
||||
* A fork is only listed as a "child" when its metadata carries both
|
||||
|
|
@ -263,17 +261,15 @@ export class SessionLegacyService implements ISessionLegacyService {
|
|||
|
||||
/**
|
||||
* Resolve the session's main agent, creating it on demand (mirrors v1's
|
||||
* `resumeSession` + the server-v2 `ensureMainAgent` helper).
|
||||
* `resumeSession`; delegates to the `agentLifecycle` domain's
|
||||
* `ensureMainAgent` bootstrap helper).
|
||||
*/
|
||||
private async resolveMainAgent(sessionId: string): Promise<IAgentScopeHandle> {
|
||||
const session = this.lifecycle.get(sessionId);
|
||||
if (session === undefined) {
|
||||
throw new KimiError(ErrorCodes.SESSION_NOT_FOUND, `session ${sessionId} does not exist`);
|
||||
}
|
||||
const agents = session.accessor.get(IAgentLifecycleService);
|
||||
const existing = agents.getHandle(MAIN_AGENT_ID);
|
||||
if (existing !== undefined) return existing;
|
||||
return agents.createMain();
|
||||
return ensureMainAgent(session);
|
||||
}
|
||||
|
||||
private async assembleStatus(sessionId: string, agent: IAgentScopeHandle): Promise<SessionStatusResponse> {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {
|
|||
import { Disposable } from '#/_base/di/lifecycle';
|
||||
import { Emitter, type Event } from '#/_base/event';
|
||||
import { encodeWorkDirKey } from '#/_base/utils/workdir-slug';
|
||||
import { IAgentLifecycleService } from '#/session/agentLifecycle';
|
||||
import { IAgentLifecycleService, ensureMainAgent, MAIN_AGENT_ID } from '#/session/agentLifecycle';
|
||||
import { IBootstrapService } from '#/app/bootstrap';
|
||||
import { IEventService } from '#/app/event';
|
||||
import { IAgentContextMemoryService } from '#/agent/contextMemory';
|
||||
|
|
@ -153,8 +153,8 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
|
|||
|
||||
const handle = await this.create({ sessionId, workDir: workspace.root });
|
||||
const agents = handle.accessor.get(IAgentLifecycleService);
|
||||
if (agents.getHandle('main') === undefined) {
|
||||
const main = await agents.createMain();
|
||||
if (agents.getHandle(MAIN_AGENT_ID) === undefined) {
|
||||
const main = await ensureMainAgent(handle);
|
||||
// Resolve context memory BEFORE restoring so its `context.splice` resumer
|
||||
// is registered; otherwise the wire replay applies splices into a void and
|
||||
// the restored transcript never lands in context memory.
|
||||
|
|
@ -280,7 +280,9 @@ export class SessionLifecycleService extends Disposable implements ISessionLifec
|
|||
const agentHandle = await target.accessor.get(IAgentLifecycleService).create({
|
||||
agentId,
|
||||
forkedFrom: sourceAgent.forkedFrom ?? legacy.parentAgentId,
|
||||
swarmItem: sourceAgent.swarmItem,
|
||||
labels:
|
||||
sourceAgent.labels ??
|
||||
(sourceAgent.swarmItem !== undefined ? { swarmItem: sourceAgent.swarmItem } : undefined),
|
||||
});
|
||||
await agentHandle.accessor.get(IAgentWireRecordService).restore();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,9 +44,8 @@ export * from '#/agent/usage';
|
|||
export * from '#/agent/toolDedupe';
|
||||
|
||||
export * from '#/agent/task';
|
||||
export * from '#/app/cronPersistence';
|
||||
export * from '#/app/cron';
|
||||
export * from '#/session/cron';
|
||||
import '#/agent/cron';
|
||||
|
||||
export * from '#/session/agentLifecycle';
|
||||
export * from '#/app/sessionLifecycle';
|
||||
|
|
@ -115,4 +114,3 @@ export type { ToolContribution, ToolContributionOptions } from '#/agent/toolRegi
|
|||
export * from '#/agent/toolState';
|
||||
export * from '#/agent/userTool';
|
||||
export * from '#/agent/wireRecord';
|
||||
export * from '#/agent/shellTools';
|
||||
|
|
|
|||
|
|
@ -39,13 +39,13 @@ import { ISessionProcessRunner } from '#/session/process';
|
|||
import type { IProcess } from '#/session/process';
|
||||
import { IAgentProfileService } from '#/agent/profile';
|
||||
import type { BuiltinTool, ExecutableToolResult, ToolExecution, ToolUpdate } from '#/agent/tool';
|
||||
import { ToolResultBuilder } from '#/agent/tool/result-builder';
|
||||
import { registerTool } from '#/agent/toolRegistry';
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import { literalRulePattern, matchesGlobRuleSubject } from '#/_base/tools/support/rule-match';
|
||||
import { renderPrompt } from '#/_base/utils/render-prompt';
|
||||
import bashDescriptionTemplate from './bash.md?raw';
|
||||
import { ProcessTask } from './process-task';
|
||||
import { ToolResultBuilder } from './result-builder';
|
||||
|
||||
const MS_PER_SECOND = 1000;
|
||||
const DEFAULT_TIMEOUT_S = 60;
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './bash';
|
||||
export * from './edit';
|
||||
export * from './glob';
|
||||
export * from './grep';
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
/**
|
||||
* Git context collection for explore subagents.
|
||||
* Git context collection for explore agents.
|
||||
*
|
||||
* `collectGitContext` produces a `<git-context>` block that is prepended to a
|
||||
* fresh explore subagent's prompt so it can orient itself in the repository
|
||||
* fresh explore agent's prompt so it can orient itself in the repository
|
||||
* before searching. Every git probe is best-effort: probes fail in perfectly
|
||||
* normal states (no `origin` remote, no commits yet, detached HEAD, older
|
||||
* Git), so a failed probe is logged and its section omitted rather than
|
||||
* dropping the whole block. The block is omitted entirely only when nothing
|
||||
* useful was collected. The one explicit state surfaced to the subagent is
|
||||
* useful was collected. The one explicit state surfaced to the agent is
|
||||
* `reason="not-a-repo"`, so it doesn't waste turns probing git history in a
|
||||
* non-repo directory. Remote URLs are sanitized so internal infrastructure
|
||||
* is not surfaced to the model.
|
||||
|
|
|
|||
|
|
@ -1,107 +1,128 @@
|
|||
/**
|
||||
* `agentLifecycle` domain (L6) — creates and tracks agents within a session.
|
||||
* `agentLifecycle` domain (L6) — flat registry of the session's agents.
|
||||
*
|
||||
* Defines the public contract of agent lifecycle: the `CreateAgentOptions` and
|
||||
* the `IAgentLifecycleService` used to create agents (`create` / `createMain`),
|
||||
* clone an existing agent (`clone`), look them up (`getHandle` / `list`), and
|
||||
* remove them. Session-scoped — one instance per session.
|
||||
* Defines the public contract of agent lifecycle: `create` (from zero, Profile
|
||||
* + Model), `fork` (inherit binding + context history), `run` (drive one
|
||||
* prompt/retry turn on an agent and await its distilled summary), plus lookup
|
||||
* (`getHandle` / `list`) and removal. Session-scoped — one instance per
|
||||
* session.
|
||||
*
|
||||
* Invariants:
|
||||
* - The registry is flat: agents have no nesting. There is no parent/child or
|
||||
* caller/callee relationship here; when a business domain needs such a
|
||||
* relationship (e.g. the `Agent` tool's display events), that domain
|
||||
* maintains it itself.
|
||||
* - The main agent is an ordinary agent whose only distinction is
|
||||
* `agentId === 'main'`. Business operations (create / fork / run / lookup)
|
||||
* treat it uniformly; the only main-specific surface is the
|
||||
* `onDidCreateMain` event, fired via `notifyMainCreated` by the main
|
||||
* bootstrapper so main-only capabilities subscribe without filtering every
|
||||
* `onDidCreate`.
|
||||
* - `forkedFrom` is provenance only (a recorded value); business logic must
|
||||
* not branch on it.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
import type { IAgentScopeHandle } from '#/_base/di/scope';
|
||||
import type { Event } from '#/_base/event';
|
||||
import type { TokenUsage } from '#/app/llmProtocol';
|
||||
import type { AgentProfileSummaryPolicy } from '#/app/agentProfileCatalog';
|
||||
import type { BindAgentInput } from '#/agent/profile';
|
||||
import type { Turn } from '#/agent/turn';
|
||||
|
||||
export interface CreateAgentOptions {
|
||||
readonly agentId?: string;
|
||||
/** Agent this one is cloned / derived from (provenance only; not used by business logic). */
|
||||
/**
|
||||
* Profile + Model to bind at creation so the agent is born runnable
|
||||
* (`Profile + Model ⇒ Agent`). May be omitted by exactly two callers:
|
||||
* session resume/fork (the binding is restored from the wire log) and the
|
||||
* edge-bootstrapped main agent (the edge binds a model right after). Every
|
||||
* other creation path must pass a full binding.
|
||||
*/
|
||||
readonly binding?: BindAgentInput;
|
||||
/** Agent this one is derived from (provenance only; not used by business logic). */
|
||||
readonly forkedFrom?: string;
|
||||
readonly cwd?: string;
|
||||
readonly swarmItem?: string;
|
||||
/**
|
||||
* Business-defined recorded values (e.g. the swarm's `swarmItem`). Persisted
|
||||
* verbatim into the session's agent registry; never interpreted here.
|
||||
*/
|
||||
readonly labels?: Readonly<Record<string, string>>;
|
||||
}
|
||||
|
||||
export interface SpawnAgentOptions {
|
||||
export interface ForkAgentOptions {
|
||||
readonly agentId?: string;
|
||||
/** Override the child's cwd. Defaults to the parent's cwd. */
|
||||
readonly cwd?: string;
|
||||
readonly swarmItem?: string;
|
||||
/**
|
||||
* Overrides merged over the source agent's binding (e.g. a title generator
|
||||
* forking `main` onto a cheaper model).
|
||||
*/
|
||||
readonly binding?: Partial<BindAgentInput>;
|
||||
}
|
||||
|
||||
export type AgentRunRequest =
|
||||
| { readonly kind: 'prompt'; readonly prompt: string }
|
||||
| { readonly kind: 'retry'; readonly trigger?: string };
|
||||
|
||||
export interface RunAgentOptions {
|
||||
/** Cancellation signal. Aborting it cancels the agent's turn. */
|
||||
readonly signal: AbortSignal;
|
||||
/**
|
||||
* Summary distillation policy. Defaults to the `summaryPolicy` of the
|
||||
* profile the target agent is bound to; pass explicitly to override.
|
||||
*/
|
||||
readonly summaryPolicy?: AgentProfileSummaryPolicy;
|
||||
/** Fires once the turn's first request is committed (used by swarm to fan out). */
|
||||
readonly onReady?: () => void;
|
||||
}
|
||||
|
||||
export interface AgentRunHandle {
|
||||
readonly agentId: string;
|
||||
readonly turn: Turn;
|
||||
readonly completion: Promise<{ readonly summary: string; readonly usage?: TokenUsage }>;
|
||||
}
|
||||
|
||||
export interface AgentListFilter {
|
||||
readonly prefix?: string;
|
||||
}
|
||||
|
||||
// ── Subagent orchestration types ────────────────────────────────────
|
||||
|
||||
export interface SubagentRecordMetadata {
|
||||
readonly parentToolCallId?: string;
|
||||
readonly description?: string;
|
||||
readonly runInBackground?: boolean;
|
||||
readonly swarmIndex?: number;
|
||||
}
|
||||
|
||||
export interface RunSubagentOptions {
|
||||
readonly callerAgentId: string;
|
||||
readonly profileName: string;
|
||||
readonly prompt: string;
|
||||
readonly signal: AbortSignal;
|
||||
readonly metadata?: SubagentRecordMetadata;
|
||||
readonly suppressRateLimitFailureEvent?: boolean;
|
||||
readonly onReady?: () => void;
|
||||
}
|
||||
|
||||
export interface ResumeSubagentOptions {
|
||||
readonly callerAgentId: string;
|
||||
readonly agentId: string;
|
||||
readonly prompt: string;
|
||||
readonly signal: AbortSignal;
|
||||
readonly metadata?: SubagentRecordMetadata;
|
||||
}
|
||||
|
||||
export interface SubagentRunHandle {
|
||||
readonly agentId: string;
|
||||
readonly profileName: string;
|
||||
readonly completion: Promise<{ readonly result: string; readonly usage?: TokenUsage }>;
|
||||
}
|
||||
|
||||
export interface IAgentLifecycleService {
|
||||
readonly _serviceBrand: undefined;
|
||||
/** Fires after an agent is created and registered, with its scope handle. */
|
||||
readonly onDidCreate: Event<IAgentScopeHandle>;
|
||||
/**
|
||||
* Fires once after the main agent is created and its main-only wirings are
|
||||
* attached, with its scope handle. Use this instead of `onDidCreate` when a
|
||||
* capability belongs exclusively to the main agent, so subscribers do not
|
||||
* need to filter every agent creation by `id === 'main'`.
|
||||
*/
|
||||
readonly onDidCreateMain: Event<IAgentScopeHandle>;
|
||||
/** Fires after an agent is removed, with its agent id. */
|
||||
readonly onDidDispose: Event<string>;
|
||||
create(opts: CreateAgentOptions): Promise<IAgentScopeHandle>;
|
||||
createMain(): Promise<IAgentScopeHandle>;
|
||||
/** Clone an agent: copy its profile and context history into a new agent. */
|
||||
clone(sourceAgentId: string): Promise<IAgentScopeHandle>;
|
||||
/** Create an agent from zero (empty context). */
|
||||
create(opts?: CreateAgentOptions): Promise<IAgentScopeHandle>;
|
||||
/**
|
||||
* Create a child agent from a parent, copying the parent's profile fields
|
||||
* (`cwd` / `modelAlias` / `thinkingLevel` / `systemPrompt` / `activeToolNames`)
|
||||
* and recording `forkedFrom = parentAgentId`. Does **not** copy the parent's
|
||||
* context memory — the child starts with an empty context. Throws when the
|
||||
* parent does not exist.
|
||||
*
|
||||
* Applying a named profile (system-prompt overlay, tool overrides, prompt
|
||||
* prefix, summary policy) is a caller concern: use `applyProfileToAgent(...)`
|
||||
* from `session/agentLifecycle` after `spawn` returns.
|
||||
* Fire {@link onDidCreateMain} for the given handle. Called exactly once by
|
||||
* the main-agent bootstrapper (`ensureMainAgent`) after main-only wirings
|
||||
* are attached, so main-only capabilities can subscribe without filtering
|
||||
* every {@link onDidCreate}. No other caller should invoke it.
|
||||
*/
|
||||
spawn(parentAgentId: string, opts?: SpawnAgentOptions): Promise<IAgentScopeHandle>;
|
||||
notifyMainCreated(handle: IAgentScopeHandle): void;
|
||||
/**
|
||||
* Fork an agent: copy its profile binding and context history into a new
|
||||
* agent, recording `forkedFrom = sourceAgentId`. Throws when the source does
|
||||
* not exist.
|
||||
*/
|
||||
fork(sourceAgentId: string, opts?: ForkAgentOptions): Promise<IAgentScopeHandle>;
|
||||
/**
|
||||
* Submit one prompt (or retry) turn to an existing agent and return a handle
|
||||
* whose `completion` resolves with the distilled summary and token usage.
|
||||
* Emits nothing on anyone else's record stream — a caller that wants to
|
||||
* 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;
|
||||
getHandle(agentId: string): IAgentScopeHandle | undefined;
|
||||
list(filter?: AgentListFilter): readonly IAgentScopeHandle[];
|
||||
remove(agentId: string): Promise<void>;
|
||||
/**
|
||||
* Spawn a new child agent under a named profile, observe its turn, and
|
||||
* return a handle to the running completion. Composes `spawn` →
|
||||
* `applyProfileToAgent` → prompt-prefix → record signaling → telemetry →
|
||||
* `observeChildAgentTurn`.
|
||||
*/
|
||||
runSubagent(opts: RunSubagentOptions): Promise<SubagentRunHandle>;
|
||||
/**
|
||||
* Resume an existing child agent with a new prompt, observe its turn, and
|
||||
* return a handle to the running completion. Validates the target agent
|
||||
* exists, resolves its profile, and delegates to `observeChildAgentTurn`.
|
||||
*/
|
||||
resumeSubagent(opts: ResumeSubagentOptions): Promise<SubagentRunHandle>;
|
||||
}
|
||||
|
||||
export const IAgentLifecycleService: ServiceIdentifier<IAgentLifecycleService> =
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
/**
|
||||
* `agentLifecycle` domain (L6) — `IAgentLifecycleService` implementation.
|
||||
*
|
||||
* Creates and tracks the session's agents as child scopes. Seeds each agent's
|
||||
* identity through `agent` scopeContext, wires per-agent wire records, blob
|
||||
* store, and MCP, and registers the agent in the session registry. Bound at
|
||||
* Session scope.
|
||||
* Creates and tracks the session's agents as child scopes in a flat registry.
|
||||
* Seeds each agent's identity through `agent` scopeContext, wires per-agent
|
||||
* wire records, blob store, and MCP, and registers the agent in the session
|
||||
* registry. Bound at Session scope.
|
||||
*
|
||||
* No agent id is special here: the main agent is created by its bootstrappers
|
||||
* as `create({ agentId: 'main' })` (see `mainAgent.ts`), and `fork` requires
|
||||
* its source to exist. Caller-facing orchestration (record mirroring, hooks,
|
||||
* telemetry, prompt prefixes) lives with the callers — see this domain's
|
||||
* wrapper helpers (`tools/agent.ts`, `mirrorAgentRun`).
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
|
|
@ -19,10 +25,9 @@ import {
|
|||
registerScopedService,
|
||||
} from '#/_base/di/scope';
|
||||
import { IBootstrapService } from '#/app/bootstrap';
|
||||
import { IPluginSessionStartInjectorService } from '#/agent/contextInjector';
|
||||
import { ILogService } from '#/app/log';
|
||||
import { IAgentProfileCatalogService, type AgentProfileDefinition } from '#/app/agentProfileCatalog';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { IAgentProfileCatalogService } from '#/app/agentProfileCatalog';
|
||||
import type { AgentProfileSummaryPolicy } from '#/app/agentProfileCatalog';
|
||||
import { AgentMcpService, IAgentMcpService } from '#/agent/mcp';
|
||||
import { McpConnectionManager } from '#/agent/mcp/connection-manager';
|
||||
import { resolveSessionMcpConfig } from '#/agent/mcp/session-config';
|
||||
|
|
@ -33,7 +38,6 @@ import { ISessionWorkspaceContext } from '#/session/workspaceContext';
|
|||
import { IAgentScopeContext } from '#/agent/scopeContext';
|
||||
import { IAgentProfileService } from '#/agent/profile';
|
||||
import { IAgentContextMemoryService } from '#/agent/contextMemory';
|
||||
import { IAgentRecordService } from '#/agent/record';
|
||||
import { IAgentBuiltinToolsRegistrar } from '#/agent/toolRegistry';
|
||||
import { IAgentWireRecordService, AgentWireRecordService } from '#/agent/wireRecord';
|
||||
import { IAgentBlobService, AgentBlobServiceImpl } from '#/agent/blob';
|
||||
|
|
@ -44,16 +48,14 @@ import {
|
|||
|
||||
import {
|
||||
type AgentListFilter,
|
||||
type AgentRunHandle,
|
||||
type AgentRunRequest,
|
||||
type CreateAgentOptions,
|
||||
type ForkAgentOptions,
|
||||
IAgentLifecycleService,
|
||||
type ResumeSubagentOptions,
|
||||
type RunSubagentOptions,
|
||||
type SpawnAgentOptions,
|
||||
type SubagentRunHandle,
|
||||
type RunAgentOptions,
|
||||
} from './agentLifecycle';
|
||||
import { applyProfileToAgent } from './applyProfileToAgent';
|
||||
import { observeChildAgentTurn } from './observeChildAgentTurn';
|
||||
import { ISessionProcessRunner } from '#/session/process';
|
||||
import { runAgentTurn } from './runAgentTurn';
|
||||
|
||||
let nextAgentId = 0;
|
||||
|
||||
|
|
@ -61,12 +63,16 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle
|
|||
declare readonly _serviceBrand: undefined;
|
||||
private readonly handles = new Map<string, IAgentScopeHandle>();
|
||||
private readonly onDidCreateEmitter = this._register(new Emitter<IAgentScopeHandle>());
|
||||
private readonly onDidCreateMainEmitter = this._register(new Emitter<IAgentScopeHandle>());
|
||||
private readonly onDidDisposeEmitter = this._register(new Emitter<string>());
|
||||
private mcpManager: McpConnectionManager | undefined;
|
||||
|
||||
get onDidCreate() {
|
||||
return this.onDidCreateEmitter.event;
|
||||
}
|
||||
get onDidCreateMain() {
|
||||
return this.onDidCreateMainEmitter.event;
|
||||
}
|
||||
get onDidDispose() {
|
||||
return this.onDidDisposeEmitter.event;
|
||||
}
|
||||
|
|
@ -80,13 +86,11 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle
|
|||
@IPluginService private readonly plugins: IPluginService,
|
||||
@ILogService private readonly log: ILogService,
|
||||
@IAgentProfileCatalogService private readonly catalog: IAgentProfileCatalogService,
|
||||
@ITelemetryService private readonly telemetry: ITelemetryService,
|
||||
@ISessionProcessRunner private readonly processRunner: ISessionProcessRunner,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async create(opts: CreateAgentOptions): Promise<IAgentScopeHandle> {
|
||||
async create(opts: CreateAgentOptions = {}): Promise<IAgentScopeHandle> {
|
||||
const agentId = opts.agentId ?? `agent-${nextAgentId++}`;
|
||||
// Per-agent homedir → the wire-record persistence key (`hashKey(homedir)`).
|
||||
// Bootstrap computes it under the session dir, mirroring v1's
|
||||
|
|
@ -134,7 +138,7 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle
|
|||
await this.sessionMetadata.registerAgent(agentId, {
|
||||
homedir: agentHomedir,
|
||||
forkedFrom: opts.forkedFrom,
|
||||
swarmItem: opts.swarmItem,
|
||||
labels: opts.labels,
|
||||
});
|
||||
this.onDidCreateEmitter.fire(handle);
|
||||
// Force-instantiate the Eager builtin-tools registrar: its constructor
|
||||
|
|
@ -154,32 +158,40 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle
|
|||
// first turn — otherwise plugin/session MCP servers would connect but their
|
||||
// tools would never register until something explicitly requests the service.
|
||||
handle.accessor.get(IAgentMcpService);
|
||||
if (opts.binding !== undefined) {
|
||||
await handle.accessor.get(IAgentProfileService).bind(opts.binding);
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
async createMain(): Promise<IAgentScopeHandle> {
|
||||
const handle = await this.create({ agentId: 'main' });
|
||||
// Force-instantiate the plugin session-start injector so it registers its
|
||||
// turn-cadence injection before the first turn. Main-agent only, matching
|
||||
// v1's `pluginSessionStarts: type === 'main' ? ... : undefined`.
|
||||
handle.accessor.get(IPluginSessionStartInjectorService);
|
||||
return handle;
|
||||
notifyMainCreated(handle: IAgentScopeHandle): void {
|
||||
this.onDidCreateMainEmitter.fire(handle);
|
||||
}
|
||||
|
||||
async clone(sourceAgentId: string): Promise<IAgentScopeHandle> {
|
||||
const source =
|
||||
this.handles.get(sourceAgentId) ??
|
||||
(sourceAgentId === 'main' ? await this.createMain() : undefined);
|
||||
async fork(sourceAgentId: string, opts?: ForkAgentOptions): Promise<IAgentScopeHandle> {
|
||||
const source = this.handles.get(sourceAgentId);
|
||||
if (source === undefined) throw new Error(`Source agent "${sourceAgentId}" does not exist`);
|
||||
const child = await this.create({ forkedFrom: source.id });
|
||||
const child = await this.create({ agentId: opts?.agentId, forkedFrom: source.id });
|
||||
|
||||
const sourceData = source.accessor.get(IAgentProfileService).data();
|
||||
child.accessor.get(IAgentProfileService).update({
|
||||
modelAlias: sourceData.modelAlias,
|
||||
thinkingLevel: sourceData.thinkingLevel,
|
||||
systemPrompt: sourceData.systemPrompt,
|
||||
activeToolNames: sourceData.activeToolNames,
|
||||
});
|
||||
const childProfile = child.accessor.get(IAgentProfileService);
|
||||
const override = opts?.binding;
|
||||
const model = override?.model ?? sourceData.modelAlias;
|
||||
if (model !== undefined) {
|
||||
await childProfile.bind({
|
||||
profile: override?.profile ?? sourceData.profileName ?? 'agent',
|
||||
model,
|
||||
thinking: override?.thinking ?? sourceData.thinkingLevel,
|
||||
cwd: override?.cwd ?? sourceData.cwd,
|
||||
});
|
||||
} else {
|
||||
childProfile.update({
|
||||
profileName: override?.profile ?? sourceData.profileName,
|
||||
thinkingLevel: override?.thinking ?? sourceData.thinkingLevel,
|
||||
systemPrompt: sourceData.systemPrompt,
|
||||
activeToolNames: sourceData.activeToolNames,
|
||||
});
|
||||
}
|
||||
|
||||
const sourceMessages = source.accessor.get(IAgentContextMemoryService)?.get();
|
||||
if (sourceMessages !== undefined && sourceMessages.length > 0) {
|
||||
|
|
@ -188,30 +200,26 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle
|
|||
return child;
|
||||
}
|
||||
|
||||
async spawn(parentAgentId: string, opts?: SpawnAgentOptions): Promise<IAgentScopeHandle> {
|
||||
const parent = this.handles.get(parentAgentId);
|
||||
if (parent === undefined) throw new Error(`Parent agent "${parentAgentId}" does not exist`);
|
||||
const parentData = parent.accessor.get(IAgentProfileService).data();
|
||||
const child = await this.create({
|
||||
agentId: opts?.agentId,
|
||||
forkedFrom: parentAgentId,
|
||||
cwd: opts?.cwd ?? parentData.cwd,
|
||||
swarmItem: opts?.swarmItem,
|
||||
run(agentId: string, request: AgentRunRequest, opts: RunAgentOptions): AgentRunHandle {
|
||||
const handle = this.handles.get(agentId);
|
||||
if (handle === undefined) throw new Error(`Agent "${agentId}" does not exist`);
|
||||
return runAgentTurn(handle, request, {
|
||||
summaryPolicy: opts.summaryPolicy ?? this.summaryPolicyFor(handle),
|
||||
signal: opts.signal,
|
||||
onReady: opts.onReady,
|
||||
});
|
||||
child.accessor.get(IAgentProfileService).update({
|
||||
cwd: opts?.cwd ?? parentData.cwd,
|
||||
modelAlias: parentData.modelAlias,
|
||||
thinkingLevel: parentData.thinkingLevel,
|
||||
systemPrompt: parentData.systemPrompt,
|
||||
activeToolNames: parentData.activeToolNames,
|
||||
});
|
||||
return child;
|
||||
}
|
||||
|
||||
private summaryPolicyFor(handle: IAgentScopeHandle): AgentProfileSummaryPolicy | undefined {
|
||||
const profileName = handle.accessor.get(IAgentProfileService).data().profileName;
|
||||
if (profileName === undefined) return undefined;
|
||||
return this.catalog.get(profileName)?.summaryPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* One shared `McpConnectionManager` per session (built lazily, cached). All
|
||||
* agents in the session share it, matching v1's session-scoped MCP and
|
||||
* avoiding a reconnect storm per subagent. Connects the session-config
|
||||
* avoiding a reconnect storm per agent. Connects the session-config
|
||||
* servers merged with enabled plugin MCP servers (fire-and-forget; the
|
||||
* manager's `initialLoad` gates tool use via `waitForInitialLoad`).
|
||||
*/
|
||||
|
|
@ -236,119 +244,6 @@ export class AgentLifecycleService extends Disposable implements IAgentLifecycle
|
|||
await manager.connectAll(servers);
|
||||
}
|
||||
|
||||
// ── Subagent orchestration ─────────────────────────────────────────
|
||||
|
||||
async runSubagent(opts: RunSubagentOptions): Promise<SubagentRunHandle> {
|
||||
const { callerAgentId, profileName, signal } = opts;
|
||||
const caller = this.handles.get(callerAgentId);
|
||||
if (caller === undefined) throw new Error(`Caller agent "${callerAgentId}" does not exist`);
|
||||
|
||||
const profile = this.catalog.get(profileName);
|
||||
if (profile === undefined) throw new Error(`Unknown agent type: "${profileName}"`);
|
||||
|
||||
const child = await this.spawn(callerAgentId);
|
||||
applyProfileToAgent(child, profile);
|
||||
|
||||
const promptText = await this.withProfilePrefix(profile, opts.prompt);
|
||||
this.emitSubagentSpawned(caller, child.id, profileName, opts.metadata);
|
||||
this.telemetry?.track('subagent_created', {
|
||||
subagent_name: profileName,
|
||||
run_in_background: opts.metadata?.runInBackground ?? false,
|
||||
});
|
||||
|
||||
const observed = observeChildAgentTurn(
|
||||
caller,
|
||||
child,
|
||||
{ kind: 'prompt', prompt: promptText },
|
||||
{
|
||||
profileName,
|
||||
summaryPolicy: profile.summaryPolicy,
|
||||
suppressRateLimitFailureEvent: opts.suppressRateLimitFailureEvent,
|
||||
signal,
|
||||
onReady: opts.onReady,
|
||||
},
|
||||
);
|
||||
if (observed === undefined) throw new Error('Subagent turn could not be started');
|
||||
|
||||
return {
|
||||
agentId: child.id,
|
||||
profileName,
|
||||
completion: observed.completion.then((r) => ({ result: r.summary, usage: r.usage })),
|
||||
};
|
||||
}
|
||||
|
||||
async resumeSubagent(opts: ResumeSubagentOptions): Promise<SubagentRunHandle> {
|
||||
const { callerAgentId, agentId, signal } = opts;
|
||||
const caller = this.handles.get(callerAgentId);
|
||||
if (caller === undefined) throw new Error(`Caller agent "${callerAgentId}" does not exist`);
|
||||
|
||||
const child = this.handles.get(agentId);
|
||||
if (child === undefined) throw new Error(`Agent instance "${agentId}" does not exist`);
|
||||
|
||||
const profileName =
|
||||
child.accessor.get(IAgentProfileService).data().profileName ?? 'subagent';
|
||||
const profile = this.catalog.get(profileName);
|
||||
|
||||
this.emitSubagentSpawned(caller, agentId, profileName, opts.metadata);
|
||||
this.telemetry?.track('subagent_created', {
|
||||
subagent_name: profileName,
|
||||
run_in_background: opts.metadata?.runInBackground ?? false,
|
||||
});
|
||||
|
||||
const observed = observeChildAgentTurn(
|
||||
caller,
|
||||
child,
|
||||
{ kind: 'prompt', prompt: opts.prompt },
|
||||
{
|
||||
profileName,
|
||||
summaryPolicy: profile?.summaryPolicy,
|
||||
signal,
|
||||
},
|
||||
);
|
||||
if (observed === undefined) throw new Error('Subagent turn could not be started');
|
||||
|
||||
return {
|
||||
agentId,
|
||||
profileName,
|
||||
completion: observed.completion.then((r) => ({ result: r.summary, usage: r.usage })),
|
||||
};
|
||||
}
|
||||
|
||||
private emitSubagentSpawned(
|
||||
caller: IAgentScopeHandle,
|
||||
subagentId: string,
|
||||
profileName: string,
|
||||
metadata?: RunSubagentOptions['metadata'],
|
||||
): void {
|
||||
caller.accessor.get(IAgentRecordService)?.signal({
|
||||
type: 'subagent.spawned',
|
||||
subagentId,
|
||||
subagentName: profileName,
|
||||
parentToolCallId: metadata?.parentToolCallId ?? '',
|
||||
callerAgentId: caller.id,
|
||||
description: metadata?.description,
|
||||
swarmIndex: metadata?.swarmIndex,
|
||||
runInBackground: metadata?.runInBackground ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
private async withProfilePrefix(
|
||||
profile: AgentProfileDefinition,
|
||||
prompt: string,
|
||||
): Promise<string> {
|
||||
if (profile.promptPrefix === undefined) return prompt;
|
||||
try {
|
||||
const prefix = await profile.promptPrefix({
|
||||
cwd: this.workspace.workDir,
|
||||
runner: this.processRunner,
|
||||
log: this.log,
|
||||
});
|
||||
return prefix.length > 0 ? `${prefix}\n\n${prompt}` : prompt;
|
||||
} catch {
|
||||
return prompt;
|
||||
}
|
||||
}
|
||||
|
||||
getHandle(agentId: string): IAgentScopeHandle | undefined {
|
||||
return this.handles.get(agentId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* `agentLifecycle` domain (L6) — main-agent bootstrap helper.
|
||||
*
|
||||
* The main agent is an ordinary agent whose only distinction is
|
||||
* `agentId === 'main'`; `IAgentLifecycleService` itself knows nothing about
|
||||
* it. What *is* main-specific is session bootstrap business: the plugin
|
||||
* session-start injector registers its turn-cadence injection on the main
|
||||
* agent only (matching v1's `pluginSessionStarts: type === 'main' ? … :
|
||||
* undefined`). `ensureMainAgent` concentrates that business in one place so
|
||||
* every bootstrapper (session resume, legacy session/message services, the
|
||||
* server edge) creates the main agent the same way.
|
||||
*
|
||||
* Not a Service: a pure composition helper over the session handle.
|
||||
*/
|
||||
|
||||
import type { ISessionScopeHandle, IAgentScopeHandle } from '#/_base/di/scope';
|
||||
import { IPluginSessionStartInjectorService } from '#/agent/contextInjector';
|
||||
import type { BindAgentInput } from '#/agent/profile';
|
||||
|
||||
import { IAgentLifecycleService } from './agentLifecycle';
|
||||
|
||||
export const MAIN_AGENT_ID = 'main';
|
||||
|
||||
export interface EnsureMainAgentOptions {
|
||||
/** Profile + Model to bind at creation. Omit for an edge-bound main agent. */
|
||||
readonly binding?: BindAgentInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the session's main agent, creating it (with its session-start
|
||||
* bootstrap wiring) when it does not exist yet.
|
||||
*/
|
||||
export async function ensureMainAgent(
|
||||
session: ISessionScopeHandle,
|
||||
opts?: EnsureMainAgentOptions,
|
||||
): Promise<IAgentScopeHandle> {
|
||||
const agents = session.accessor.get(IAgentLifecycleService);
|
||||
const existing = agents.getHandle(MAIN_AGENT_ID);
|
||||
if (existing !== undefined) return existing;
|
||||
const main = await agents.create({ agentId: MAIN_AGENT_ID, binding: opts?.binding });
|
||||
// Force-instantiate the plugin session-start injector so it registers its
|
||||
// turn-cadence injection before the first turn. Main-agent-only business.
|
||||
main.accessor.get(IPluginSessionStartInjectorService);
|
||||
// Notify main-only capabilities (e.g. the cron tool registrar) that the main
|
||||
// agent is ready, so they bind to it without filtering every `onDidCreate`.
|
||||
agents.notifyMainCreated(main);
|
||||
return main;
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
/**
|
||||
* `agentLifecycle` domain (L6) — caller-side mirroring of an agent run.
|
||||
*
|
||||
* When one agent drives another through `IAgentLifecycleService.run` (the
|
||||
* `Agent` tool, the swarm scheduler), the *requesting* agent surfaces that run
|
||||
* on its own record stream so the UI can nest the child transcript under the
|
||||
* launching tool call, external hooks fire, and telemetry is tracked. That
|
||||
* requester ↔ target association is business data of this wrapper layer — the
|
||||
* lifecycle registry itself stays flat and knows nothing about it.
|
||||
*
|
||||
* External hooks (`SubagentStart` / `SubagentStop`) are invoked directly on
|
||||
* the requester's `IAgentExternalHooksService` — there is no intermediary
|
||||
* hook service; the tool wrapper is just a thin layer over the lifecycle.
|
||||
*
|
||||
* Wire shape note: the record signals are still named `subagent.spawned /
|
||||
* started / completed / failed` and telemetry still tracks `subagent_created`
|
||||
* so existing session recordings and dashboards stay valid. Rename lives on a
|
||||
* separate wire-cleanup PR.
|
||||
*/
|
||||
|
||||
import type { IAgentScopeHandle } from '#/_base/di/scope';
|
||||
import { userCancellationReason } from '#/_base/utils/abort';
|
||||
import { isProviderRateLimitError, type TokenUsage } from '#/app/llmProtocol';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { IAgentExternalHooksService } from '#/agent/externalHooks';
|
||||
import { IAgentRecordService } from '#/agent/record';
|
||||
import { isAbortError } from '#/agent/loop/errors';
|
||||
|
||||
import type { AgentRunHandle } from './agentLifecycle';
|
||||
|
||||
const HOOK_TEXT_PREVIEW_LENGTH = 500;
|
||||
|
||||
export interface AgentRunSpawnedMeta {
|
||||
readonly profileName: string;
|
||||
readonly parentToolCallId?: string;
|
||||
readonly parentToolCallUuid?: string;
|
||||
readonly description?: string;
|
||||
readonly swarmIndex?: number;
|
||||
readonly runInBackground?: boolean;
|
||||
}
|
||||
|
||||
export interface MirrorAgentRunOptions {
|
||||
/** Profile the target runs under; only used for hooks / record labels. */
|
||||
readonly profileName: string;
|
||||
/**
|
||||
* Prompt text submitted to the target. When present the `SubagentStart`
|
||||
* external hook fires (and may block the run); omit for retry turns, which
|
||||
* skip the hook.
|
||||
*/
|
||||
readonly prompt?: string;
|
||||
/** Skip the requester-side `subagent.failed` record for provider-rate-limit / aborted failures. */
|
||||
readonly suppressRateLimitFailureEvent?: boolean;
|
||||
/** The requester's cancellation signal (passed through to the start hook). */
|
||||
readonly signal: AbortSignal;
|
||||
/** Called to abort the underlying run when the start hook blocks it. */
|
||||
readonly cancel?: (reason?: unknown) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the requester-side "an agent run was launched" record + telemetry.
|
||||
* Called once per launch (spawn or resume), before or right after the run is
|
||||
* submitted, because it carries tool-call provenance (`parentToolCallId`,
|
||||
* `swarmIndex`, `runInBackground`) only the requester knows.
|
||||
*/
|
||||
export function emitAgentRunSpawned(
|
||||
requester: IAgentScopeHandle,
|
||||
targetAgentId: string,
|
||||
meta: AgentRunSpawnedMeta,
|
||||
): void {
|
||||
requester.accessor.get(IAgentRecordService)?.signal({
|
||||
type: 'subagent.spawned',
|
||||
subagentId: targetAgentId,
|
||||
subagentName: meta.profileName,
|
||||
parentToolCallId: meta.parentToolCallId ?? '',
|
||||
parentToolCallUuid: meta.parentToolCallUuid,
|
||||
callerAgentId: requester.id,
|
||||
description: meta.description,
|
||||
swarmIndex: meta.swarmIndex,
|
||||
runInBackground: meta.runInBackground ?? false,
|
||||
});
|
||||
requester.accessor.get(ITelemetryService)?.track('subagent_created', {
|
||||
subagent_name: meta.profileName,
|
||||
run_in_background: meta.runInBackground ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror a running agent turn onto the requester's record stream + external
|
||||
* hooks and await its completion. Returns the distilled summary/usage;
|
||||
* rethrows the run's failure after emitting the requester-side failure record.
|
||||
*/
|
||||
export async function mirrorAgentRun(
|
||||
requester: IAgentScopeHandle,
|
||||
run: AgentRunHandle,
|
||||
options: MirrorAgentRunOptions,
|
||||
): Promise<{ summary: string; usage?: TokenUsage }> {
|
||||
const record = requester.accessor.get(IAgentRecordService);
|
||||
const externalHooks = requester.accessor.get(IAgentExternalHooksService);
|
||||
record?.signal({ type: 'subagent.started', subagentId: run.agentId });
|
||||
if (options.prompt !== undefined) {
|
||||
try {
|
||||
await externalHooks?.runAgentTaskStart({
|
||||
agentName: options.profileName,
|
||||
prompt: options.prompt.slice(0, HOOK_TEXT_PREVIEW_LENGTH),
|
||||
signal: options.signal,
|
||||
});
|
||||
} catch (error) {
|
||||
options.cancel?.(error);
|
||||
void run.completion.catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
if (options.signal.aborted) {
|
||||
const reason: unknown = options.signal.reason ?? userCancellationReason();
|
||||
options.cancel?.(reason);
|
||||
void run.completion.catch(() => {});
|
||||
throw reason;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const result = await run.completion;
|
||||
record?.signal({
|
||||
type: 'subagent.completed',
|
||||
subagentId: run.agentId,
|
||||
resultSummary: result.summary,
|
||||
usage: result.usage,
|
||||
});
|
||||
externalHooks?.notifyAgentTaskStop({
|
||||
agentName: options.profileName,
|
||||
response: result.summary.slice(0, HOOK_TEXT_PREVIEW_LENGTH),
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (!isAbortError(error) && !shouldSuppressFailure(options, error)) {
|
||||
record?.signal({
|
||||
type: 'subagent.failed',
|
||||
subagentId: run.agentId,
|
||||
error: errorMessage(error),
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldSuppressFailure(options: MirrorAgentRunOptions, error: unknown): boolean {
|
||||
if (options.suppressRateLimitFailureEvent !== true) return false;
|
||||
if (isProviderRateLimitError(error)) return true;
|
||||
return isAbortError(error) || options.signal.aborted;
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
|
@ -1,291 +0,0 @@
|
|||
/**
|
||||
* `agentLifecycle` domain (L6) — helper that runs one prompt (or retry) turn on
|
||||
* a child agent, mirrors the child's turn lifecycle onto the caller's record
|
||||
* stream + subagent hooks, and distills a summary the caller can hand back to
|
||||
* its own tool result.
|
||||
*
|
||||
* Not a Service: `observeChildAgentTurn` is a pure function that borrows
|
||||
* `IAgentPromptService`, `IAgentContextMemoryService`, `IAgentUsageService`
|
||||
* from the child scope and `IAgentRecordService`, `IAgentToolService` from the
|
||||
* caller scope. It replaces the free-function `spawnChildAgent` /
|
||||
* `resumeChildAgent` orchestration under the old `agentTool` domain: the fork
|
||||
* step is now `IAgentLifecycleService.spawn`, profile application is
|
||||
* `applyProfileToAgent`, and the caller-side spawn record (`subagent.spawned`)
|
||||
* is emitted by the caller directly because it carries tool-call provenance
|
||||
* (`parentToolCallId`, `swarmIndex`, `runInBackground`) the observer does not
|
||||
* know about.
|
||||
*
|
||||
* The lifecycle is imperative — the caller (`Agent` tool, `sessionSwarmService`)
|
||||
* awaits the returned `completion` promise. Turn hooks are not used because
|
||||
* there is exactly one observer (the caller who spawned the child); a hook
|
||||
* indirection would only obscure the flow.
|
||||
*/
|
||||
|
||||
import {
|
||||
APIProviderRateLimitError,
|
||||
isProviderRateLimitError,
|
||||
type TokenUsage,
|
||||
} from '#/app/llmProtocol';
|
||||
|
||||
import { linkAbortSignal, userCancellationReason } from '#/_base/utils/abort';
|
||||
import type { IAgentScopeHandle } from '#/_base/di/scope';
|
||||
import {
|
||||
IAgentContextMemoryService,
|
||||
type ContextMessage,
|
||||
type PromptOrigin,
|
||||
} from '#/agent/contextMemory';
|
||||
import { ErrorCodes, toKimiErrorPayload, type KimiErrorPayload } from '#/errors';
|
||||
import { IAgentRecordService } from '#/agent/record';
|
||||
import { IAgentToolService } from '#/agent/agentTool';
|
||||
import { isAbortError } from '#/agent/loop/errors';
|
||||
import { IAgentPromptService } from '#/agent/prompt';
|
||||
import { IAgentUsageService } from '#/agent/usage';
|
||||
import type { Turn } from '#/agent/turn';
|
||||
import type { AgentProfileSummaryPolicy } from '#/app/agentProfileCatalog';
|
||||
|
||||
/**
|
||||
* Legacy `PromptOrigin` tag emitted by the `Agent` tool and swarm scheduler
|
||||
* when they submit a prompt to a child agent. Wire shape kept unchanged
|
||||
* (`kind: 'system_trigger', name: 'subagent'`) so existing session recordings
|
||||
* replay against v2 without a protocol schema bump. Rename lives on a separate
|
||||
* wire-cleanup PR.
|
||||
*/
|
||||
export const CHILD_AGENT_PROMPT_ORIGIN: PromptOrigin = {
|
||||
kind: 'system_trigger',
|
||||
name: 'subagent',
|
||||
};
|
||||
|
||||
const HOOK_TEXT_PREVIEW_LENGTH = 500;
|
||||
|
||||
export type ChildAgentTurnRequest =
|
||||
| { readonly kind: 'prompt'; readonly prompt: string }
|
||||
| { readonly kind: 'retry'; readonly trigger?: string };
|
||||
|
||||
export interface ObserveChildAgentTurnOptions {
|
||||
/** Profile the child was configured under; only used for external hooks / record labels. */
|
||||
readonly profileName: string;
|
||||
/** When set, drives a continuation-prompt loop when the child's summary is too short. */
|
||||
readonly summaryPolicy?: AgentProfileSummaryPolicy;
|
||||
/** Skip the caller-side `subagent.failed` record for provider-rate-limit / aborted failures. */
|
||||
readonly suppressRateLimitFailureEvent?: boolean;
|
||||
/** Caller's cancellation signal. Aborting it cancels the child's turn. */
|
||||
readonly signal: AbortSignal;
|
||||
/** Fires once the child's first request is committed (used by swarm to fan out). */
|
||||
readonly onReady?: () => void;
|
||||
}
|
||||
|
||||
export interface ObservedChildAgentTurn {
|
||||
readonly turn: Turn;
|
||||
readonly completion: Promise<{ readonly summary: string; readonly usage?: TokenUsage }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a prompt (or a retry) to `child`, wire the caller's record/hook
|
||||
* projections and the summary-distillation policy, and return the running
|
||||
* `Turn` plus a promise of the distilled summary/usage.
|
||||
*
|
||||
* Returns `undefined` when the underlying `IAgentPromptService.prompt/retry`
|
||||
* refuses to launch a turn (busy / no head).
|
||||
*/
|
||||
export function observeChildAgentTurn(
|
||||
caller: IAgentScopeHandle,
|
||||
child: IAgentScopeHandle,
|
||||
request: ChildAgentTurnRequest,
|
||||
options: ObserveChildAgentTurnOptions,
|
||||
): ObservedChildAgentTurn | undefined {
|
||||
options.signal.throwIfAborted();
|
||||
const promptService = child.accessor.get(IAgentPromptService);
|
||||
const turn =
|
||||
request.kind === 'prompt'
|
||||
? promptService.prompt({
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: request.prompt }],
|
||||
toolCalls: [],
|
||||
origin: CHILD_AGENT_PROMPT_ORIGIN,
|
||||
})
|
||||
: promptService.retry(request.trigger ?? 'agent-host');
|
||||
if (turn === undefined) return undefined;
|
||||
|
||||
if (options.onReady !== undefined) {
|
||||
void turn.ready.then(() => options.onReady?.()).catch(() => {});
|
||||
}
|
||||
|
||||
const completion = runObservation(caller, child, turn, request, options);
|
||||
return { turn, completion };
|
||||
}
|
||||
|
||||
async function runObservation(
|
||||
caller: IAgentScopeHandle,
|
||||
child: IAgentScopeHandle,
|
||||
turn: Turn,
|
||||
request: ChildAgentTurnRequest,
|
||||
options: ObserveChildAgentTurnOptions,
|
||||
): Promise<{ summary: string; usage?: TokenUsage }> {
|
||||
const controller = new AbortController();
|
||||
const unlink = linkAbortSignal(options.signal, controller);
|
||||
const record = caller.accessor.get(IAgentRecordService);
|
||||
const agentTool = caller.accessor.get(IAgentToolService);
|
||||
let turnRef: Turn = turn;
|
||||
record?.signal({ type: 'subagent.started', subagentId: child.id });
|
||||
if (request.kind === 'prompt') {
|
||||
try {
|
||||
await agentTool.hooks.onWillRunSubagent.run({
|
||||
agentName: options.profileName,
|
||||
prompt: request.prompt.slice(0, HOOK_TEXT_PREVIEW_LENGTH),
|
||||
signal: options.signal,
|
||||
});
|
||||
} catch (error) {
|
||||
unlink();
|
||||
throw error;
|
||||
}
|
||||
if (options.signal.aborted) {
|
||||
unlink();
|
||||
throw options.signal.reason ?? userCancellationReason();
|
||||
}
|
||||
}
|
||||
try {
|
||||
const result = await awaitTurn(turnRef, controller);
|
||||
classifyTurnResult(result);
|
||||
const summary = await distillSummary(child, controller, options.summaryPolicy, (t) => {
|
||||
turnRef = t;
|
||||
});
|
||||
const usage = child.accessor.get(IAgentUsageService)?.status().total;
|
||||
record?.signal({
|
||||
type: 'subagent.completed',
|
||||
subagentId: child.id,
|
||||
resultSummary: summary,
|
||||
usage,
|
||||
});
|
||||
void agentTool.hooks.onDidRunSubagent.run({
|
||||
agentName: options.profileName,
|
||||
response: summary.slice(0, HOOK_TEXT_PREVIEW_LENGTH),
|
||||
}).catch(() => undefined);
|
||||
return { summary, usage };
|
||||
} catch (error) {
|
||||
if (!isAbortError(error) && !shouldSuppressFailure(options, error)) {
|
||||
record?.signal({
|
||||
type: 'subagent.failed',
|
||||
subagentId: child.id,
|
||||
error: errorMessage(error),
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
unlink();
|
||||
if (controller.signal.aborted) {
|
||||
turnRef.abortController.abort(controller.signal.reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function awaitTurn(
|
||||
turn: Turn,
|
||||
controller: AbortController,
|
||||
): Promise<{ reason: string; error?: unknown }> {
|
||||
const onAbort = (): void => {
|
||||
turn.abortController.abort(controller.signal.reason);
|
||||
};
|
||||
controller.signal.addEventListener('abort', onAbort, { once: true });
|
||||
try {
|
||||
return await Promise.race([turn.result, abortPromise(controller.signal)]);
|
||||
} finally {
|
||||
controller.signal.removeEventListener('abort', onAbort);
|
||||
}
|
||||
}
|
||||
|
||||
async function distillSummary(
|
||||
child: IAgentScopeHandle,
|
||||
controller: AbortController,
|
||||
policy: AgentProfileSummaryPolicy | undefined,
|
||||
setTurn: (turn: Turn) => void,
|
||||
): Promise<string> {
|
||||
const memory = child.accessor.get(IAgentContextMemoryService);
|
||||
let summary = latestAssistantText(memory.get());
|
||||
if (policy === undefined) return summary;
|
||||
if (summary.trim().length >= policy.minChars) return summary;
|
||||
|
||||
const promptService = child.accessor.get(IAgentPromptService);
|
||||
for (let attempt = 0; attempt < policy.retries; attempt++) {
|
||||
const turn = promptService.prompt({
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: policy.continuationPrompt }],
|
||||
toolCalls: [],
|
||||
origin: CHILD_AGENT_PROMPT_ORIGIN,
|
||||
});
|
||||
if (turn === undefined) break;
|
||||
setTurn(turn);
|
||||
const result = await awaitTurn(turn, controller);
|
||||
if (result.reason !== 'completed') break;
|
||||
const continued = latestAssistantText(memory.get());
|
||||
if (continued.trim().length > 0) summary = continued;
|
||||
if (summary.trim().length >= policy.minChars) break;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
function classifyTurnResult(result: { reason: string; error?: unknown }): void {
|
||||
if (result.reason === 'filtered') {
|
||||
throw new Error('Subagent turn blocked by provider safety policy');
|
||||
}
|
||||
if (result.reason === 'failed') {
|
||||
const error = result.error;
|
||||
if (isProviderRateLimitError(error)) throw error;
|
||||
const payload = toKimiErrorPayload(error);
|
||||
if (payload.code === ErrorCodes.PROVIDER_RATE_LIMIT) {
|
||||
throw providerRateLimitErrorFromPayload(payload);
|
||||
}
|
||||
throw error instanceof Error ? error : new Error(String(error ?? 'Subagent turn failed'));
|
||||
}
|
||||
if (result.reason === 'cancelled') {
|
||||
throw userCancellationReason();
|
||||
}
|
||||
}
|
||||
|
||||
function shouldSuppressFailure(
|
||||
options: ObserveChildAgentTurnOptions,
|
||||
error: unknown,
|
||||
): boolean {
|
||||
if (options.suppressRateLimitFailureEvent !== true) return false;
|
||||
if (isProviderRateLimitError(error)) return true;
|
||||
return isAbortError(error) || options.signal.aborted;
|
||||
}
|
||||
|
||||
function providerRateLimitErrorFromPayload(error: KimiErrorPayload): APIProviderRateLimitError {
|
||||
const requestId =
|
||||
typeof error.details?.['requestId'] === 'string' ? error.details['requestId'] : null;
|
||||
return new APIProviderRateLimitError(error.message, requestId);
|
||||
}
|
||||
|
||||
function abortPromise(signal: AbortSignal): Promise<never> {
|
||||
if (signal.aborted) {
|
||||
return Promise.reject(signal.reason ?? userCancellationReason());
|
||||
}
|
||||
return new Promise<never>((_resolve, reject) => {
|
||||
signal.addEventListener(
|
||||
'abort',
|
||||
() => reject(signal.reason ?? userCancellationReason()),
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function latestAssistantText(messages: readonly ContextMessage[]): string {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i]!;
|
||||
if (message.role !== 'assistant') continue;
|
||||
return contentText(message.content);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function contentText(content: ContextMessage['content']): string {
|
||||
if (typeof content === 'string') return content;
|
||||
return content
|
||||
.filter((part): part is Extract<(typeof content)[number], { type: 'text' }> => part.type === 'text')
|
||||
.map((part) => part.text)
|
||||
.join('');
|
||||
}
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
/**
|
||||
* `agentLifecycle` domain (L6) — helper that runs one prompt (or retry) turn on
|
||||
* an agent and distills a summary from its context once the turn ends.
|
||||
*
|
||||
* Not a Service: `runAgentTurn` is a pure function that borrows
|
||||
* `IAgentPromptService`, `IAgentContextMemoryService`, `IAgentUsageService`
|
||||
* from the target agent's scope. It has no notion of a caller: it emits no
|
||||
* record signals, runs no hooks, and tracks no telemetry. Callers that want to
|
||||
* surface the run on their own record stream (the `Agent` tool, the swarm
|
||||
* scheduler) compose this with `mirrorAgentRun` from the `agentTool` domain.
|
||||
*
|
||||
* The lifecycle is imperative — the caller awaits the returned `completion`
|
||||
* promise. Turn hooks are not used because there is exactly one observer (the
|
||||
* caller who requested the run); a hook indirection would only obscure the
|
||||
* flow.
|
||||
*/
|
||||
|
||||
import {
|
||||
APIProviderRateLimitError,
|
||||
isProviderRateLimitError,
|
||||
type TokenUsage,
|
||||
} from '#/app/llmProtocol';
|
||||
|
||||
import { linkAbortSignal, userCancellationReason } from '#/_base/utils/abort';
|
||||
import type { IAgentScopeHandle } from '#/_base/di/scope';
|
||||
import {
|
||||
IAgentContextMemoryService,
|
||||
type ContextMessage,
|
||||
type PromptOrigin,
|
||||
} from '#/agent/contextMemory';
|
||||
import { ErrorCodes, toKimiErrorPayload, type KimiErrorPayload } from '#/errors';
|
||||
import { IAgentPromptService } from '#/agent/prompt';
|
||||
import { IAgentUsageService } from '#/agent/usage';
|
||||
import type { Turn } from '#/agent/turn';
|
||||
import type { AgentProfileSummaryPolicy } from '#/app/agentProfileCatalog';
|
||||
|
||||
import type { AgentRunHandle, AgentRunRequest } from './agentLifecycle';
|
||||
|
||||
/**
|
||||
* Legacy `PromptOrigin` tag emitted when one agent submits a prompt to another
|
||||
* (the `Agent` tool, swarm scheduler, …). Wire shape kept unchanged
|
||||
* (`kind: 'system_trigger', name: 'subagent'`) so existing session recordings
|
||||
* replay against v2 without a protocol schema bump. Rename lives on a separate
|
||||
* wire-cleanup PR.
|
||||
*/
|
||||
export const AGENT_RUN_PROMPT_ORIGIN: PromptOrigin = {
|
||||
kind: 'system_trigger',
|
||||
name: 'subagent',
|
||||
};
|
||||
|
||||
export interface RunAgentTurnOptions {
|
||||
/** When set, drives a continuation-prompt loop when the agent's summary is too short. */
|
||||
readonly summaryPolicy?: AgentProfileSummaryPolicy;
|
||||
/** Cancellation signal. Aborting it cancels the agent's turn. */
|
||||
readonly signal: AbortSignal;
|
||||
/** Fires once the turn's first request is committed (used by swarm to fan out). */
|
||||
readonly onReady?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* `IAgentPromptService.prompt/retry` refuses to launch a turn (busy / no head).
|
||||
*/
|
||||
export function runAgentTurn(
|
||||
target: IAgentScopeHandle,
|
||||
request: AgentRunRequest,
|
||||
options: RunAgentTurnOptions,
|
||||
): AgentRunHandle {
|
||||
options.signal.throwIfAborted();
|
||||
const promptService = target.accessor.get(IAgentPromptService);
|
||||
const turn =
|
||||
request.kind === 'prompt'
|
||||
? promptService.prompt({
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: request.prompt }],
|
||||
toolCalls: [],
|
||||
origin: AGENT_RUN_PROMPT_ORIGIN,
|
||||
})
|
||||
: promptService.retry(request.trigger ?? 'agent-host');
|
||||
if (turn === undefined) throw new Error('Agent turn could not be started');
|
||||
|
||||
if (options.onReady !== undefined) {
|
||||
void turn.ready.then(() => options.onReady?.()).catch(() => {});
|
||||
}
|
||||
|
||||
const completion = awaitRun(target, turn, options);
|
||||
return { agentId: target.id, turn, completion };
|
||||
}
|
||||
|
||||
async function awaitRun(
|
||||
target: IAgentScopeHandle,
|
||||
turn: Turn,
|
||||
options: RunAgentTurnOptions,
|
||||
): Promise<{ summary: string; usage?: TokenUsage }> {
|
||||
const controller = new AbortController();
|
||||
const unlink = linkAbortSignal(options.signal, controller);
|
||||
let turnRef: Turn = turn;
|
||||
try {
|
||||
const result = await awaitTurn(turnRef, controller);
|
||||
classifyTurnResult(result);
|
||||
const summary = await distillSummary(target, controller, options.summaryPolicy, (t) => {
|
||||
turnRef = t;
|
||||
});
|
||||
const usage = target.accessor.get(IAgentUsageService)?.status().total;
|
||||
return { summary, usage };
|
||||
} finally {
|
||||
unlink();
|
||||
if (controller.signal.aborted) {
|
||||
turnRef.abortController.abort(controller.signal.reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function awaitTurn(
|
||||
turn: Turn,
|
||||
controller: AbortController,
|
||||
): Promise<{ reason: string; error?: unknown }> {
|
||||
const onAbort = (): void => {
|
||||
turn.abortController.abort(controller.signal.reason);
|
||||
};
|
||||
controller.signal.addEventListener('abort', onAbort, { once: true });
|
||||
try {
|
||||
return await Promise.race([turn.result, abortPromise(controller.signal)]);
|
||||
} finally {
|
||||
controller.signal.removeEventListener('abort', onAbort);
|
||||
}
|
||||
}
|
||||
|
||||
async function distillSummary(
|
||||
target: IAgentScopeHandle,
|
||||
controller: AbortController,
|
||||
policy: AgentProfileSummaryPolicy | undefined,
|
||||
setTurn: (turn: Turn) => void,
|
||||
): Promise<string> {
|
||||
const memory = target.accessor.get(IAgentContextMemoryService);
|
||||
let summary = latestAssistantText(memory.get());
|
||||
if (policy === undefined) return summary;
|
||||
if (summary.trim().length >= policy.minChars) return summary;
|
||||
|
||||
const promptService = target.accessor.get(IAgentPromptService);
|
||||
for (let attempt = 0; attempt < policy.retries; attempt++) {
|
||||
const turn = promptService.prompt({
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: policy.continuationPrompt }],
|
||||
toolCalls: [],
|
||||
origin: AGENT_RUN_PROMPT_ORIGIN,
|
||||
});
|
||||
if (turn === undefined) break;
|
||||
setTurn(turn);
|
||||
const result = await awaitTurn(turn, controller);
|
||||
if (result.reason !== 'completed') break;
|
||||
const continued = latestAssistantText(memory.get());
|
||||
if (continued.trim().length > 0) summary = continued;
|
||||
if (summary.trim().length >= policy.minChars) break;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
function classifyTurnResult(result: { reason: string; error?: unknown }): void {
|
||||
if (result.reason === 'filtered') {
|
||||
throw new Error('Agent turn blocked by provider safety policy');
|
||||
}
|
||||
if (result.reason === 'failed') {
|
||||
const error = result.error;
|
||||
if (isProviderRateLimitError(error)) throw error;
|
||||
const payload = toKimiErrorPayload(error);
|
||||
if (payload.code === ErrorCodes.PROVIDER_RATE_LIMIT) {
|
||||
throw providerRateLimitErrorFromPayload(payload);
|
||||
}
|
||||
throw toRunError(error);
|
||||
}
|
||||
if (result.reason === 'cancelled') {
|
||||
throw userCancellationReason();
|
||||
}
|
||||
}
|
||||
|
||||
function toRunError(error: unknown): Error {
|
||||
if (error instanceof Error) return error;
|
||||
if (error === undefined || error === null) return new Error('Agent turn failed');
|
||||
return new Error(stringifyRunError(error));
|
||||
}
|
||||
|
||||
function stringifyRunError(value: unknown): string {
|
||||
if (typeof value === 'string') return value;
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function providerRateLimitErrorFromPayload(error: KimiErrorPayload): APIProviderRateLimitError {
|
||||
const requestId =
|
||||
typeof error.details?.['requestId'] === 'string' ? error.details['requestId'] : null;
|
||||
return new APIProviderRateLimitError(error.message, requestId);
|
||||
}
|
||||
|
||||
function abortPromise(signal: AbortSignal): Promise<never> {
|
||||
if (signal.aborted) {
|
||||
return Promise.reject(signal.reason ?? userCancellationReason());
|
||||
}
|
||||
return new Promise<never>((_resolve, reject) => {
|
||||
signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
reject(signal.reason ?? userCancellationReason());
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function latestAssistantText(messages: readonly ContextMessage[]): string {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i]!;
|
||||
if (message.role !== 'assistant') continue;
|
||||
return contentText(message.content);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function contentText(content: ContextMessage['content']): string {
|
||||
if (typeof content === 'string') return content;
|
||||
return content
|
||||
.filter((part): part is Extract<(typeof content)[number], { type: 'text' }> => part.type === 'text')
|
||||
.map((part) => part.text)
|
||||
.join('');
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
/**
|
||||
* `agentLifecycle` domain (L6) — the `Agent` collaboration tool.
|
||||
*
|
||||
* Thin adapter over `IAgentLifecycleService.runSubagent` /
|
||||
* `resumeSubagent`. The tool owns only the LLM-facing surface: JSON
|
||||
* schema + tool description, approval rule, task registration
|
||||
* (so the LLM can see the child under TaskList/TaskOutput/TaskStop when
|
||||
* `run_in_background=true` or after detach), and terminal text
|
||||
* formatting.
|
||||
* The LLM-facing wrapper over `IAgentLifecycleService`: translates the tool
|
||||
* args into a Profile + Model binding, creates (or resumes) an agent, drives
|
||||
* one turn via `run`, and mirrors the run onto the calling agent's record
|
||||
* stream (`mirrorAgentRun`). The tool also owns the JSON schema + description,
|
||||
* approval rule, background-task registration (so the LLM can see the run
|
||||
* under TaskList/TaskOutput/TaskStop when `run_in_background=true` or after
|
||||
* detach), and terminal text formatting.
|
||||
*
|
||||
* Registered via the module-level `registerTool(AgentTool)` at the bottom of
|
||||
* this file — the same "import = register" pattern used by every builtin tool.
|
||||
|
|
@ -33,12 +34,16 @@ import type {
|
|||
import { ToolAccesses } from '#/agent/tool';
|
||||
import { registerTool } from '#/agent/toolRegistry';
|
||||
import {
|
||||
applyProfilePromptPrefix,
|
||||
IAgentProfileCatalogService,
|
||||
type AgentProfileDefinition,
|
||||
type AgentProfile,
|
||||
} from '#/app/agentProfileCatalog';
|
||||
import { ILogService } from '#/app/log';
|
||||
import { ISessionProcessRunner } from '#/session/process';
|
||||
import { ISessionWorkspaceContext } from '#/session/workspaceContext';
|
||||
|
||||
import { IAgentLifecycleService } from '../agentLifecycle';
|
||||
import { emitAgentRunSpawned, mirrorAgentRun } from '../mirrorAgentRun';
|
||||
import { SubagentTask, type SubagentHandle } from './subagent-task';
|
||||
|
||||
import AGENT_BACKGROUND_DISABLED_DESCRIPTION from './agent-background-disabled.md?raw';
|
||||
|
|
@ -135,6 +140,8 @@ export class AgentTool implements BuiltinTool<AgentToolInput> {
|
|||
@IAgentScopeContext scopeContext: IAgentScopeContext,
|
||||
@IAgentTaskService private readonly tasks: IAgentTaskService,
|
||||
@IAgentProfileService private readonly profile: IAgentProfileService,
|
||||
@ISessionWorkspaceContext private readonly workspace: ISessionWorkspaceContext,
|
||||
@ISessionProcessRunner private readonly processRunner: ISessionProcessRunner,
|
||||
@ILogService private readonly log: ILogService,
|
||||
) {
|
||||
this.callerAgentId = scopeContext.agentId;
|
||||
|
|
@ -188,9 +195,99 @@ export class AgentTool implements BuiltinTool<AgentToolInput> {
|
|||
}
|
||||
|
||||
private resumeProfileName(agentId: string): string | undefined {
|
||||
const child = this.lifecycle.getHandle(agentId);
|
||||
if (child === undefined) return undefined;
|
||||
return child.accessor.get(IAgentProfileService).data().profileName;
|
||||
const target = this.lifecycle.getHandle(agentId);
|
||||
if (target === undefined) return undefined;
|
||||
return target.accessor.get(IAgentProfileService).data().profileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch (or resume) the target agent and start its turn: create from an
|
||||
* explicit Profile + Model binding inherited from this agent's own config,
|
||||
* submit the prompt via `lifecycle.run`, and mirror the run onto this
|
||||
* agent's record stream. Returns a handle whose completion carries the
|
||||
* distilled result text.
|
||||
*/
|
||||
private async launch(
|
||||
args: AgentToolInput,
|
||||
toolCallId: string,
|
||||
controller: AbortController,
|
||||
): Promise<SubagentHandle> {
|
||||
const requester = this.lifecycle.getHandle(this.callerAgentId);
|
||||
if (requester === undefined) {
|
||||
throw new Error(`Caller agent "${this.callerAgentId}" does not exist`);
|
||||
}
|
||||
|
||||
const resumeAgentId = args.resume?.trim();
|
||||
const isResume = resumeAgentId !== undefined && resumeAgentId.length > 0;
|
||||
|
||||
let agentId: string;
|
||||
let profileName: string;
|
||||
let promptText = args.prompt;
|
||||
if (isResume) {
|
||||
const target = this.lifecycle.getHandle(resumeAgentId);
|
||||
if (target === undefined) {
|
||||
throw new Error(`Agent instance "${resumeAgentId}" does not exist`);
|
||||
}
|
||||
agentId = target.id;
|
||||
profileName =
|
||||
target.accessor.get(IAgentProfileService).data().profileName ?? RESUMED_LABEL;
|
||||
} else {
|
||||
const requestedProfileName = args.subagent_type?.length
|
||||
? args.subagent_type
|
||||
: DEFAULT_PROFILE_NAME;
|
||||
const profile = this.catalog.get(requestedProfileName);
|
||||
if (profile === undefined) {
|
||||
throw new Error(`Unknown agent type: "${requestedProfileName}"`);
|
||||
}
|
||||
const own = this.profile.data();
|
||||
if (own.modelAlias === undefined) {
|
||||
throw new Error('Caller agent has no model bound');
|
||||
}
|
||||
// Explicit inheritance: the new agent runs the requested profile on this
|
||||
// agent's own model / thinking level / cwd (Profile + Model ⇒ Agent).
|
||||
const created = await this.lifecycle.create({
|
||||
binding: {
|
||||
profile: profile.name,
|
||||
model: own.modelAlias,
|
||||
thinking: own.thinkingLevel,
|
||||
cwd: own.cwd,
|
||||
},
|
||||
});
|
||||
agentId = created.id;
|
||||
profileName = profile.name;
|
||||
promptText = await applyProfilePromptPrefix(profile, args.prompt, {
|
||||
cwd: this.workspace.workDir,
|
||||
runner: this.processRunner,
|
||||
log: this.log,
|
||||
});
|
||||
}
|
||||
|
||||
const runInBackground = args.run_in_background === true;
|
||||
emitAgentRunSpawned(requester, agentId, {
|
||||
profileName,
|
||||
parentToolCallId: toolCallId,
|
||||
description: args.description,
|
||||
runInBackground,
|
||||
});
|
||||
|
||||
const run = this.lifecycle.run(
|
||||
agentId,
|
||||
{ kind: 'prompt', prompt: promptText },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
const mirrored = mirrorAgentRun(requester, run, {
|
||||
profileName,
|
||||
prompt: promptText,
|
||||
signal: controller.signal,
|
||||
cancel: (reason) => {
|
||||
controller.abort(reason);
|
||||
},
|
||||
});
|
||||
return {
|
||||
agentId,
|
||||
profileName,
|
||||
completion: mirrored.then((r) => ({ result: r.summary, usage: r.usage })),
|
||||
};
|
||||
}
|
||||
|
||||
private async execution(
|
||||
|
|
@ -213,13 +310,6 @@ export class AgentTool implements BuiltinTool<AgentToolInput> {
|
|||
return { output: BACKGROUND_AGENT_UNAVAILABLE, isError: true };
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
parentToolCallId: toolCallId,
|
||||
description: args.description,
|
||||
runInBackground,
|
||||
};
|
||||
|
||||
// Delegate spawn-or-resume + turn observation to the lifecycle service.
|
||||
const controller = new AbortController();
|
||||
const abortBeforeRegister = (): void => {
|
||||
controller.abort(signal.reason);
|
||||
|
|
@ -228,36 +318,16 @@ export class AgentTool implements BuiltinTool<AgentToolInput> {
|
|||
signal.addEventListener('abort', abortBeforeRegister, { once: true });
|
||||
}
|
||||
|
||||
let handle;
|
||||
let handle: SubagentHandle;
|
||||
try {
|
||||
handle = isResume
|
||||
? await this.lifecycle.resumeSubagent({
|
||||
callerAgentId: this.callerAgentId,
|
||||
agentId: resumeAgentId!,
|
||||
prompt: args.prompt,
|
||||
signal: controller.signal,
|
||||
metadata,
|
||||
})
|
||||
: await this.lifecycle.runSubagent({
|
||||
callerAgentId: this.callerAgentId,
|
||||
profileName: requestedProfileName ?? DEFAULT_PROFILE_NAME,
|
||||
prompt: args.prompt,
|
||||
signal: controller.signal,
|
||||
metadata,
|
||||
});
|
||||
handle = await this.launch(args, toolCallId, controller);
|
||||
} catch (error) {
|
||||
signal.removeEventListener('abort', abortBeforeRegister);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Wrap the lifecycle handle in a background task so the LLM can interact
|
||||
// Wrap the run handle in a background task so the LLM can interact
|
||||
// with it via TaskList / TaskOutput / TaskStop.
|
||||
const subagentHandle: SubagentHandle = {
|
||||
agentId: handle.agentId,
|
||||
profileName: handle.profileName,
|
||||
completion: handle.completion,
|
||||
};
|
||||
|
||||
let taskId: string;
|
||||
try {
|
||||
const registerOptions: RegisterAgentTaskOptions = {
|
||||
|
|
@ -266,18 +336,18 @@ export class AgentTool implements BuiltinTool<AgentToolInput> {
|
|||
signal: runInBackground ? undefined : signal,
|
||||
};
|
||||
taskId = this.tasks.registerTask(
|
||||
new SubagentTask(subagentHandle, args.description, controller),
|
||||
new SubagentTask(handle, args.description, controller),
|
||||
registerOptions,
|
||||
);
|
||||
signal.removeEventListener('abort', abortBeforeRegister);
|
||||
} catch (error) {
|
||||
controller.abort();
|
||||
void subagentHandle.completion.catch(() => {});
|
||||
void handle.completion.catch(() => {});
|
||||
signal.removeEventListener('abort', abortBeforeRegister);
|
||||
this.log?.warn('subagent task registration failed', {
|
||||
this.log?.warn('background agent task registration failed', {
|
||||
toolCallId,
|
||||
agentId: subagentHandle.agentId,
|
||||
subagentType: subagentHandle.profileName,
|
||||
agentId: handle.agentId,
|
||||
subagentType: handle.profileName,
|
||||
error,
|
||||
});
|
||||
return {
|
||||
|
|
@ -288,17 +358,17 @@ export class AgentTool implements BuiltinTool<AgentToolInput> {
|
|||
|
||||
if (runInBackground) {
|
||||
return {
|
||||
output: formatBackgroundAgentResult(taskId, subagentHandle, args.description, allowBackground),
|
||||
output: formatBackgroundAgentResult(taskId, handle, args.description, allowBackground),
|
||||
};
|
||||
}
|
||||
|
||||
const release = await this.tasks.waitForForegroundRelease(taskId);
|
||||
if (release === 'detached') {
|
||||
return {
|
||||
output: formatBackgroundAgentResult(taskId, subagentHandle, args.description, allowBackground),
|
||||
output: formatBackgroundAgentResult(taskId, handle, args.description, allowBackground),
|
||||
};
|
||||
}
|
||||
return await this.formatForegroundResult(taskId, subagentHandle);
|
||||
return await this.formatForegroundResult(taskId, handle);
|
||||
} catch (error) {
|
||||
return { output: `subagent error: ${launchErrorMessage(error, signal)}`, isError: true };
|
||||
}
|
||||
|
|
@ -334,7 +404,7 @@ registerTool(AgentTool);
|
|||
// ── formatting helpers ───────────────────────────────────────────────
|
||||
|
||||
function buildProfileDescriptions(
|
||||
profiles: readonly AgentProfileDefinition[],
|
||||
profiles: readonly AgentProfile[],
|
||||
): string {
|
||||
return profiles
|
||||
.map((profile) => {
|
||||
|
|
@ -342,10 +412,10 @@ function buildProfileDescriptions(
|
|||
(part): part is string => part !== undefined && part.length > 0,
|
||||
);
|
||||
const header = details.length === 0 ? `- ${profile.name}` : `- ${profile.name}: ${details.join(' ')}`;
|
||||
if (profile.activeToolNames === undefined || profile.activeToolNames.length === 0) {
|
||||
if (profile.tools.length === 0) {
|
||||
return header;
|
||||
}
|
||||
return `${header}\n Tools: ${profile.activeToolNames.join(', ')}`;
|
||||
return `${header}\n Tools: ${profile.tools.join(', ')}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ type SubagentCompletion = {
|
|||
readonly usage?: TokenUsage;
|
||||
};
|
||||
|
||||
/** Handle to an agent run launched by the `Agent` tool (or swarm). */
|
||||
export type SubagentHandle = {
|
||||
readonly agentId: string;
|
||||
readonly profileName: string;
|
||||
|
|
@ -19,9 +20,9 @@ export type SubagentHandle = {
|
|||
|
||||
export interface SubagentTaskInfo extends AgentTaskInfoBase {
|
||||
readonly kind: 'agent';
|
||||
/** Subagent identifier accepted by Agent(resume=...). */
|
||||
/** Agent identifier accepted by Agent(resume=...). */
|
||||
readonly agentId?: string;
|
||||
/** Subagent profile name. */
|
||||
/** Profile name of the agent. Wire DTO field name kept for compatibility. */
|
||||
readonly subagentType?: string;
|
||||
}
|
||||
|
||||
|
|
@ -40,8 +41,8 @@ function errorMessage(err: unknown): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create a `taskService.run()`-compatible executor that waits for a
|
||||
* subagent completion promise. Resolves with the subagent result on
|
||||
* Create a `taskService.run()`-compatible executor that waits for an
|
||||
* agent-run completion promise. Resolves with the agent's result on
|
||||
* success, throws on abort or failure.
|
||||
*/
|
||||
export function createSubagentExecutor(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
/**
|
||||
* `btw` domain — `ISessionBtwService` implementation.
|
||||
*
|
||||
* Clones the main agent into a side-question child: inherits profile/context via
|
||||
* `IAgentLifecycleService.clone`, then disables tool calls (deny-all permission
|
||||
* Forks the main agent into a side-question child: inherits profile/context via
|
||||
* `IAgentLifecycleService.fork`, then disables tool calls (deny-all permission
|
||||
* policy) and appends the side-channel system reminder. Bound at Session scope —
|
||||
* `clone('main')` is a session-level operation, so the service injects the
|
||||
* `fork('main')` is a session-level operation, so the service injects the
|
||||
* session's `IAgentLifecycleService` directly rather than resolving it through
|
||||
* the main agent's accessor.
|
||||
* the main agent's accessor. The main agent is guaranteed to exist by session
|
||||
* bootstrap (`ensureMainAgent`); forking a missing source throws.
|
||||
*/
|
||||
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
|
|
@ -28,7 +29,7 @@ export class SessionBtwService implements ISessionBtwService {
|
|||
) {}
|
||||
|
||||
async start(): Promise<string> {
|
||||
const child = await this.lifecycle.clone('main');
|
||||
const child = await this.lifecycle.fork('main');
|
||||
child.accessor
|
||||
.get(IAgentSystemReminderService)
|
||||
?.appendSystemReminder(SIDE_QUESTION_SYSTEM_REMINDER, {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import type { ContentPart } from '#/app/llmProtocol';
|
|||
|
||||
import { createDecorator } from '#/_base/di';
|
||||
import type { Turn } from '#/agent/turn';
|
||||
import type { CronTask, CronTaskInit } from '#/app/cronPersistence';
|
||||
import type { CronTask, CronTaskInit } from '#/app/cron';
|
||||
|
||||
export interface CronLoadOptions {
|
||||
readonly replace?: boolean;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@
|
|||
* (tick / coalesce / jitter / cursor), persists mutations through the
|
||||
* App-scoped `ICronTaskPersistence`, mirrors mutations onto `wireRecord` for
|
||||
* replay via the main agent's `IAgentRecordService` (cross-scope borrow),
|
||||
* and steers the main agent through `IAgentPromptService` when a task fires.
|
||||
* steers the main agent through `IAgentPromptService` when a task fires, and
|
||||
* registers the cron tools (`CronCreate` / `CronList` / `CronDelete`) into the
|
||||
* main agent's `IAgentToolRegistryService` once `IAgentLifecycleService`
|
||||
* signals `onDidCreateMain`. Bound at Session scope.
|
||||
* Bound at Session scope.
|
||||
*/
|
||||
|
||||
|
|
@ -17,37 +20,41 @@ import type { CronJobOrigin, CronMissedOrigin } from '@moonshot-ai/protocol';
|
|||
|
||||
import { Disposable, toDisposable } from '#/_base/di';
|
||||
import { InstantiationType } from '#/_base/di/extensions';
|
||||
import { IInstantiationService } from '#/_base/di/instantiation';
|
||||
import { type IAgentScopeHandle, LifecycleScope, registerScopedService } from '#/_base/di/scope';
|
||||
import { IntervalTimer } from '#/_base/utils';
|
||||
|
||||
import { IConfigService } from '#/app/config';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { ICronTaskPersistence, type CronTask, type CronTaskInit } from '#/app/cronPersistence';
|
||||
import {
|
||||
computeNextCronRun,
|
||||
type ClockSources,
|
||||
type CronConfig,
|
||||
type CronTask,
|
||||
type CronTaskInit,
|
||||
CRON_SECTION,
|
||||
DEFAULT_CRON_CONFIG,
|
||||
ICronTaskPersistence,
|
||||
jitteredNextCronRunMs,
|
||||
oneShotJitteredNextCronRunMs,
|
||||
parseCronExpression,
|
||||
type ParsedCronExpression,
|
||||
renderCronFireXml,
|
||||
resolveClockSources,
|
||||
SYSTEM_CLOCKS,
|
||||
} from '#/app/cron';
|
||||
import { ISessionContext } from '#/session/sessionContext';
|
||||
import { IAgentLifecycleService } from '#/session/agentLifecycle';
|
||||
import type { ContextMessage } from '#/agent/contextMemory';
|
||||
import { IAgentPromptService } from '#/agent/prompt';
|
||||
import { IAgentRecordService } from '#/agent/record';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
import type { Turn } from '#/agent/turn';
|
||||
import { IAgentTurnService } from '#/agent/turn';
|
||||
|
||||
import {
|
||||
type CronConfig,
|
||||
CRON_SECTION,
|
||||
DEFAULT_CRON_CONFIG,
|
||||
} from '#/agent/cron/configSection';
|
||||
import {
|
||||
computeNextCronRun,
|
||||
parseCronExpression,
|
||||
type ParsedCronExpression,
|
||||
} from '#/agent/cron/cron-expr';
|
||||
import { renderCronFireXml } from '#/agent/cron/format';
|
||||
import { jitteredNextCronRunMs, oneShotJitteredNextCronRunMs } from '#/agent/cron/jitter';
|
||||
import {
|
||||
resolveClockSources,
|
||||
SYSTEM_CLOCKS,
|
||||
type ClockSources,
|
||||
} from '#/agent/cron/clock';
|
||||
import { CronCreateTool } from './tools/cron-create';
|
||||
import { CronListTool } from './tools/cron-list';
|
||||
import { CronDeleteTool } from './tools/cron-delete';
|
||||
|
||||
import { ISessionCronService, type CronLoadOptions } from './sessionCronService';
|
||||
|
||||
|
|
@ -116,15 +123,14 @@ export class SessionCronServiceImpl extends Disposable implements ISessionCronSe
|
|||
resolveClockSources(this.cronConfig.clock, this.cronConfig.debug) ?? SYSTEM_CLOCKS;
|
||||
|
||||
this._register(
|
||||
this.agentLifecycle.onDidCreate((handle) => {
|
||||
if (handle.id !== 'main') return;
|
||||
this.wireMainAgent(handle);
|
||||
this.agentLifecycle.onDidCreateMain((handle) => {
|
||||
this.bindMainAgent(handle);
|
||||
}),
|
||||
);
|
||||
|
||||
const existingMain = this.agentLifecycle.getHandle('main');
|
||||
if (existingMain) {
|
||||
this.wireMainAgent(existingMain);
|
||||
this.bindMainAgent(existingMain);
|
||||
}
|
||||
|
||||
this._register(
|
||||
|
|
@ -134,7 +140,7 @@ export class SessionCronServiceImpl extends Disposable implements ISessionCronSe
|
|||
);
|
||||
}
|
||||
|
||||
private wireMainAgent(handle: IAgentScopeHandle): void {
|
||||
private bindMainAgent(handle: IAgentScopeHandle): void {
|
||||
const record = handle.accessor.get(IAgentRecordService);
|
||||
|
||||
record.define('cron.add', {
|
||||
|
|
@ -158,9 +164,24 @@ export class SessionCronServiceImpl extends Disposable implements ISessionCronSe
|
|||
await next();
|
||||
});
|
||||
|
||||
this.registerCronTools(handle);
|
||||
|
||||
void this.loadFromStore().then(() => this.start());
|
||||
}
|
||||
|
||||
private registerCronTools(handle: IAgentScopeHandle): void {
|
||||
const instantiation = handle.accessor.get(IInstantiationService);
|
||||
const registry = handle.accessor.get(IAgentToolRegistryService);
|
||||
const tools = [
|
||||
instantiation.createInstance(CronCreateTool, this.cronConfig.disabled),
|
||||
instantiation.createInstance(CronListTool),
|
||||
instantiation.createInstance(CronDeleteTool),
|
||||
];
|
||||
for (const tool of tools) {
|
||||
this._register(registry.register(tool, { source: 'builtin' }));
|
||||
}
|
||||
}
|
||||
|
||||
now(): number {
|
||||
return this.clocks.wallNow();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,28 +26,19 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { ExecutableTool as BuiltinTool, ToolExecution } from '#/agent/tool';
|
||||
import { registerTool } from '#/agent/toolRegistry';
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import { literalRulePattern } from '#/_base/tools/support/rule-match';
|
||||
import { IConfigService } from '#/app/config';
|
||||
import { ISessionCronService } from '#/session/cron';
|
||||
import {
|
||||
CRON_SECTION,
|
||||
DEFAULT_CRON_CONFIG,
|
||||
type CronConfig,
|
||||
} from '#/agent/cron/configSection';
|
||||
import {
|
||||
computeNextCronRun,
|
||||
cronToHuman,
|
||||
formatLocalIsoWithOffset,
|
||||
hasFireWithinYears,
|
||||
parseCronExpression,
|
||||
type ParsedCronExpression,
|
||||
} from '#/agent/cron/cron-expr';
|
||||
import { formatLocalIsoWithOffset } from '#/agent/cron/format';
|
||||
import {
|
||||
jitteredNextCronRunMs,
|
||||
oneShotJitteredNextCronRunMs,
|
||||
} from '#/agent/cron/jitter';
|
||||
parseCronExpression,
|
||||
type ParsedCronExpression,
|
||||
} from '#/app/cron';
|
||||
import CRON_CREATE_DESCRIPTION from './cron-create.md?raw';
|
||||
|
||||
// ── Constants ────────────────────────────────────────────────────────
|
||||
|
|
@ -322,13 +313,6 @@ export class CronCreateTool implements BuiltinTool<CronCreateInput> {
|
|||
}
|
||||
}
|
||||
|
||||
registerTool(CronCreateTool, {
|
||||
staticArgs: (accessor) => [
|
||||
accessor.get(IConfigService).get<CronConfig>(CRON_SECTION)?.disabled
|
||||
?? DEFAULT_CRON_CONFIG.disabled,
|
||||
],
|
||||
});
|
||||
|
||||
function formatOutput(o: CronCreateOutput): string {
|
||||
const lines = [
|
||||
`id: ${o.id}`,
|
||||
|
|
@ -38,7 +38,6 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { ExecutableTool as BuiltinTool, ToolExecution } from '#/agent/tool';
|
||||
import { registerTool } from '#/agent/toolRegistry';
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import { ISessionCronService } from '#/session/cron';
|
||||
import CRON_DELETE_DESCRIPTION from './cron-delete.md?raw';
|
||||
|
|
@ -116,5 +115,3 @@ export class CronDeleteTool implements BuiltinTool<CronDeleteInput> {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
registerTool(CronDeleteTool);
|
||||
|
|
@ -43,15 +43,14 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import type { ExecutableTool as BuiltinTool, ToolExecution } from '#/agent/tool';
|
||||
import { registerTool } from '#/agent/toolRegistry';
|
||||
import { toInputJsonSchema } from '#/_base/tools/support/input-schema';
|
||||
import { ISessionCronService } from '#/session/cron';
|
||||
import type { CronTask } from '#/app/cronPersistence';
|
||||
import {
|
||||
cronToHuman,
|
||||
formatLocalIsoWithOffset,
|
||||
parseCronExpression,
|
||||
} from '#/agent/cron/cron-expr';
|
||||
import { formatLocalIsoWithOffset } from '#/agent/cron/format';
|
||||
type CronTask,
|
||||
} from '#/app/cron';
|
||||
import CRON_LIST_DESCRIPTION from './cron-list.md?raw';
|
||||
|
||||
// ── Input schema ─────────────────────────────────────────────────────
|
||||
|
|
@ -170,5 +169,3 @@ export class CronListTool implements BuiltinTool<CronListInput> {
|
|||
].join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
registerTool(CronListTool);
|
||||
|
|
@ -15,8 +15,14 @@ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiatio
|
|||
export interface AgentMeta {
|
||||
/** Per-agent directory used as the wire-record `homedir` (persistence key). */
|
||||
readonly homedir: string;
|
||||
/** Agent this one was cloned / derived from (provenance only; not used by business logic). */
|
||||
/** Agent this one was forked / derived from (provenance only; not used by business logic). */
|
||||
readonly forkedFrom?: string;
|
||||
/**
|
||||
* Business-defined recorded values (e.g. the swarm's `swarmItem`), persisted
|
||||
* verbatim. Never interpreted by the lifecycle.
|
||||
*/
|
||||
readonly labels?: Readonly<Record<string, string>>;
|
||||
/** @deprecated Legacy on-disk field predating `labels`; read-compat only. */
|
||||
readonly swarmItem?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* Owns the burst-then-throttle launch ramp and the provider-rate-limit recovery
|
||||
* loop used by `SessionSwarmService`; drives each attempt through a
|
||||
* `SubagentBatchLauncher` and surfaces requeues via `suspended`. Pure scheduling
|
||||
* `AgentRunBatchLauncher` and surfaces requeues via `suspended`. Pure scheduling
|
||||
* logic — owns no scoped state. Not part of the public surface: only
|
||||
* `SessionSwarmService` imports it.
|
||||
*/
|
||||
|
|
@ -16,13 +16,13 @@ import type { SessionSwarmRunResult, SessionSwarmTask } from './sessionSwarm';
|
|||
|
||||
// ── Launcher contract ────────────────────────────────────────────────
|
||||
//
|
||||
// The scheduler drives child-agent attempts through a small launcher
|
||||
// The scheduler drives agent-run attempts through a small launcher
|
||||
// interface. Consumers (currently only `SessionSwarmService`) implement it
|
||||
// on top of `IAgentLifecycleService.spawn({ profile })` +
|
||||
// `observeChildAgentTurn`; the option shapes are defined here so the
|
||||
// scheduler has a stable contract regardless of how launches are wired.
|
||||
// on top of `IAgentLifecycleService.create({ binding })` + `run` +
|
||||
// `mirrorAgentRun`; the option shapes are defined here so the scheduler has
|
||||
// a stable contract regardless of how launches are wired.
|
||||
|
||||
export interface RunSubagentOptions {
|
||||
export interface AgentRunAttemptOptions {
|
||||
readonly parentToolCallId: string;
|
||||
readonly parentToolCallUuid?: string;
|
||||
readonly prompt: string;
|
||||
|
|
@ -34,12 +34,12 @@ export interface RunSubagentOptions {
|
|||
readonly suppressRateLimitFailureEvent?: boolean;
|
||||
}
|
||||
|
||||
export interface SpawnSubagentOptions extends RunSubagentOptions {
|
||||
export interface AgentSpawnAttemptOptions extends AgentRunAttemptOptions {
|
||||
readonly profileName: string;
|
||||
readonly swarmItem?: string;
|
||||
}
|
||||
|
||||
export type SubagentHandle = {
|
||||
export type AgentRunAttemptHandle = {
|
||||
readonly agentId: string;
|
||||
readonly profileName: string;
|
||||
readonly completion: Promise<{
|
||||
|
|
@ -49,7 +49,7 @@ export type SubagentHandle = {
|
|||
};
|
||||
|
||||
/*
|
||||
Subagent batch scheduling contract:
|
||||
Agent-run batch scheduling contract:
|
||||
Normal phase:
|
||||
- Return results in input order; empty input returns an empty list.
|
||||
- Start up to 5 tasks immediately, then 1 more every 700 ms while queued work remains. By default active tasks do not cap this ramp; when KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY is set to a positive integer, the ramp additionally stops while active tasks reach that cap, and resumes as tasks complete.
|
||||
|
|
@ -79,23 +79,23 @@ const RATE_LIMIT_SUSPENDED_REASON = 'Provider rate limit; subagent requeued for
|
|||
|
||||
const AGENT_SWARM_MAX_CONCURRENCY_ENV = 'KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY';
|
||||
|
||||
export type QueuedSubagentTask<T = unknown> = SessionSwarmTask<T>;
|
||||
export type QueuedAgentRunTask<T = unknown> = SessionSwarmTask<T>;
|
||||
|
||||
export type SubagentResult<T = unknown> = SessionSwarmRunResult<T>;
|
||||
export type AgentRunResult<T = unknown> = SessionSwarmRunResult<T>;
|
||||
|
||||
export type QueuedSubagentRunResult<T = unknown> = SessionSwarmRunResult<T>;
|
||||
export type QueuedAgentRunResult<T = unknown> = SessionSwarmRunResult<T>;
|
||||
|
||||
export type SubagentSuspendedEvent = {
|
||||
readonly task: QueuedSubagentTask;
|
||||
export type AgentRunSuspendedEvent = {
|
||||
readonly task: QueuedAgentRunTask;
|
||||
readonly agentId: string;
|
||||
readonly reason: string;
|
||||
};
|
||||
|
||||
export type SubagentBatchLauncher = {
|
||||
spawn(options: SpawnSubagentOptions): Promise<SubagentHandle>;
|
||||
resume(agentId: string, options: RunSubagentOptions): Promise<SubagentHandle>;
|
||||
retry(agentId: string, options: RunSubagentOptions): Promise<SubagentHandle>;
|
||||
suspended?(event: SubagentSuspendedEvent): void;
|
||||
export type AgentRunBatchLauncher = {
|
||||
spawn(options: AgentSpawnAttemptOptions): Promise<AgentRunAttemptHandle>;
|
||||
resume(agentId: string, options: AgentRunAttemptOptions): Promise<AgentRunAttemptHandle>;
|
||||
retry(agentId: string, options: AgentRunAttemptOptions): Promise<AgentRunAttemptHandle>;
|
||||
suspended?(event: AgentRunSuspendedEvent): void;
|
||||
};
|
||||
|
||||
type RateLimitedOutcome = {
|
||||
|
|
@ -104,11 +104,11 @@ type RateLimitedOutcome = {
|
|||
readonly error: string;
|
||||
};
|
||||
|
||||
type AttemptOutcome<T> = SubagentResult<T> | RateLimitedOutcome;
|
||||
type AttemptOutcome<T> = AgentRunResult<T> | RateLimitedOutcome;
|
||||
|
||||
type TaskState<T> = {
|
||||
readonly index: number;
|
||||
readonly task: QueuedSubagentTask<T>;
|
||||
readonly task: QueuedAgentRunTask<T>;
|
||||
agentId?: string;
|
||||
retryAgentId?: string;
|
||||
retryCount: number;
|
||||
|
|
@ -124,19 +124,19 @@ type ActiveAttempt<T> = {
|
|||
timedOut: boolean;
|
||||
};
|
||||
|
||||
export type SubagentBatchOptions = {
|
||||
export type AgentRunBatchOptions = {
|
||||
/**
|
||||
* Optional cap on how many subagents may run concurrently during the normal
|
||||
* Optional cap on how many agent runs may execute concurrently during the normal
|
||||
* phase. `undefined` means no cap (legacy ramp behavior). The rate-limit
|
||||
* phase is governed by its own capacity logic and is not affected.
|
||||
*/
|
||||
readonly maxConcurrency?: number;
|
||||
};
|
||||
|
||||
export class SubagentBatch<T> {
|
||||
export class AgentRunBatch<T> {
|
||||
private readonly states: Array<TaskState<T>>;
|
||||
private readonly pending: Array<TaskState<T>>;
|
||||
private readonly results: Array<SubagentResult<T> | undefined>;
|
||||
private readonly results: Array<AgentRunResult<T> | undefined>;
|
||||
private readonly active = new Set<ActiveAttempt<T>>();
|
||||
private readonly controller = new AbortController();
|
||||
private readonly batchSignal: AbortSignal | undefined;
|
||||
|
|
@ -145,7 +145,7 @@ export class SubagentBatch<T> {
|
|||
private normalLaunchCount = 0;
|
||||
private normalLaunchTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
private rateLimitLaunchTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
private resolve: ((results: Array<SubagentResult<T>>) => void) | undefined;
|
||||
private resolve: ((results: Array<AgentRunResult<T>>) => void) | undefined;
|
||||
private reject: ((error: unknown) => void) | undefined;
|
||||
private finished = false;
|
||||
private started = false;
|
||||
|
|
@ -159,9 +159,9 @@ export class SubagentBatch<T> {
|
|||
private nextRateLimitLaunchAt = 0;
|
||||
|
||||
constructor(
|
||||
private readonly launcher: SubagentBatchLauncher,
|
||||
tasks: readonly QueuedSubagentTask<T>[],
|
||||
options: SubagentBatchOptions = {},
|
||||
private readonly launcher: AgentRunBatchLauncher,
|
||||
tasks: readonly QueuedAgentRunTask<T>[],
|
||||
options: AgentRunBatchOptions = {},
|
||||
) {
|
||||
this.maxConcurrency = options.maxConcurrency;
|
||||
this.states = tasks.map((task, index) => ({
|
||||
|
|
@ -184,9 +184,9 @@ export class SubagentBatch<T> {
|
|||
};
|
||||
}
|
||||
|
||||
run(): Promise<Array<SubagentResult<T>>> {
|
||||
run(): Promise<Array<AgentRunResult<T>>> {
|
||||
if (this.started) {
|
||||
throw new Error('SubagentBatch.run() can only be called once.');
|
||||
throw new Error('AgentRunBatch.run() can only be called once.');
|
||||
}
|
||||
this.started = true;
|
||||
|
||||
|
|
@ -307,7 +307,7 @@ export class SubagentBatch<T> {
|
|||
|
||||
private async runAttempt(attempt: ActiveAttempt<T>): Promise<AttemptOutcome<T>> {
|
||||
const task = attempt.state.task;
|
||||
const runOptions: RunSubagentOptions = {
|
||||
const runOptions: AgentRunAttemptOptions = {
|
||||
parentToolCallId: task.parentToolCallId,
|
||||
parentToolCallUuid: task.parentToolCallUuid,
|
||||
prompt: task.prompt,
|
||||
|
|
@ -321,7 +321,7 @@ export class SubagentBatch<T> {
|
|||
suppressRateLimitFailureEvent: true,
|
||||
};
|
||||
|
||||
let handle: SubagentHandle;
|
||||
let handle: AgentRunAttemptHandle;
|
||||
try {
|
||||
attempt.controller.signal.throwIfAborted();
|
||||
if (attempt.state.retryAgentId !== undefined) {
|
||||
|
|
@ -329,7 +329,7 @@ export class SubagentBatch<T> {
|
|||
} else if (task.kind === 'resume') {
|
||||
handle = await this.launcher.resume(task.resumeAgentId, runOptions);
|
||||
} else {
|
||||
const spawnOptions: SpawnSubagentOptions = {
|
||||
const spawnOptions: AgentSpawnAttemptOptions = {
|
||||
profileName: task.profileName,
|
||||
swarmItem: task.swarmItem,
|
||||
...runOptions,
|
||||
|
|
@ -363,7 +363,7 @@ export class SubagentBatch<T> {
|
|||
}
|
||||
}
|
||||
|
||||
private failedAttemptOutcome(attempt: ActiveAttempt<T>, error: unknown): SubagentResult<T> {
|
||||
private failedAttemptOutcome(attempt: ActiveAttempt<T>, error: unknown): AgentRunResult<T> {
|
||||
const status =
|
||||
attempt.controller.signal.aborted && isUserCancellation(attempt.controller.signal.reason)
|
||||
? 'aborted'
|
||||
|
|
@ -588,7 +588,7 @@ export class SubagentBatch<T> {
|
|||
);
|
||||
}
|
||||
|
||||
private finish(results: Array<SubagentResult<T>>): void {
|
||||
private finish(results: Array<AgentRunResult<T>>): void {
|
||||
if (this.finished) return;
|
||||
this.finished = true;
|
||||
this.cleanup();
|
||||
|
|
@ -622,7 +622,7 @@ export class SubagentBatch<T> {
|
|||
this.rateLimitLaunchTimer = undefined;
|
||||
}
|
||||
|
||||
private linkAttemptSignals(attempt: ActiveAttempt<T>, task: QueuedSubagentTask<T>): () => void {
|
||||
private linkAttemptSignals(attempt: ActiveAttempt<T>, task: QueuedAgentRunTask<T>): () => void {
|
||||
const abortFromBatch = () => {
|
||||
attempt.controller.abort(this.controller.signal.reason);
|
||||
};
|
||||
|
|
@ -656,7 +656,7 @@ export class SubagentBatch<T> {
|
|||
private attemptErrorMessage(
|
||||
attempt: ActiveAttempt<T>,
|
||||
error: unknown,
|
||||
status: SubagentResult<T>['status'],
|
||||
status: AgentRunResult<T>['status'],
|
||||
): string {
|
||||
if (attempt.timedOut && attempt.state.task.timeout !== undefined) {
|
||||
return 'Subagent timed out.';
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
* `sessionSwarm` domain barrel — re-exports the sessionSwarm contract
|
||||
* (`sessionSwarm`) and its scoped service (`sessionSwarmService`). Importing
|
||||
* this barrel registers the `ISessionSwarmService` binding into the scope
|
||||
* registry. The internal `subagentBatch` scheduler is not exported.
|
||||
* registry. The internal `agentRunBatch` scheduler is not exported.
|
||||
*/
|
||||
|
||||
export * from './sessionSwarm';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
/**
|
||||
* `sessionSwarm` domain (L4) — batch scheduler for swarm subagent runs.
|
||||
* `sessionSwarm` domain (L4) — batch scheduler for swarm agent runs.
|
||||
*
|
||||
* Defines `ISessionSwarmService`, the Session-scoped service that runs a batch
|
||||
* of subagents on behalf of a caller agent. Owns the in-flight batch state so
|
||||
* of agents on behalf of a caller agent. Owns the in-flight batch state so
|
||||
* cancellation can reach every run; the actual concurrency / rate-limit logic
|
||||
* lives in the internal `subagentBatch` module. Bound at Session scope.
|
||||
* lives in the internal `agentRunBatch` module. Bound at Session scope.
|
||||
*/
|
||||
|
||||
import type { TokenUsage } from '#/app/llmProtocol';
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
/**
|
||||
* `sessionSwarm` domain (L4) — `ISessionSwarmService` implementation.
|
||||
*
|
||||
* Runs a batch of subagents on behalf of a caller agent: builds a
|
||||
* `SubagentBatchLauncher` on top of the `agentLifecycle` primitives
|
||||
* (`spawn({ profile })`, `observeChildAgentTurn`), drives the
|
||||
* internal `SubagentBatch` scheduler, and tracks one `AbortController` per
|
||||
* caller so `cancel` can abort every in-flight run. `subagent.spawned` facts
|
||||
* carrying the swarm's tool-call context, and `subagent.suspended` facts
|
||||
* emitted when a task is requeued after a provider rate limit, are recorded
|
||||
* on the caller agent's event sink; the child's own turn lifecycle
|
||||
* (`subagent.started/completed/failed`) is mirrored inside
|
||||
* `observeChildAgentTurn`. Bound at Session scope.
|
||||
* Runs a batch of agents on behalf of a caller agent: builds an
|
||||
* `AgentRunBatchLauncher` on top of the `agentLifecycle` primitives
|
||||
* (`create({ binding })`, `run`), drives the internal `AgentRunBatch`
|
||||
* scheduler, and tracks one `AbortController` per caller so `cancel` can abort
|
||||
* every in-flight run. The caller ↔ child association is this domain's own
|
||||
* business data: requester-side display facts (`subagent.spawned` wire signals
|
||||
* carrying the swarm's tool-call context, `subagent.suspended` when a task is
|
||||
* requeued after a provider rate limit) are emitted here / via the
|
||||
* `agentLifecycle` wrapper helper `mirrorAgentRun`; the lifecycle registry
|
||||
* itself stays flat. Bound at Session scope.
|
||||
*/
|
||||
|
||||
import type { TokenUsage } from '#/app/llmProtocol';
|
||||
|
|
@ -21,11 +21,14 @@ import { linkAbortSignal } from '#/_base/utils/abort';
|
|||
import type { IAgentScopeHandle } from '#/_base/di/scope';
|
||||
import { IAgentProfileService } from '#/agent/profile';
|
||||
import { IAgentRecordService } from '#/agent/record';
|
||||
import { ITelemetryService } from '#/app/telemetry';
|
||||
import { IAgentProfileCatalogService } from '#/app/agentProfileCatalog';
|
||||
import {
|
||||
applyProfilePromptPrefix,
|
||||
IAgentProfileCatalogService,
|
||||
} from '#/app/agentProfileCatalog';
|
||||
import {
|
||||
emitAgentRunSpawned,
|
||||
IAgentLifecycleService,
|
||||
observeChildAgentTurn,
|
||||
mirrorAgentRun,
|
||||
} from '#/session/agentLifecycle';
|
||||
import { IExecContext } from '#/session/execContext';
|
||||
import { ISessionProcessRunner } from '#/session/process';
|
||||
|
|
@ -39,12 +42,18 @@ import {
|
|||
} from './sessionSwarm';
|
||||
import {
|
||||
resolveSwarmMaxConcurrency,
|
||||
SubagentBatch,
|
||||
type RunSubagentOptions,
|
||||
type SpawnSubagentOptions,
|
||||
type SubagentBatchLauncher,
|
||||
type SubagentHandle,
|
||||
} from './subagentBatch';
|
||||
AgentRunBatch,
|
||||
type AgentRunAttemptOptions,
|
||||
type AgentSpawnAttemptOptions,
|
||||
type AgentRunBatchLauncher,
|
||||
type AgentRunAttemptHandle,
|
||||
} from './agentRunBatch';
|
||||
|
||||
/**
|
||||
* Requester-facing label for a resumed agent whose profile binding is unknown.
|
||||
* Kept as the legacy wire display value.
|
||||
*/
|
||||
const RESUMED_PROFILE_FALLBACK = 'subagent';
|
||||
|
||||
export class SessionSwarmService implements ISessionSwarmService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
|
@ -68,7 +77,7 @@ export class SessionSwarmService implements ISessionSwarmService {
|
|||
if (task.signal !== undefined) unlinks.push(linkAbortSignal(task.signal, controller));
|
||||
return { ...task, signal: controller.signal };
|
||||
});
|
||||
const launcher: SubagentBatchLauncher = {
|
||||
const launcher: AgentRunBatchLauncher = {
|
||||
spawn: (options) => this.spawnAttempt(callerAgentId, options),
|
||||
resume: (agentId, options) => this.resumeAttempt(callerAgentId, agentId, options, false),
|
||||
retry: (agentId, options) => this.resumeAttempt(callerAgentId, agentId, options, true),
|
||||
|
|
@ -82,7 +91,7 @@ export class SessionSwarmService implements ISessionSwarmService {
|
|||
},
|
||||
};
|
||||
const maxConcurrency = resolveSwarmMaxConcurrency();
|
||||
const promise = new SubagentBatch(launcher, linkedTasks, { maxConcurrency }).run();
|
||||
const promise = new AgentRunBatch(launcher, linkedTasks, { maxConcurrency }).run();
|
||||
void promise.finally(() => {
|
||||
for (const unlink of unlinks) unlink();
|
||||
if (this.inFlight.get(callerAgentId) === controller) this.inFlight.delete(callerAgentId);
|
||||
|
|
@ -96,114 +105,95 @@ export class SessionSwarmService implements ISessionSwarmService {
|
|||
|
||||
private async spawnAttempt(
|
||||
callerAgentId: string,
|
||||
options: SpawnSubagentOptions,
|
||||
): Promise<SubagentHandle> {
|
||||
options: AgentSpawnAttemptOptions,
|
||||
): Promise<AgentRunAttemptHandle> {
|
||||
options.signal.throwIfAborted();
|
||||
const caller = this.requireHandle(callerAgentId, 'Caller agent');
|
||||
const profile = this.catalog.get(options.profileName);
|
||||
if (profile === undefined) {
|
||||
throw new Error(`Unknown agent type: "${options.profileName}"`);
|
||||
}
|
||||
const child = await this.lifecycle.spawn(callerAgentId, {
|
||||
swarmItem: options.swarmItem,
|
||||
profile: profile.name,
|
||||
});
|
||||
this.emitSpawned(caller, child.id, options.profileName, options);
|
||||
const promptText = profile.promptPrefix !== undefined
|
||||
? await this.withProfilePrefix(profile.promptPrefix, options.prompt)
|
||||
: options.prompt;
|
||||
const observed = observeChildAgentTurn(
|
||||
caller,
|
||||
child,
|
||||
{ kind: 'prompt', prompt: promptText },
|
||||
{
|
||||
profileName: options.profileName,
|
||||
summaryPolicy: profile.summaryPolicy,
|
||||
suppressRateLimitFailureEvent: options.suppressRateLimitFailureEvent,
|
||||
signal: options.signal,
|
||||
onReady: options.onReady,
|
||||
const callerData = caller.accessor.get(IAgentProfileService).data();
|
||||
if (callerData.modelAlias === undefined) {
|
||||
throw new Error('Caller agent has no model bound');
|
||||
}
|
||||
// Explicit inheritance: the child runs the requested profile on the
|
||||
// caller's own model / thinking level / cwd (Profile + Model ⇒ Agent).
|
||||
const child = await this.lifecycle.create({
|
||||
binding: {
|
||||
profile: profile.name,
|
||||
model: callerData.modelAlias,
|
||||
thinking: callerData.thinkingLevel,
|
||||
cwd: callerData.cwd,
|
||||
},
|
||||
);
|
||||
if (observed === undefined) throw new Error('Subagent turn could not be started');
|
||||
return {
|
||||
agentId: child.id,
|
||||
labels: options.swarmItem === undefined ? undefined : { swarmItem: options.swarmItem },
|
||||
});
|
||||
emitAgentRunSpawned(caller, child.id, {
|
||||
profileName: options.profileName,
|
||||
completion: observed.completion.then((r) => ({ result: r.summary, usage: r.usage })),
|
||||
};
|
||||
parentToolCallId: options.parentToolCallId,
|
||||
parentToolCallUuid: options.parentToolCallUuid,
|
||||
description: options.description,
|
||||
swarmIndex: options.swarmIndex,
|
||||
runInBackground: options.runInBackground,
|
||||
});
|
||||
const promptText = await applyProfilePromptPrefix(profile, options.prompt, {
|
||||
cwd: this.execContext.cwd,
|
||||
runner: this.processRunner,
|
||||
log: this.log,
|
||||
});
|
||||
return this.observe(caller, child.id, options.profileName, {
|
||||
kind: 'prompt',
|
||||
prompt: promptText,
|
||||
}, options);
|
||||
}
|
||||
|
||||
private async resumeAttempt(
|
||||
callerAgentId: string,
|
||||
agentId: string,
|
||||
options: RunSubagentOptions,
|
||||
options: AgentRunAttemptOptions,
|
||||
retryTurn: boolean,
|
||||
): Promise<SubagentHandle> {
|
||||
): Promise<AgentRunAttemptHandle> {
|
||||
options.signal.throwIfAborted();
|
||||
const caller = this.requireHandle(callerAgentId, 'Caller agent');
|
||||
const child = this.requireHandle(agentId, 'Agent instance');
|
||||
const profileName =
|
||||
child.accessor.get(IAgentProfileService).data().profileName ?? 'subagent';
|
||||
const profile = this.catalog.get(profileName);
|
||||
this.emitSpawned(caller, agentId, profileName, options);
|
||||
const request = retryTurn
|
||||
? ({ kind: 'retry' } as const)
|
||||
: ({ kind: 'prompt', prompt: options.prompt } as const);
|
||||
const observed = observeChildAgentTurn(caller, child, request, {
|
||||
child.accessor.get(IAgentProfileService).data().profileName ?? RESUMED_PROFILE_FALLBACK;
|
||||
emitAgentRunSpawned(caller, agentId, {
|
||||
profileName,
|
||||
summaryPolicy: profile?.summaryPolicy,
|
||||
suppressRateLimitFailureEvent: options.suppressRateLimitFailureEvent,
|
||||
signal: options.signal,
|
||||
onReady: options.onReady,
|
||||
});
|
||||
if (observed === undefined) throw new Error('Subagent turn could not be started');
|
||||
return {
|
||||
agentId,
|
||||
profileName,
|
||||
completion: observed.completion.then((r) => ({ result: r.summary, usage: r.usage })),
|
||||
};
|
||||
}
|
||||
|
||||
private emitSpawned(
|
||||
caller: IAgentScopeHandle,
|
||||
subagentId: string,
|
||||
profileName: string,
|
||||
options: RunSubagentOptions,
|
||||
): void {
|
||||
caller.accessor.get(IAgentRecordService)?.signal({
|
||||
type: 'subagent.spawned',
|
||||
subagentId,
|
||||
subagentName: profileName,
|
||||
parentToolCallId: options.parentToolCallId,
|
||||
parentToolCallUuid: options.parentToolCallUuid,
|
||||
callerAgentId: caller.id,
|
||||
description: options.description,
|
||||
swarmIndex: options.swarmIndex,
|
||||
runInBackground: options.runInBackground,
|
||||
});
|
||||
caller.accessor.get(ITelemetryService)?.track('subagent_created', {
|
||||
subagent_name: profileName,
|
||||
run_in_background: options.runInBackground,
|
||||
});
|
||||
const request = retryTurn
|
||||
? ({ kind: 'retry' } as const)
|
||||
: ({ kind: 'prompt', prompt: options.prompt } as const);
|
||||
return this.observe(caller, child.id, profileName, request, options);
|
||||
}
|
||||
|
||||
private async withProfilePrefix(
|
||||
promptPrefix: (ctx: {
|
||||
cwd: string;
|
||||
runner: ISessionProcessRunner;
|
||||
log?: ILogService;
|
||||
}) => Promise<string>,
|
||||
prompt: string,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const prefix = await promptPrefix({
|
||||
cwd: this.execContext.cwd,
|
||||
runner: this.processRunner,
|
||||
log: this.log,
|
||||
});
|
||||
return prefix.length > 0 ? `${prefix}\n\n${prompt}` : prompt;
|
||||
} catch {
|
||||
return prompt;
|
||||
}
|
||||
private observe(
|
||||
caller: IAgentScopeHandle,
|
||||
agentId: string,
|
||||
profileName: string,
|
||||
request: { kind: 'prompt'; prompt: string } | { kind: 'retry' },
|
||||
options: AgentRunAttemptOptions,
|
||||
): AgentRunAttemptHandle {
|
||||
const run = this.lifecycle.run(agentId, request, {
|
||||
signal: options.signal,
|
||||
onReady: options.onReady,
|
||||
});
|
||||
const mirrored = mirrorAgentRun(caller, run, {
|
||||
profileName,
|
||||
prompt: request.kind === 'prompt' ? request.prompt : undefined,
|
||||
suppressRateLimitFailureEvent: options.suppressRateLimitFailureEvent,
|
||||
signal: options.signal,
|
||||
});
|
||||
return {
|
||||
agentId,
|
||||
profileName,
|
||||
completion: mirrored.then((r) => ({ result: r.summary, usage: r.usage })),
|
||||
};
|
||||
}
|
||||
|
||||
private requireHandle(agentId: string, label: string): IAgentScopeHandle {
|
||||
|
|
@ -214,7 +204,7 @@ export class SessionSwarmService implements ISessionSwarmService {
|
|||
}
|
||||
|
||||
// Kept as a type-anchor so future maintenance imports the usage shape from here.
|
||||
export type _SubagentUsage = TokenUsage;
|
||||
export type _AgentRunUsage = TokenUsage;
|
||||
|
||||
registerScopedService(
|
||||
LifecycleScope.Session,
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ import { TestInstantiationService } from '#/_base/di/test';
|
|||
import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle';
|
||||
import { AgentLifecycleService } from '#/session/agentLifecycle/agentLifecycleService';
|
||||
import { IBootstrapService } from '#/app/bootstrap';
|
||||
import { IConfigService } from '#/app/config';
|
||||
import { IPluginSessionStartInjectorService } from '#/agent/contextInjector';
|
||||
import { IAgentEventSinkService } from '#/agent/eventSink';
|
||||
import { ILogService } from '#/app/log';
|
||||
import { IPluginService } from '#/app/plugin';
|
||||
import { ISessionContext } from '#/session/sessionContext/sessionContext';
|
||||
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
|
||||
import { IAgentToolExecutorService } from '#/agent/toolExecutor';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
import { IAgentToolRegistryService, _clearToolContributionsForTests } from '#/agent/toolRegistry';
|
||||
import { ISessionWorkspaceContext } from '#/session/workspaceContext';
|
||||
|
||||
const noopLog = {
|
||||
|
|
@ -52,6 +52,10 @@ describe('AgentLifecycleService', () => {
|
|||
let registerAgent: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
// The unit under test force-instantiates the builtin-tools registrar per
|
||||
// created agent; clear module-level tool contributions so no real tool
|
||||
// (with its own service dependencies) is constructed in this unit test.
|
||||
_clearToolContributionsForTests();
|
||||
disposables = new DisposableStore();
|
||||
ix = disposables.add(new TestInstantiationService());
|
||||
registerAgent = vi.fn(() => Promise.resolve());
|
||||
|
|
@ -76,6 +80,10 @@ describe('AgentLifecycleService', () => {
|
|||
_serviceBrand: undefined,
|
||||
homeDir: '/tmp/kimi-agentLifecycle-home',
|
||||
cwd: '/tmp/kimi-agentLifecycle-home',
|
||||
agentHomedir: (_ws: string, _session: string, agentId: string) =>
|
||||
`/tmp/kimi-agentLifecycle-test/agents/${agentId}`,
|
||||
agentScope: (_ws: string, _session: string, agentId: string) =>
|
||||
`test/agents/${agentId}`,
|
||||
} as unknown as IBootstrapService);
|
||||
ix.stub(ISessionWorkspaceContext, {
|
||||
_serviceBrand: undefined,
|
||||
|
|
@ -83,6 +91,10 @@ describe('AgentLifecycleService', () => {
|
|||
additionalDirs: [],
|
||||
} as unknown as ISessionWorkspaceContext);
|
||||
ix.stub(IPluginService, pluginServiceStub);
|
||||
ix.stub(IConfigService, {
|
||||
ready: Promise.resolve(),
|
||||
get: (() => undefined) as IConfigService['get'],
|
||||
} as unknown as IConfigService);
|
||||
ix.stub(ILogService, noopLog);
|
||||
ix.stub(IPluginSessionStartInjectorService, {
|
||||
_serviceBrand: undefined,
|
||||
|
|
@ -93,11 +105,6 @@ describe('AgentLifecycleService', () => {
|
|||
resolve: () => undefined,
|
||||
list: () => [],
|
||||
} as unknown as IAgentToolRegistryService);
|
||||
ix.stub(IAgentEventSinkService, {
|
||||
_serviceBrand: undefined,
|
||||
emit: () => {},
|
||||
on: () => ({ dispose: () => {} }),
|
||||
} as unknown as IAgentEventSinkService);
|
||||
ix.stub(IAgentToolExecutorService, {
|
||||
_serviceBrand: undefined,
|
||||
hooks: {
|
||||
|
|
@ -111,7 +118,7 @@ describe('AgentLifecycleService', () => {
|
|||
|
||||
it('create / getHandle / list / remove', async () => {
|
||||
const svc = ix.get(IAgentLifecycleService);
|
||||
const main = await svc.createMain();
|
||||
const main = await svc.create({ agentId: 'main' });
|
||||
expect(main.id).toBe('main');
|
||||
expect(svc.getHandle('main')).toBe(main);
|
||||
expect(svc.list()).toEqual([main]);
|
||||
|
|
@ -126,23 +133,35 @@ describe('AgentLifecycleService', () => {
|
|||
expect(a.id).not.toBe(b.id);
|
||||
});
|
||||
|
||||
it('persists subagent metadata when creating a child agent', async () => {
|
||||
it('persists provenance and labels when creating an agent', async () => {
|
||||
const svc = ix.get(IAgentLifecycleService);
|
||||
|
||||
const child = await svc.create({
|
||||
agentId: 'child',
|
||||
forkedFrom: 'main',
|
||||
swarmItem: 'swarm-item-1',
|
||||
labels: { swarmItem: 'swarm-item-1' },
|
||||
});
|
||||
|
||||
expect(child.id).toBe('child');
|
||||
expect(registerAgent).toHaveBeenCalledWith('child', {
|
||||
homedir: '/tmp/kimi-agentLifecycle-test/agents/child',
|
||||
forkedFrom: 'main',
|
||||
swarmItem: 'swarm-item-1',
|
||||
labels: { swarmItem: 'swarm-item-1' },
|
||||
});
|
||||
});
|
||||
|
||||
it('fork throws when the source agent does not exist', async () => {
|
||||
const svc = ix.get(IAgentLifecycleService);
|
||||
await expect(svc.fork('missing')).rejects.toThrow('Source agent "missing" does not exist');
|
||||
});
|
||||
|
||||
it('run throws when the agent does not exist', () => {
|
||||
const svc = ix.get(IAgentLifecycleService);
|
||||
expect(() =>
|
||||
svc.run('missing', { kind: 'prompt', prompt: 'hi' }, { signal: new AbortController().signal }),
|
||||
).toThrow('Agent "missing" does not exist');
|
||||
});
|
||||
|
||||
it('fires onDidCreate on create and onDidDispose on remove', async () => {
|
||||
const svc = ix.get(IAgentLifecycleService);
|
||||
const created: string[] = [];
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|||
import {
|
||||
CronCreateTool,
|
||||
type CronCreateInput,
|
||||
} from '#/agent/cron/tools/cron-create';
|
||||
} from '#/session/cron/tools/cron-create';
|
||||
import { ISessionCronService } from '#/session/cron';
|
||||
import { IAgentProfileService } from '#/agent/profile';
|
||||
import { IAgentToolRegistryService } from '#/agent/toolRegistry';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { join } from 'node:path';
|
|||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveClockSources, SYSTEM_CLOCKS } from '#/agent/cron/clock';
|
||||
import { resolveClockSources, SYSTEM_CLOCKS } from '#/app/cron/clock';
|
||||
|
||||
describe('cron clock sources', () => {
|
||||
describe('SYSTEM_CLOCKS', () => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
cronToHuman,
|
||||
hasFireWithinYears,
|
||||
parseCronExpression,
|
||||
} from '#/agent/cron/cron-expr';
|
||||
} from '#/app/cron/cron-expr';
|
||||
|
||||
function localDate(
|
||||
year: number,
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { CronCreateTool } from '#/agent/cron/tools/cron-create';
|
||||
import { CronDeleteTool } from '#/agent/cron/tools/cron-delete';
|
||||
import { CronListTool } from '#/agent/cron/tools/cron-list';
|
||||
import { CronCreateTool } from '#/session/cron/tools/cron-create';
|
||||
import { CronDeleteTool } from '#/session/cron/tools/cron-delete';
|
||||
import { CronListTool } from '#/session/cron/tools/cron-list';
|
||||
import type { ExecutableToolOutput } from '#/agent/tool';
|
||||
import type { ContextMessage } from '#/agent/contextMemory';
|
||||
import { ISessionCronService } from '#/session/cron';
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { parseCronExpression } from '#/agent/cron/cron-expr';
|
||||
import { parseCronExpression } from '#/app/cron/cron-expr';
|
||||
import {
|
||||
DEFAULT_CRON_JITTER_CONFIG,
|
||||
jitteredNextCronRunMs,
|
||||
oneShotJitteredNextCronRunMs,
|
||||
} from '#/agent/cron/jitter';
|
||||
} from '#/app/cron/jitter';
|
||||
|
||||
function localDate(
|
||||
year: number,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|||
|
||||
import type { ContentPart } from '#/app/llmProtocol/kosong';
|
||||
|
||||
import type { CronTask } from '#/app/cronPersistence';
|
||||
import type { CronTask } from '#/app/cron';
|
||||
import {
|
||||
CRON_FIRED,
|
||||
CRON_MISSED,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { CronTask } from '#/app/cronPersistence';
|
||||
import { CRON_ID_REGEX, isValidCronTask } from '#/app/cronPersistence';
|
||||
import type { CronTask } from '#/app/cron';
|
||||
import { CRON_ID_REGEX, isValidCronTask } from '#/app/cron';
|
||||
|
||||
const validTask: CronTask = {
|
||||
id: '0123abcd',
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|||
import type { ContentPart } from '#/app/llmProtocol/kosong';
|
||||
import type { ContextMessage, PromptOrigin } from '#/agent/contextMemory';
|
||||
import { IAgentPromptService } from '#/agent/prompt';
|
||||
import type { CronTask } from '#/app/cronPersistence';
|
||||
import type { CronTask } from '#/app/cron';
|
||||
import { ISessionCronService } from '#/session/cron';
|
||||
import { IBootstrapService } from '#/app/bootstrap';
|
||||
import { IAtomicDocumentStore } from '#/persistence/interface/atomicDocumentStore';
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { readFileSync } from 'node:fs';
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const GUARDED_FILES = [
|
||||
'cronService.ts',
|
||||
'jitter.ts',
|
||||
{ dir: 'session/cron', file: 'sessionCronServiceImpl.ts' },
|
||||
{ dir: 'app/cron', file: 'jitter.ts' },
|
||||
] as const;
|
||||
|
||||
describe('cron source clock guard', () => {
|
||||
it.each(GUARDED_FILES)('%s does not call Date.now()', (file) => {
|
||||
const source = readFileSync(new URL(`../../src/agent/cron/${file}`, import.meta.url), 'utf8');
|
||||
it.each(GUARDED_FILES)('$file does not call Date.now()', ({ dir, file }) => {
|
||||
const source = readFileSync(new URL(`../../src/${dir}/${file}`, import.meta.url), 'utf8');
|
||||
expect(stripComments(source)).not.toMatch(/\bDate\.now\s*\(/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,24 +8,24 @@ import type {
|
|||
RunnableToolExecution,
|
||||
ToolExecution,
|
||||
} from '#/agent/tool';
|
||||
import type { CronTask, CronTaskInit } from '#/app/cronPersistence';
|
||||
import type { CronTask, CronTaskInit } from '#/app/cron';
|
||||
import type { ISessionCronService } from '#/session/cron';
|
||||
import {
|
||||
computeNextCronRun,
|
||||
parseCronExpression,
|
||||
} from '#/agent/cron/cron-expr';
|
||||
import { renderCronFireXml } from '#/agent/cron/format';
|
||||
} from '#/app/cron/cron-expr';
|
||||
import { renderCronFireXml } from '#/app/cron/format';
|
||||
import {
|
||||
jitteredNextCronRunMs,
|
||||
oneShotJitteredNextCronRunMs,
|
||||
} from '#/agent/cron/jitter';
|
||||
} from '#/app/cron/jitter';
|
||||
import {
|
||||
CronCreateTool,
|
||||
MAX_CRON_JOBS_PER_SESSION,
|
||||
type CronCreateInput,
|
||||
} from '#/agent/cron/tools/cron-create';
|
||||
import { CronDeleteTool, type CronDeleteInput } from '#/agent/cron/tools/cron-delete';
|
||||
import { CronListTool, type CronListInput } from '#/agent/cron/tools/cron-list';
|
||||
} from '#/session/cron/tools/cron-create';
|
||||
import { CronDeleteTool, type CronDeleteInput } from '#/session/cron/tools/cron-delete';
|
||||
import { CronListTool, type CronListInput } from '#/session/cron/tools/cron-list';
|
||||
|
||||
const WALL_ANCHOR = 1_700_000_000_000;
|
||||
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
type TestInstantiationService,
|
||||
} from '#/_base/di/test';
|
||||
import { Event } from '#/_base/event';
|
||||
import { IAgentToolService } from '#/agent/agentTool';
|
||||
import { IAgentTaskService } from '#/agent/task';
|
||||
import {
|
||||
AgentExternalHooksService,
|
||||
|
|
@ -139,9 +138,6 @@ describe('HookEngine integration', () => {
|
|||
reg.definePartialInstance(IAgentTaskService, {
|
||||
hooks: createHooks(['onDidNotify']),
|
||||
});
|
||||
reg.definePartialInstance(IAgentToolService, {
|
||||
hooks: createHooks(['onWillRunSubagent', 'onDidRunSubagent']),
|
||||
});
|
||||
},
|
||||
});
|
||||
ix.set(
|
||||
|
|
|
|||
|
|
@ -72,15 +72,16 @@ describe('RestGateway', () => {
|
|||
_serviceBrand: undefined,
|
||||
onDidCreate: () => ({ dispose: () => {} }),
|
||||
onDidDispose: () => ({ dispose: () => {} }),
|
||||
onDidCreateMain: () => ({ dispose: () => {} }),
|
||||
notifyMainCreated: () => {},
|
||||
create: () => Promise.resolve(agentHandle),
|
||||
createMain: () => Promise.resolve(agentHandle),
|
||||
clone: () => Promise.resolve(agentHandle),
|
||||
spawn: () => Promise.resolve(agentHandle),
|
||||
fork: () => Promise.resolve(agentHandle),
|
||||
run: () => {
|
||||
throw new Error('not implemented in test');
|
||||
},
|
||||
getHandle: (id) => (id === 'main' ? agentHandle : undefined),
|
||||
list: () => [agentHandle],
|
||||
remove: () => Promise.resolve(),
|
||||
runSubagent: () => Promise.reject(new Error('not implemented in test')),
|
||||
resumeSubagent: () => Promise.reject(new Error('not implemented in test')),
|
||||
};
|
||||
const sessionHandle: ISessionScopeHandle = {
|
||||
id: 's1',
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import { IAgentContextInjectorService } from '#/agent/contextInjector';
|
|||
import type { ContextMessage } from '#/agent/contextMemory';
|
||||
import { ISessionCronService } from '#/session/cron/sessionCronService';
|
||||
import { SessionCronServiceImpl } from '#/session/cron/sessionCronServiceImpl';
|
||||
import { ICronTaskPersistence } from '#/app/cronPersistence/cronTaskPersistence';
|
||||
import { CronTaskPersistenceService } from '#/app/cronPersistence/cronTaskPersistenceService';
|
||||
import { ICronTaskPersistence } from '#/app/cron/cronTaskPersistence';
|
||||
import { CronTaskPersistenceService } from '#/app/cron/cronTaskPersistenceService';
|
||||
import type { HookEngine } from '#/agent/externalHooks/engine';
|
||||
import type { FullCompactionServiceOptions } from '#/agent/fullCompaction';
|
||||
import { AgentGoalService, IAgentGoalService, type GoalServiceOptions } from '#/agent/goal';
|
||||
|
|
|
|||
|
|
@ -56,15 +56,16 @@ function lifecycle(handles: readonly IAgentScopeHandle[]): IAgentLifecycleServic
|
|||
_serviceBrand: undefined,
|
||||
onDidCreate: () => ({ dispose: () => {} }),
|
||||
onDidDispose: () => ({ dispose: () => {} }),
|
||||
onDidCreateMain: () => ({ dispose: () => {} }),
|
||||
notifyMainCreated: () => {},
|
||||
create: () => Promise.resolve(handles[0]!),
|
||||
createMain: () => Promise.resolve(handles[0]!),
|
||||
clone: () => Promise.resolve(handles[0]!),
|
||||
spawn: () => Promise.resolve(handles[0]!),
|
||||
fork: () => Promise.resolve(handles[0]!),
|
||||
run: () => {
|
||||
throw new Error('not implemented in test');
|
||||
},
|
||||
getHandle: () => undefined,
|
||||
list: () => handles,
|
||||
remove: () => Promise.resolve(),
|
||||
runSubagent: () => Promise.reject(new Error('not implemented in test')),
|
||||
resumeSubagent: () => Promise.reject(new Error('not implemented in test')),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,12 +31,12 @@ import {
|
|||
type RegisterAgentTaskOptions,
|
||||
} from '#/agent/task';
|
||||
import type { AgentTaskSettlement } from '#/agent/task/types';
|
||||
import { ProcessTask } from '#/agent/shellTools/tools/process-task';
|
||||
import { ProcessTask } from '#/os/backends/node-local/tools/process-task';
|
||||
import type { IHostEnvironment } from '#/os/interface/hostEnvironment';
|
||||
import type { IAgentProfileService } from '#/agent/profile';
|
||||
import { createExecContext, type IExecContext } from '#/session/execContext';
|
||||
import type { IProcess, ISessionProcessRunner } from '#/session/process';
|
||||
import { type BashInput, BashInputSchema, BashTool } from '#/agent/shellTools/tools/bash';
|
||||
import { type BashInput, BashInputSchema, BashTool } from '#/os/backends/node-local/tools/bash';
|
||||
import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '#/agent/tool';
|
||||
import { createHooks } from '#/hooks';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { ToolResultBuilder } from '#/agent/shellTools/tools/result-builder';
|
||||
import { ToolResultBuilder } from '#/agent/tool/result-builder';
|
||||
|
||||
describe('ToolResultBuilder', () => {
|
||||
it('returns concatenated output and a confirmation message under the limit', () => {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import type { IProcess } from '#/session/process';
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { IAgentTaskService } from '#/agent/task';
|
||||
import { ProcessTask } from '#/agent/shellTools/tools/process-task';
|
||||
import { ProcessTask } from '#/os/backends/node-local/tools/process-task';
|
||||
import {
|
||||
taskServices,
|
||||
createTestAgent,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
SubagentTask,
|
||||
type SubagentHandle,
|
||||
} from '#/session/agentLifecycle/tools/subagent-task';
|
||||
import { ProcessTask } from '#/agent/shellTools/tools/process-task';
|
||||
import { ProcessTask } from '#/os/backends/node-local/tools/process-task';
|
||||
import { createTestAgent, type TestAgentContext } from '../harness';
|
||||
import { createAgentTaskPersistence } from './stubs';
|
||||
|
||||
|
|
@ -31,7 +31,6 @@ function agentTask(
|
|||
const handle: SubagentHandle = {
|
||||
agentId: 'agent-child',
|
||||
profileName: 'coder',
|
||||
resumed: false,
|
||||
completion,
|
||||
};
|
||||
return new SubagentTask(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
SubagentTask,
|
||||
type SubagentHandle,
|
||||
} from '#/session/agentLifecycle/tools/subagent-task';
|
||||
import { ProcessTask } from '#/agent/shellTools/tools/process-task';
|
||||
import { ProcessTask } from '#/os/backends/node-local/tools/process-task';
|
||||
import { isUserCancellation, userCancellationReason } from '#/_base/utils/abort';
|
||||
import {
|
||||
configServices,
|
||||
|
|
@ -88,7 +88,6 @@ function agentTask(
|
|||
const handle: SubagentHandle = {
|
||||
agentId: options.agentId ?? 'agent-child',
|
||||
profileName: options.subagentType ?? 'coder',
|
||||
resumed: false,
|
||||
completion,
|
||||
};
|
||||
const task = new SubagentTask(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { join } from 'pathe';
|
|||
import type { IProcess } from '#/session/process';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { IAgentTaskService } from '#/agent/task';
|
||||
import { ProcessTask } from '#/agent/shellTools/tools/process-task';
|
||||
import { ProcessTask } from '#/os/backends/node-local/tools/process-task';
|
||||
import { createAgentTaskPersistence, type TaskServiceTestManager } from './stubs';
|
||||
import { taskServices, createTestAgent, homeDirServices, type TestAgentContext } from '../harness';
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
SubagentTask,
|
||||
type SubagentHandle,
|
||||
} from '#/session/agentLifecycle/tools/subagent-task';
|
||||
import { ProcessTask } from '#/agent/shellTools/tools/process-task';
|
||||
import { ProcessTask } from '#/os/backends/node-local/tools/process-task';
|
||||
import { IAgentContextMemoryService } from '#/agent/contextMemory';
|
||||
import { IAgentEventSinkService } from '#/agent/eventSink';
|
||||
import type { HookEngine } from '#/agent/externalHooks/engine';
|
||||
|
|
@ -92,7 +92,6 @@ function agentTask(
|
|||
const handle: SubagentHandle = {
|
||||
agentId: options.agentId ?? 'agent-child',
|
||||
profileName: options.subagentType ?? 'coder',
|
||||
resumed: false,
|
||||
completion,
|
||||
};
|
||||
const task = new SubagentTask(
|
||||
|
|
|
|||
|
|
@ -5,23 +5,16 @@
|
|||
* `main` materializes it here. Both the `/api/v1` routes and the `/api/v2`
|
||||
* dispatcher resolve the main agent through {@link ensureMainAgent} so a
|
||||
* missing main agent is created instead of reported as `agent.not_found`.
|
||||
*
|
||||
* The main agent is created unbound (no Profile / Model). It becomes runnable
|
||||
* when the edge binds a Model — via the `profile:setModel` action, a legacy
|
||||
* prompt's `body.model` override, or a resumed wire log — at which point the
|
||||
* default profile is applied automatically. There is intentionally no default
|
||||
* model baked in here: a runnable agent only exists once a model is chosen.
|
||||
*
|
||||
* Both symbols are re-exports of the core `agentLifecycle` domain's bootstrap
|
||||
* helper, so main-agent bootstrap business (plugin session-start injection)
|
||||
* lives in exactly one place.
|
||||
*/
|
||||
|
||||
import {
|
||||
IAgentLifecycleService,
|
||||
type IAgentScopeHandle,
|
||||
type ISessionScopeHandle,
|
||||
} from '@moonshot-ai/agent-core-v2';
|
||||
|
||||
export const MAIN_AGENT_ID = 'main';
|
||||
|
||||
/**
|
||||
* Return the session's main agent, creating it on demand when it does not
|
||||
* exist yet.
|
||||
*/
|
||||
export async function ensureMainAgent(session: ISessionScopeHandle): Promise<IAgentScopeHandle> {
|
||||
const agents = session.accessor.get(IAgentLifecycleService);
|
||||
const existing = agents.getHandle(MAIN_AGENT_ID);
|
||||
if (existing !== undefined) return existing;
|
||||
return agents.createMain();
|
||||
}
|
||||
export { ensureMainAgent, MAIN_AGENT_ID } from '@moonshot-ai/agent-core-v2';
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ describe('server-v2 /api/v1/sessions/{sid}/messages', () => {
|
|||
if (session === undefined) throw new Error(`session ${sessionId} not found`);
|
||||
let agent = session.accessor.get(IAgentLifecycleService).getHandle('main');
|
||||
if (agent === undefined) {
|
||||
agent = await session.accessor.get(IAgentLifecycleService).createMain();
|
||||
agent = await session.accessor.get(IAgentLifecycleService).create({ agentId: 'main' });
|
||||
}
|
||||
if (messages.length > 0) {
|
||||
agent.accessor.get(IAgentContextMemoryService).splice(0, 0, messages);
|
||||
|
|
@ -283,7 +283,7 @@ describe('server-v2 /api/v1/sessions/{sid}/messages', () => {
|
|||
const id = await createSession();
|
||||
const session = server!.core.accessor.get(ISessionLifecycleService).get(id);
|
||||
if (session === undefined) throw new Error(`session ${id} not found`);
|
||||
const agent = await session.accessor.get(IAgentLifecycleService).createMain();
|
||||
const agent = await session.accessor.get(IAgentLifecycleService).create({ agentId: 'main' });
|
||||
const ctx = agent.accessor.get(IAgentContextMemoryService);
|
||||
// Three messages, then a compaction that folds the prefix into a summary.
|
||||
ctx.splice(0, 0, [
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ describe('server-v2 /api/v1 prompts', () => {
|
|||
async function createMainAgent(sessionId: string): Promise<void> {
|
||||
const session = server!.core.accessor.get(ISessionLifecycleService).get(sessionId);
|
||||
if (session === undefined) throw new Error(`session ${sessionId} not found`);
|
||||
await session.accessor.get(IAgentLifecycleService).createMain();
|
||||
await session.accessor.get(IAgentLifecycleService).create({ agentId: 'main' });
|
||||
}
|
||||
|
||||
it('submits a prompt and lists it as active', async () => {
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ describe('server-v2 /api/v2 RPC', () => {
|
|||
async function createMainAgent(sessionId: string): Promise<void> {
|
||||
const session = server!.core.accessor.get(ISessionLifecycleService).get(sessionId);
|
||||
if (session === undefined) throw new Error(`session ${sessionId} not found`);
|
||||
await session.accessor.get(IAgentLifecycleService).createMain();
|
||||
await session.accessor.get(IAgentLifecycleService).create({ agentId: 'main' });
|
||||
}
|
||||
|
||||
// --- Core scope -----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ describe('server-v2 /api/v1 skills', () => {
|
|||
const session = server!.core.accessor.get(ISessionLifecycleService).get(sessionId);
|
||||
if (session === undefined) throw new Error(`session ${sessionId} not found`);
|
||||
const agents = session.accessor.get(IAgentLifecycleService);
|
||||
if (agents.getHandle('main') === undefined) await agents.createMain();
|
||||
if (agents.getHandle('main') === undefined) await agents.create({ agentId: 'main' });
|
||||
}
|
||||
|
||||
describe('GET /api/v1/sessions/{sid}/skills', () => {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ describe('server-v2 GET /api/v1/sessions/:id/snapshot', () => {
|
|||
async function ensureMainAgent(sessionId: string): Promise<void> {
|
||||
const session = server!.core.accessor.get(ISessionLifecycleService).get(sessionId);
|
||||
const agents = session!.accessor.get(IAgentLifecycleService);
|
||||
if (agents.getHandle('main') === undefined) await agents.createMain();
|
||||
if (agents.getHandle('main') === undefined) await agents.create({ agentId: 'main' });
|
||||
}
|
||||
|
||||
function emit(sessionId: string, event: AgentEvent): void {
|
||||
|
|
@ -172,7 +172,7 @@ describe('server-v2 GET /api/v1/sessions/:id/snapshot', () => {
|
|||
// snapshot projects message timestamps from the normalized numeric base.
|
||||
const resumed = await server!.core.accessor.get(ISessionLifecycleService).resume(sid);
|
||||
if (resumed === undefined) throw new Error(`session ${sid} failed to resume`);
|
||||
const main = await resumed.accessor.get(IAgentLifecycleService).createMain();
|
||||
const main = await resumed.accessor.get(IAgentLifecycleService).create({ agentId: 'main' });
|
||||
main.accessor.get(IAgentContextMemoryService).splice(0, 0, [
|
||||
{ role: 'user', content: [{ type: 'text', text: 'hello' }], toolCalls: [] },
|
||||
{ role: 'assistant', content: [{ type: 'text', text: 'hi' }], toolCalls: [] },
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ describe('server-v2 /api/v1/sessions/{sid}/tasks', () => {
|
|||
if (session === undefined) throw new Error(`session ${sessionId} not found`);
|
||||
const agent =
|
||||
session.accessor.get(IAgentLifecycleService).getHandle('main') ??
|
||||
(await session.accessor.get(IAgentLifecycleService).createMain());
|
||||
(await session.accessor.get(IAgentLifecycleService).create({ agentId: 'main' }));
|
||||
return agent.accessor.get(IAgentTaskService);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ describe('server-v2 /api/v1 tools + mcp', () => {
|
|||
const session = server!.core.accessor.get(ISessionLifecycleService).get(sessionId);
|
||||
if (session === undefined) throw new Error(`session ${sessionId} not found`);
|
||||
let agent = session.accessor.get(IAgentLifecycleService).getHandle('main');
|
||||
agent ??= await session.accessor.get(IAgentLifecycleService).createMain();
|
||||
agent ??= await session.accessor.get(IAgentLifecycleService).create({ agentId: 'main' });
|
||||
return agent;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ describe('server-v2 /api/v1/ws resync', () => {
|
|||
expect(session).toBeDefined();
|
||||
const agents = session!.accessor.get(IAgentLifecycleService);
|
||||
if (agents.getHandle('main') === undefined) {
|
||||
await agents.createMain();
|
||||
await agents.create({ agentId: 'main' });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue