mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix: resume subagents lazily (#380)
This commit is contained in:
parent
90879f37af
commit
8639105313
11 changed files with 182 additions and 127 deletions
6
.changeset/lazy-subagent-resume.md
Normal file
6
.changeset/lazy-subagent-resume.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@moonshot-ai/agent-core": patch
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Resume saved subagents lazily when they are accessed.
|
||||
|
|
@ -10,6 +10,7 @@ import { MoonshotWebSearchProvider } from '#/tools/providers/moonshot-web-search
|
|||
import type { PromisableMethods } from '#/utils/types';
|
||||
import { getCoreVersion } from '#/version';
|
||||
import { resolveThinkingLevel } from '../agent/config/thinking';
|
||||
import { Agent } from '../agent';
|
||||
import {
|
||||
ensureKimiHome,
|
||||
loadRuntimeConfig,
|
||||
|
|
@ -860,7 +861,9 @@ async function resumeSessionResult(
|
|||
): Promise<ResumeSessionResult> {
|
||||
const api = new SessionAPIImpl(session);
|
||||
const agents: Record<string, ResumedAgentState> = {};
|
||||
for (const [agentId, agent] of session.agents) {
|
||||
for (const [agentId, entry] of session.agents) {
|
||||
if (!(entry instanceof Agent)) continue;
|
||||
const agent = entry;
|
||||
const config = await api.getConfig({ agentId });
|
||||
const context = await api.getContext({ agentId });
|
||||
const permission = await api.getPermission({ agentId });
|
||||
|
|
|
|||
|
|
@ -76,6 +76,13 @@ export interface AgentMeta {
|
|||
readonly parentAgentId: string | null;
|
||||
}
|
||||
|
||||
interface ResumedAgent {
|
||||
readonly agent: Agent;
|
||||
readonly warning?: string;
|
||||
}
|
||||
|
||||
type AgentEntry = Agent | Promise<ResumedAgent>;
|
||||
|
||||
export interface CreateAgentOptions {
|
||||
readonly profile?: ResolvedAgentProfile;
|
||||
readonly parentAgentId?: string;
|
||||
|
|
@ -99,7 +106,7 @@ export class Session {
|
|||
readonly rpc: SDKSessionRPC;
|
||||
readonly telemetry: TelemetryClient;
|
||||
readonly skills: SkillRegistry;
|
||||
readonly agents: Map<string, Agent> = new Map();
|
||||
readonly agents: Map<string, AgentEntry> = new Map();
|
||||
readonly mcp: McpConnectionManager;
|
||||
readonly log: Logger;
|
||||
private readonly logHandle: SessionLogHandle | undefined;
|
||||
|
|
@ -149,7 +156,7 @@ export class Session {
|
|||
}
|
||||
return this.writeMetadata();
|
||||
},
|
||||
auditSink: () => this.agents.get('main')?.records,
|
||||
auditSink: () => this.getReadyAgent('main')?.records,
|
||||
onGoalUpdated: (snapshot, change) => {
|
||||
void this.rpc.emitEvent({ type: 'goal.updated', agentId: 'main', snapshot, change });
|
||||
},
|
||||
|
|
@ -192,37 +199,31 @@ export class Session {
|
|||
// agents are rebuilt. The audit record (if any) is queued and flushed below.
|
||||
await this.goals.normalizeMetadata();
|
||||
this.agents.clear();
|
||||
let warning: string | undefined;
|
||||
const resumeTasks = Object.keys(agents).map(async (id) => {
|
||||
const agent = this.ensureResumeAgentInstantiated(id, agents);
|
||||
const result = await agent.resume();
|
||||
if (result.warning !== undefined && warning === undefined) {
|
||||
warning = result.warning;
|
||||
}
|
||||
});
|
||||
await Promise.all(resumeTasks);
|
||||
// Only the main agent is needed to reopen the session; subagents replay
|
||||
// lazily when an RPC or Agent(resume=...) call asks for their state.
|
||||
const { warning } =
|
||||
agents['main'] === undefined ? { warning: undefined } : await this.resumeAgent('main');
|
||||
// The main-agent audit sink now exists; flush any goal records queued during
|
||||
// normalizeMetadata (e.g. the active -> paused resume transition).
|
||||
this.goals.flushPendingRecords();
|
||||
const resumeWarning = warning;
|
||||
// A session migrated from an external tool ships a wire without the
|
||||
// `config.update` bootstrap events a natively-created agent writes, so the
|
||||
// main agent comes back with an empty system prompt and no tools. Apply the
|
||||
// default profile so the resumed session is usable. Native sessions always
|
||||
// replay a non-empty system prompt and never enter this branch.
|
||||
const main = this.agents.get('main');
|
||||
const main = this.getReadyAgent('main');
|
||||
const profile = DEFAULT_AGENT_PROFILES['agent'];
|
||||
if (main !== undefined && profile !== undefined && main.config.systemPrompt === '') {
|
||||
await this.bootstrapAgentProfile(main, profile);
|
||||
}
|
||||
await this.triggerSessionStart('resume');
|
||||
return { warning: resumeWarning };
|
||||
return { warning };
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
try {
|
||||
await Promise.allSettled(
|
||||
Array.from(this.agents.values(), async (agent) => agent.cron?.stop()),
|
||||
Array.from(this.readyAgents(), async (agent) => agent.cron?.stop()),
|
||||
);
|
||||
await this.stopBackgroundTasksOnExit();
|
||||
await this.flushMetadata();
|
||||
|
|
@ -246,7 +247,7 @@ export class Session {
|
|||
});
|
||||
if (keepAliveOnExit) return;
|
||||
await Promise.all(
|
||||
Array.from(this.agents.values(), (agent) =>
|
||||
Array.from(this.readyAgents(), (agent) =>
|
||||
agent.background.stopAll('Session closed'),
|
||||
),
|
||||
);
|
||||
|
|
@ -279,6 +280,15 @@ export class Session {
|
|||
return { id, agent };
|
||||
}
|
||||
|
||||
async ensureAgentResumed(id: string): Promise<Agent> {
|
||||
const entry = this.agents.get(id);
|
||||
if (entry !== undefined) return (await this.resolveAgentEntry(entry)).agent;
|
||||
if (this.metadata.agents[id] === undefined) {
|
||||
throw new KimiError(ErrorCodes.AGENT_NOT_FOUND, `Agent "${id}" was not found`);
|
||||
}
|
||||
return (await this.resumeAgent(id)).agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a profile's derived config — cwd, system prompt, active tools — to
|
||||
* an agent. Fresh creation and resume-of-an-incomplete-wire both route
|
||||
|
|
@ -323,7 +333,7 @@ export class Session {
|
|||
}
|
||||
|
||||
get hasActiveTurn(): boolean {
|
||||
for (const agent of this.agents.values()) {
|
||||
for (const agent of this.readyAgents()) {
|
||||
if (agent.turn.hasActiveTurn) return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -352,7 +362,7 @@ export class Session {
|
|||
async flushMetadata() {
|
||||
await this.skillsReady;
|
||||
await this.writeMetadataPromise;
|
||||
await Promise.all(Array.from(this.agents.values()).map((agent) => agent.records.flush()));
|
||||
await Promise.all(Array.from(this.readyAgents()).map((agent) => agent.records.flush()));
|
||||
}
|
||||
|
||||
async listSkills(): Promise<readonly SkillSummary[]> {
|
||||
|
|
@ -433,7 +443,7 @@ export class Session {
|
|||
}
|
||||
|
||||
private refreshAgentBuiltinTools(): void {
|
||||
for (const agent of this.agents.values()) {
|
||||
for (const agent of this.readyAgents()) {
|
||||
if (!agent.config.hasProvider) continue;
|
||||
agent.tools.initializeBuiltinTools();
|
||||
}
|
||||
|
|
@ -446,7 +456,7 @@ export class Session {
|
|||
config: Partial<AgentOptions> = {},
|
||||
parentAgentId: string | null = null,
|
||||
): Agent {
|
||||
const parentAgent = parentAgentId !== null ? this.agents.get(parentAgentId) : undefined;
|
||||
const parentAgent = parentAgentId !== null ? this.getReadyAgent(parentAgentId) : undefined;
|
||||
const cwd = parentAgent?.config.cwd ?? this.options.kaos.getcwd();
|
||||
return new Agent({
|
||||
...config,
|
||||
|
|
@ -483,17 +493,30 @@ export class Session {
|
|||
}
|
||||
return {
|
||||
...input,
|
||||
parent: input?.parent ?? this.agents.get(parentAgentId)?.permission,
|
||||
parent: input?.parent ?? this.getReadyAgent(parentAgentId)?.permission,
|
||||
};
|
||||
}
|
||||
|
||||
private ensureResumeAgentInstantiated(
|
||||
getReadyAgent(id: string): Agent | undefined {
|
||||
const entry = this.agents.get(id);
|
||||
return entry instanceof Agent ? entry : undefined;
|
||||
}
|
||||
|
||||
*readyAgents(): Iterable<Agent> {
|
||||
for (const entry of this.agents.values()) {
|
||||
if (entry instanceof Agent) yield entry;
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveAgentEntry(entry: AgentEntry): Promise<ResumedAgent> {
|
||||
if (entry instanceof Agent) return { agent: entry };
|
||||
return entry;
|
||||
}
|
||||
|
||||
private resumeAgent(
|
||||
id: string,
|
||||
agents: Record<string, AgentMeta>,
|
||||
stack: readonly string[] = [],
|
||||
): Agent {
|
||||
const existing = this.agents.get(id);
|
||||
if (existing !== undefined) return existing;
|
||||
): Promise<ResumedAgent> {
|
||||
if (stack.includes(id)) {
|
||||
throw new KimiError(
|
||||
ErrorCodes.SESSION_STATE_INVALID,
|
||||
|
|
@ -501,19 +524,42 @@ export class Session {
|
|||
);
|
||||
}
|
||||
|
||||
const meta = agents[id];
|
||||
const entry = this.agents.get(id);
|
||||
if (entry !== undefined) return this.resolveAgentEntry(entry);
|
||||
|
||||
const promise = this.resumePersistedAgent(id, stack);
|
||||
this.agents.set(id, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
private async resumePersistedAgent(
|
||||
id: string,
|
||||
stack: readonly string[] = [],
|
||||
): Promise<ResumedAgent> {
|
||||
await this.skillsReady;
|
||||
const meta = this.metadata.agents[id];
|
||||
if (meta === undefined) {
|
||||
throw new KimiError(ErrorCodes.SESSION_STATE_INVALID, `Session agent "${id}" is missing`);
|
||||
}
|
||||
|
||||
const parentAgentId = meta.parentAgentId ?? null;
|
||||
if (parentAgentId !== null) {
|
||||
this.ensureResumeAgentInstantiated(parentAgentId, agents, [...stack, id]);
|
||||
}
|
||||
const parent =
|
||||
parentAgentId === null
|
||||
? undefined
|
||||
: await this.resumeAgent(parentAgentId, [...stack, id]);
|
||||
|
||||
const agent = this.instantiateAgent(id, meta.homedir, meta.type, {}, parentAgentId);
|
||||
this.agents.set(id, agent);
|
||||
return agent;
|
||||
try {
|
||||
const agent = this.instantiateAgent(id, meta.homedir, meta.type, {}, parentAgentId);
|
||||
const result = await agent.resume();
|
||||
this.agents.set(id, agent);
|
||||
return { agent, warning: parent?.warning ?? result.warning };
|
||||
} catch (error) {
|
||||
const entry = this.agents.get(id);
|
||||
if (entry instanceof Promise) {
|
||||
this.agents.delete(id);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private nextGeneratedAgentId(): string {
|
||||
|
|
@ -526,7 +572,7 @@ export class Session {
|
|||
}
|
||||
|
||||
private requireMainAgent(): Agent {
|
||||
const agent = this.agents.get('main');
|
||||
const agent = this.getReadyAgent('main');
|
||||
if (agent === undefined) {
|
||||
throw new KimiError(ErrorCodes.AGENT_NOT_FOUND, 'Main agent was not found');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,10 +108,6 @@ export class SessionAPIImpl implements PromisableMethods<SessionAPI> {
|
|||
return this.session.generateAgentsMd();
|
||||
}
|
||||
|
||||
async startBtw({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>): Promise<string> {
|
||||
return this.getAgent(agentId).startBtw(payload);
|
||||
}
|
||||
|
||||
// --- Goal lifecycle (delegates to the session goal store) -------------
|
||||
|
||||
createGoal(payload: CreateGoalPayload) {
|
||||
|
|
@ -140,7 +136,7 @@ export class SessionAPIImpl implements PromisableMethods<SessionAPI> {
|
|||
actor: 'user',
|
||||
reason: payload.reason,
|
||||
});
|
||||
this.session.agents.get('main')?.context.appendSystemReminder(
|
||||
this.session.getReadyAgent('main')?.context.appendSystemReminder(
|
||||
[
|
||||
'The user cancelled the current goal.',
|
||||
'Ignore earlier active-goal reminders for that goal.',
|
||||
|
|
@ -160,121 +156,125 @@ export class SessionAPIImpl implements PromisableMethods<SessionAPI> {
|
|||
if (agentId === 'main') {
|
||||
await this.updatePromptMetadata(promptMetadataTextFromPayload(payload));
|
||||
}
|
||||
return this.getAgent(agentId).prompt(payload);
|
||||
return (await this.getAgent(agentId)).prompt(payload);
|
||||
}
|
||||
|
||||
steer({ agentId, ...payload }: AgentScopedPayload<SteerPayload>) {
|
||||
return this.getAgent(agentId).steer(payload);
|
||||
async steer({ agentId, ...payload }: AgentScopedPayload<SteerPayload>) {
|
||||
return (await this.getAgent(agentId)).steer(payload);
|
||||
}
|
||||
|
||||
cancel({ agentId, ...payload }: AgentScopedPayload<CancelPayload>) {
|
||||
return this.getAgent(agentId).cancel(payload);
|
||||
async cancel({ agentId, ...payload }: AgentScopedPayload<CancelPayload>) {
|
||||
return (await this.getAgent(agentId)).cancel(payload);
|
||||
}
|
||||
|
||||
undoHistory({ agentId, ...payload }: AgentScopedPayload<UndoHistoryPayload>) {
|
||||
return this.getAgent(agentId).undoHistory(payload);
|
||||
async undoHistory({ agentId, ...payload }: AgentScopedPayload<UndoHistoryPayload>) {
|
||||
return (await this.getAgent(agentId)).undoHistory(payload);
|
||||
}
|
||||
|
||||
setModel({ agentId, ...payload }: AgentScopedPayload<SetModelPayload>) {
|
||||
return this.getAgent(agentId).setModel(payload);
|
||||
async setModel({ agentId, ...payload }: AgentScopedPayload<SetModelPayload>) {
|
||||
return (await this.getAgent(agentId)).setModel(payload);
|
||||
}
|
||||
|
||||
setThinking({ agentId, ...payload }: AgentScopedPayload<SetThinkingPayload>) {
|
||||
return this.getAgent(agentId).setThinking(payload);
|
||||
async setThinking({ agentId, ...payload }: AgentScopedPayload<SetThinkingPayload>) {
|
||||
return (await this.getAgent(agentId)).setThinking(payload);
|
||||
}
|
||||
|
||||
setPermission({ agentId, ...payload }: AgentScopedPayload<SetPermissionPayload>) {
|
||||
return this.getAgent(agentId).setPermission(payload);
|
||||
async setPermission({ agentId, ...payload }: AgentScopedPayload<SetPermissionPayload>) {
|
||||
return (await this.getAgent(agentId)).setPermission(payload);
|
||||
}
|
||||
|
||||
getModel({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return this.getAgent(agentId).getModel(payload);
|
||||
async getModel({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return (await this.getAgent(agentId)).getModel(payload);
|
||||
}
|
||||
|
||||
enterPlan({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return this.getAgent(agentId).enterPlan(payload);
|
||||
async enterPlan({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return (await this.getAgent(agentId)).enterPlan(payload);
|
||||
}
|
||||
|
||||
cancelPlan({ agentId, ...payload }: AgentScopedPayload<CancelPlanPayload>) {
|
||||
return this.getAgent(agentId).cancelPlan(payload);
|
||||
async cancelPlan({ agentId, ...payload }: AgentScopedPayload<CancelPlanPayload>) {
|
||||
return (await this.getAgent(agentId)).cancelPlan(payload);
|
||||
}
|
||||
|
||||
clearPlan({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return this.getAgent(agentId).clearPlan(payload);
|
||||
async clearPlan({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return (await this.getAgent(agentId)).clearPlan(payload);
|
||||
}
|
||||
|
||||
beginCompaction({ agentId, ...payload }: AgentScopedPayload<BeginCompactionPayload>) {
|
||||
return this.getAgent(agentId).beginCompaction(payload);
|
||||
async beginCompaction({ agentId, ...payload }: AgentScopedPayload<BeginCompactionPayload>) {
|
||||
return (await this.getAgent(agentId)).beginCompaction(payload);
|
||||
}
|
||||
|
||||
cancelCompaction({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return this.getAgent(agentId).cancelCompaction(payload);
|
||||
async cancelCompaction({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return (await this.getAgent(agentId)).cancelCompaction(payload);
|
||||
}
|
||||
|
||||
registerTool({ agentId, ...payload }: AgentScopedPayload<RegisterToolPayload>) {
|
||||
return this.getAgent(agentId).registerTool(payload);
|
||||
async registerTool({ agentId, ...payload }: AgentScopedPayload<RegisterToolPayload>) {
|
||||
return (await this.getAgent(agentId)).registerTool(payload);
|
||||
}
|
||||
|
||||
unregisterTool({ agentId, ...payload }: AgentScopedPayload<UnregisterToolPayload>) {
|
||||
return this.getAgent(agentId).unregisterTool(payload);
|
||||
async unregisterTool({ agentId, ...payload }: AgentScopedPayload<UnregisterToolPayload>) {
|
||||
return (await this.getAgent(agentId)).unregisterTool(payload);
|
||||
}
|
||||
|
||||
setActiveTools({ agentId, ...payload }: AgentScopedPayload<SetActiveToolsPayload>) {
|
||||
return this.getAgent(agentId).setActiveTools(payload);
|
||||
async setActiveTools({ agentId, ...payload }: AgentScopedPayload<SetActiveToolsPayload>) {
|
||||
return (await this.getAgent(agentId)).setActiveTools(payload);
|
||||
}
|
||||
|
||||
stopBackground({ agentId, ...payload }: AgentScopedPayload<StopBackgroundPayload>) {
|
||||
return this.getAgent(agentId).stopBackground(payload);
|
||||
async stopBackground({ agentId, ...payload }: AgentScopedPayload<StopBackgroundPayload>) {
|
||||
return (await this.getAgent(agentId)).stopBackground(payload);
|
||||
}
|
||||
|
||||
clearContext({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return this.getAgent(agentId).clearContext(payload);
|
||||
async clearContext({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return (await this.getAgent(agentId)).clearContext(payload);
|
||||
}
|
||||
|
||||
async activateSkill({ agentId, ...payload }: AgentScopedPayload<ActivateSkillPayload>) {
|
||||
await this.getAgent(agentId).activateSkill(payload);
|
||||
await (await this.getAgent(agentId)).activateSkill(payload);
|
||||
if (agentId === 'main') {
|
||||
await this.updatePromptMetadata(promptMetadataTextFromSkill(payload));
|
||||
}
|
||||
}
|
||||
|
||||
getBackgroundOutput({ agentId, ...payload }: AgentScopedPayload<GetBackgroundOutputPayload>) {
|
||||
return this.getAgent(agentId).getBackgroundOutput(payload);
|
||||
async startBtw({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>): Promise<string> {
|
||||
return (await this.getAgent(agentId)).startBtw(payload);
|
||||
}
|
||||
|
||||
getContext({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return this.getAgent(agentId).getContext(payload);
|
||||
async getBackgroundOutput({
|
||||
agentId,
|
||||
...payload
|
||||
}: AgentScopedPayload<GetBackgroundOutputPayload>) {
|
||||
return (await this.getAgent(agentId)).getBackgroundOutput(payload);
|
||||
}
|
||||
|
||||
getConfig({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return this.getAgent(agentId).getConfig(payload);
|
||||
async getContext({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return (await this.getAgent(agentId)).getContext(payload);
|
||||
}
|
||||
|
||||
getPermission({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return this.getAgent(agentId).getPermission(payload);
|
||||
async getConfig({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return (await this.getAgent(agentId)).getConfig(payload);
|
||||
}
|
||||
|
||||
getPlan({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return this.getAgent(agentId).getPlan(payload);
|
||||
async getPermission({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return (await this.getAgent(agentId)).getPermission(payload);
|
||||
}
|
||||
|
||||
getUsage({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return this.getAgent(agentId).getUsage(payload);
|
||||
async getPlan({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return (await this.getAgent(agentId)).getPlan(payload);
|
||||
}
|
||||
|
||||
getTools({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return this.getAgent(agentId).getTools(payload);
|
||||
async getUsage({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return (await this.getAgent(agentId)).getUsage(payload);
|
||||
}
|
||||
|
||||
getBackground({ agentId, ...payload }: AgentScopedPayload<GetBackgroundPayload>) {
|
||||
return this.getAgent(agentId).getBackground(payload);
|
||||
async getTools({ agentId, ...payload }: AgentScopedPayload<EmptyPayload>) {
|
||||
return (await this.getAgent(agentId)).getTools(payload);
|
||||
}
|
||||
|
||||
private getAgent(agentId: string): PromisableMethods<AgentAPI> {
|
||||
const agent = this.session.agents.get(agentId);
|
||||
if (agent === undefined) {
|
||||
throw new KimiError(ErrorCodes.AGENT_NOT_FOUND, `Agent "${agentId}" was not found`);
|
||||
}
|
||||
async getBackground({ agentId, ...payload }: AgentScopedPayload<GetBackgroundPayload>) {
|
||||
return (await this.getAgent(agentId)).getBackground(payload);
|
||||
}
|
||||
|
||||
private async getAgent(agentId: string): Promise<PromisableMethods<AgentAPI>> {
|
||||
const agent = await this.session.ensureAgentResumed(agentId);
|
||||
return agent.rpcMethods;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,10 +81,7 @@ export class SessionSubagentHost {
|
|||
async spawn(profileName: string, options: RunSubagentOptions): Promise<SubagentHandle> {
|
||||
options.signal.throwIfAborted();
|
||||
|
||||
const parent = this.session.agents.get(this.ownerAgentId);
|
||||
if (parent === undefined) {
|
||||
throw new Error(`Parent agent "${this.ownerAgentId}" was not found`);
|
||||
}
|
||||
const parent = await this.session.ensureAgentResumed(this.ownerAgentId);
|
||||
|
||||
const profile = this.resolveProfile(parent, profileName);
|
||||
const { id, agent } = await this.session.createAgent(
|
||||
|
|
@ -124,15 +121,7 @@ export class SessionSubagentHost {
|
|||
async resume(agentId: string, options: RunSubagentOptions): Promise<SubagentHandle> {
|
||||
options.signal.throwIfAborted();
|
||||
|
||||
const parent = this.session.agents.get(this.ownerAgentId);
|
||||
if (parent === undefined) {
|
||||
throw new Error(`Parent agent "${this.ownerAgentId}" was not found`);
|
||||
}
|
||||
|
||||
const child = this.session.agents.get(agentId);
|
||||
if (child === undefined) {
|
||||
throw new Error(`Agent instance "${agentId}" was not found`);
|
||||
}
|
||||
const parent = await this.session.ensureAgentResumed(this.ownerAgentId);
|
||||
const metadata = this.session.metadata.agents[agentId];
|
||||
if (metadata?.type !== 'sub') {
|
||||
throw new Error(`Agent instance "${agentId}" is not a subagent`);
|
||||
|
|
@ -140,6 +129,7 @@ export class SessionSubagentHost {
|
|||
if (metadata.parentAgentId !== this.ownerAgentId) {
|
||||
throw new Error(`Agent instance "${agentId}" does not belong to this parent agent`);
|
||||
}
|
||||
const child = await this.session.ensureAgentResumed(agentId);
|
||||
if (this.activeChildren.has(agentId) || child.turn.hasActiveTurn) {
|
||||
throw new Error(
|
||||
`Agent instance "${agentId}" is already running and cannot be resumed concurrently`,
|
||||
|
|
@ -185,7 +175,7 @@ export class SessionSubagentHost {
|
|||
}
|
||||
|
||||
async startBtw(): Promise<string> {
|
||||
const parent = this.session.agents.get(this.ownerAgentId)!;
|
||||
const parent = await this.session.ensureAgentResumed(this.ownerAgentId);
|
||||
const { id, agent: child } = await this.session.createAgent(
|
||||
{
|
||||
type: 'sub',
|
||||
|
|
@ -215,19 +205,19 @@ export class SessionSubagentHost {
|
|||
([, child]) => !child.runInBackground,
|
||||
);
|
||||
for (const [childId, child] of foregroundChildren) {
|
||||
this.session.agents.get(childId)?.subagentHost?.cancelAll(reason);
|
||||
this.session.getReadyAgent(childId)?.subagentHost?.cancelAll(reason);
|
||||
// Abort with the cancel reason (a user interruption by default) so the
|
||||
// subagent's in-flight tools report the cause accurately to the model.
|
||||
child.controller.abort(reason);
|
||||
}
|
||||
}
|
||||
|
||||
getProfileName(agentId: string): string | undefined {
|
||||
async getProfileName(agentId: string): Promise<string | undefined> {
|
||||
const metadata = this.session.metadata.agents[agentId];
|
||||
if (metadata?.type !== 'sub' || metadata.parentAgentId !== this.ownerAgentId) {
|
||||
return undefined;
|
||||
}
|
||||
return this.session.agents.get(agentId)?.config.profileName;
|
||||
return (await this.session.ensureAgentResumed(agentId)).config.profileName;
|
||||
}
|
||||
|
||||
private resolveProfile(parent: Agent, profileName: string): ResolvedAgentProfile {
|
||||
|
|
|
|||
|
|
@ -136,11 +136,11 @@ export class AgentTool implements BuiltinTool<AgentToolInput> {
|
|||
|
||||
private readonly log?: Logger;
|
||||
|
||||
resolveExecution(args: AgentToolInput): ToolExecution {
|
||||
async resolveExecution(args: AgentToolInput): Promise<ToolExecution> {
|
||||
let profileName = args.subagent_type?.length ? args.subagent_type : 'coder';
|
||||
const resumeAgentId = args.resume?.trim();
|
||||
if (resumeAgentId !== undefined && resumeAgentId.length > 0) {
|
||||
profileName = this.subagentHost.getProfileName?.(resumeAgentId) ?? 'subagent';
|
||||
profileName = (await this.subagentHost.getProfileName?.(resumeAgentId)) ?? 'subagent';
|
||||
}
|
||||
const prefix = args.run_in_background === true ? 'Launching background' : 'Launching';
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ max_context_size = 100000
|
|||
|
||||
const created = await rpc.createSession({ id: 'ses_runtime_default_model', workDir });
|
||||
const session = core.sessions.get(created.id);
|
||||
const mainAgent = session?.agents.get('main');
|
||||
const mainAgent = session?.getReadyAgent('main');
|
||||
|
||||
expect(mainAgent?.config.modelAlias).toBe('default-mock');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ describe('AgentAPI.startBtw', () => {
|
|||
expect(agentId).toBe('agent-0');
|
||||
expect(scripted.calls).toHaveLength(0);
|
||||
expect(session.metadata.agents[agentId]).toBeUndefined();
|
||||
const childAgent = session.agents.get(agentId);
|
||||
const childAgent = session.getReadyAgent(agentId);
|
||||
if (childAgent === undefined) throw new Error('Expected /btw child agent');
|
||||
const inheritedHistory = trimTrailingOpenToolExchange(
|
||||
mainAgent.context.project(mainAgent.context.history),
|
||||
|
|
@ -274,7 +274,7 @@ describe('AgentAPI.startBtw', () => {
|
|||
expect(JSON.stringify(mainAgent.context.history)).not.toContain(
|
||||
'What are you working on right now?',
|
||||
);
|
||||
expect(JSON.stringify(session.agents.get('agent-0')?.context.history)).toContain(
|
||||
expect(JSON.stringify(session.getReadyAgent('agent-0')?.context.history)).toContain(
|
||||
'What are you working on right now?',
|
||||
);
|
||||
scripted.mockNextResponse({ type: 'text', text: 'Follow-up answer from the same side agent.' });
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ describe('SessionSubagentHost', () => {
|
|||
const host = new SessionSubagentHost(
|
||||
{
|
||||
agents: new Map([['main', parent.agent]]),
|
||||
ensureAgentResumed: vi.fn(async () => parent.agent),
|
||||
createAgent,
|
||||
} as never,
|
||||
'main',
|
||||
|
|
@ -349,6 +350,7 @@ describe('SessionSubagentHost', () => {
|
|||
const host = new SessionSubagentHost(
|
||||
{
|
||||
agents: new Map([['main', parent.agent]]),
|
||||
ensureAgentResumed: vi.fn(async () => parent.agent),
|
||||
createAgent,
|
||||
} as never,
|
||||
'main',
|
||||
|
|
@ -891,7 +893,7 @@ describe('Session resume permission parent chain', () => {
|
|||
try {
|
||||
await session.resume();
|
||||
|
||||
const child = session.agents.get('agent-0');
|
||||
const child = await session.ensureAgentResumed('agent-0');
|
||||
expect(child?.permission.mode).toBe('yolo');
|
||||
expect(child?.permission.rules).toEqual([]);
|
||||
expect(child?.permission.data().rules).toEqual([]);
|
||||
|
|
@ -1133,6 +1135,14 @@ function fakeSession(
|
|||
custom: {},
|
||||
},
|
||||
writeMetadata: vi.fn(async () => {}),
|
||||
getReadyAgent: vi.fn((id: string) => agents.get(id)),
|
||||
ensureAgentResumed: vi.fn(async (id: string) => {
|
||||
const agent = agents.get(id);
|
||||
if (agent === undefined) {
|
||||
throw new Error(`Agent "${id}" was not found`);
|
||||
}
|
||||
return agent;
|
||||
}),
|
||||
createAgent: vi.fn(
|
||||
async (
|
||||
config: Parameters<Session['createAgent']>[0],
|
||||
|
|
|
|||
|
|
@ -385,10 +385,10 @@ describe('AgentTool', () => {
|
|||
expect(result.output).toContain('actual_subagent_type: explore');
|
||||
});
|
||||
|
||||
it('declares no resource accesses so concurrent Agent calls can run in parallel', () => {
|
||||
it('declares no resource accesses so concurrent Agent calls can run in parallel', async () => {
|
||||
const host = mockSubagentHost({ spawn: vi.fn() });
|
||||
const tool = new AgentTool(host);
|
||||
const execution = tool.resolveExecution({
|
||||
const execution = await tool.resolveExecution({
|
||||
prompt: 'Investigate',
|
||||
description: 'Find cause',
|
||||
subagent_type: 'explore',
|
||||
|
|
@ -398,13 +398,13 @@ describe('AgentTool', () => {
|
|||
expect(execution.accesses).toEqual(ToolAccesses.none());
|
||||
});
|
||||
|
||||
it('uses the resumed agent profile in the activity description', () => {
|
||||
it('uses the resumed agent profile in the activity description', async () => {
|
||||
const host = mockSubagentHost({
|
||||
spawn: vi.fn(),
|
||||
getProfileName: vi.fn().mockReturnValue('explore'),
|
||||
});
|
||||
const tool = new AgentTool(host);
|
||||
const execution = tool.resolveExecution({
|
||||
const execution = await tool.resolveExecution({
|
||||
prompt: 'Continue',
|
||||
description: 'Continue work',
|
||||
resume: ' agent-existing ',
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@
|
|||
* - `SessionStore.list({ workDir })`
|
||||
* - `encodeWorkDirKey` / `normalizeWorkDir`
|
||||
* all from `@moonshot-ai/agent-core/session/store`.
|
||||
* - `Session` (constructor + `resume()` + `agents` map), from
|
||||
* - `Session` (constructor + `resume()` + `getReadyAgent()`), from
|
||||
* `@moonshot-ai/agent-core`; `localKaos` from `@moonshot-ai/kaos`. After
|
||||
* `resume()`, `session.agents.get('main').context.messages` exposes the
|
||||
* `resume()`, `session.getReadyAgent('main').context.messages` exposes the
|
||||
* replayed message history.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
|
@ -134,7 +134,7 @@ describe('migrated session loads in real kimi-core', () => {
|
|||
});
|
||||
try {
|
||||
await session.resume();
|
||||
const mainAgent = session.agents.get('main');
|
||||
const mainAgent = session.getReadyAgent('main');
|
||||
expect(mainAgent).toBeDefined();
|
||||
|
||||
// The migrated wire carries no `config.update` bootstrap events, so a
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue