fix: resume subagents lazily (#380)

This commit is contained in:
_Kerman 2026-06-03 20:16:43 +08:00 committed by GitHub
parent 90879f37af
commit 8639105313
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 182 additions and 127 deletions

View file

@ -0,0 +1,6 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kimi-code": patch
---
Resume saved subagents lazily when they are accessed.

View file

@ -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 });

View file

@ -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');
}

View file

@ -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;
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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');
});

View file

@ -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.' });

View file

@ -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],

View file

@ -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 ',

View file

@ -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