kimi-code/packages/agent-core/test/tools/agent.test.ts
_Kerman ba64072559
Some checks failed
CI / build (push) Has been cancelled
CI / test (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / typecheck (push) Has been cancelled
Nix Build / Check flake.nix workspace sync (push) Has been cancelled
Release / Release (push) Has been cancelled
Release / Native release artifact (push) Has been cancelled
Nix Build / nix build .#kimi-code (push) Has been cancelled
Release / Deploy docs (push) Has been cancelled
Release / Publish native release assets (push) Has been cancelled
feat: detach foreground tasks to background (#821)
2026-06-20 21:24:22 +08:00

889 lines
29 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { ToolAccesses } from '../../src/loop';
import type { Logger, LogPayload } from '../../src/logging';
import type { ResolvedAgentProfile } from '../../src/profile';
import type { SessionSubagentHost } from '../../src/session/subagent-host';
import { AgentTool, AgentToolInputSchema } from '../../src/tools/builtin/collaboration/agent';
import { userCancellationReason } from '../../src/utils/abort';
import { agentTask, createBackgroundManager } from '../agent/background/helpers';
import { executeTool } from './fixtures/execute-tool';
const signal = new AbortController().signal;
function context<Input>(args: Input, toolCallId = 'call_agent') {
return { turnId: '0', toolCallId, args, signal };
}
function mockSubagentHost<T extends Pick<SessionSubagentHost, 'spawn'> & Partial<SessionSubagentHost>>(
host: T,
): T & SessionSubagentHost {
return { resume: vi.fn(), ...host } as unknown as T & SessionSubagentHost;
}
function agentTool(
host: SessionSubagentHost,
background = createBackgroundManager().manager,
subagents?: ResolvedAgentProfile['subagents'],
options?: ConstructorParameters<typeof AgentTool>[3],
): AgentTool {
return new AgentTool(host, background, subagents, options);
}
interface CapturedLogEntry {
readonly level: 'error' | 'warn' | 'info' | 'debug';
readonly message: string;
readonly payload: LogPayload | undefined;
}
function captureLogs(): { logger: Logger; entries: CapturedLogEntry[] } {
const entries: CapturedLogEntry[] = [];
const capture =
(level: CapturedLogEntry['level']) => (message: string, payload?: LogPayload) => {
entries.push({ level, message, payload });
};
const logger: Logger = {
error: capture('error'),
warn: capture('warn'),
info: capture('info'),
debug: capture('debug'),
createChild: () => logger,
};
return {
entries,
logger,
};
}
describe('AgentTool', () => {
it('accepts the snake_case background parameter', () => {
const parsed = AgentToolInputSchema.parse({
prompt: 'Investigate',
description: 'Find cause',
subagent_type: 'explore',
run_in_background: true,
});
expect(parsed).toMatchObject({
prompt: 'Investigate',
description: 'Find cause',
subagent_type: 'explore',
run_in_background: true,
});
});
it('exposes run_in_background and not runInBackground in the JSON schema', () => {
const host = mockSubagentHost({ spawn: vi.fn() });
const tool = agentTool(host);
const properties = (tool.parameters as { properties: Record<string, unknown> }).properties;
expect(properties).toHaveProperty('run_in_background');
expect(properties).not.toHaveProperty('runInBackground');
});
it('describes subagent_type and run_in_background parameters', () => {
const host = mockSubagentHost({ spawn: vi.fn() });
const tool = agentTool(host);
const properties = (
tool.parameters as {
properties: Record<string, { description?: string }>;
}
).properties;
const subagentTypeDescription = properties['subagent_type']?.description ?? '';
// #7: the description states the default is coder
expect(subagentTypeDescription).toContain('coder');
// #6: terminology aligned with the "Available agent types" prose heading —
// no longer "agent registry"
expect(subagentTypeDescription).not.toContain('registry');
expect(subagentTypeDescription).toContain('agent type');
expect(properties['run_in_background']?.description).toContain('false');
});
it('does not expose a timeout parameter in the JSON schema', () => {
const host = mockSubagentHost({ spawn: vi.fn() });
const tool = agentTool(host);
const properties = (tool.parameters as { properties: Record<string, unknown> }).properties;
expect(properties).not.toHaveProperty('timeout');
});
it('explains the fixed background subagent timeout', () => {
const host = mockSubagentHost({ spawn: vi.fn() });
const tool = agentTool(host);
expect(tool.description).toContain('fixed 30-minute timeout');
expect(tool.description).not.toContain('operator-configured background timeout');
expect(tool.description).not.toContain('no time limit');
});
it('does not expose a model parameter in the JSON schema', () => {
const host = mockSubagentHost({ spawn: vi.fn() });
const tool = agentTool(host);
const properties = (tool.parameters as { properties: Record<string, unknown> }).properties;
expect(properties).not.toHaveProperty('model');
});
it('renders the tool set for each subagent type', () => {
const host = mockSubagentHost({ spawn: vi.fn() });
const subagents = {
explore: profile({
name: 'explore',
description: 'Read-only exploration.',
tools: ['Read', 'Grep', 'Glob'],
}),
coder: profile({
name: 'coder',
description: 'General coding.',
tools: ['Read', 'Write', 'Edit', 'Bash'],
}),
};
const tool = agentTool(host, createBackgroundManager().manager, subagents);
expect(tool.description).toContain('Tools: Read, Grep, Glob');
expect(tool.description).toContain('Tools: Read, Write, Edit, Bash');
});
it('mentions resume preference and result visibility in the description', () => {
const host = mockSubagentHost({ spawn: vi.fn() });
const tool = agentTool(host);
expect(tool.description.toLowerCase()).toContain('resume');
expect(tool.description.toLowerCase()).toContain('only visible to you');
expect(tool.description.toLowerCase()).toContain('when not to');
});
it('normalizes the default subagent type into tool args', () => {
expect(
AgentToolInputSchema.parse({
prompt: 'Investigate',
description: 'Find cause',
}).subagent_type,
).toBe('coder');
expect(
AgentToolInputSchema.parse({
prompt: 'Investigate',
description: 'Find cause',
subagent_type: '',
}).subagent_type,
).toBe('coder');
expect(
AgentToolInputSchema.parse({
prompt: 'Continue',
description: 'Continue work',
resume: 'agent-existing',
}).subagent_type,
).toBeUndefined();
});
it('describes configured subagent types', () => {
const host = mockSubagentHost({ spawn: vi.fn() });
const subagents = {
explore: profile({
name: 'explore',
description: 'Read-only exploration.',
whenToUse: 'Use for searches.',
}),
coder: profile({ name: 'coder', description: 'General coding.' }),
};
const tool = agentTool(host, createBackgroundManager().manager, subagents);
expect(tool.description).toContain('Available agent types');
expect(tool.description).toContain('- explore: Read-only exploration. Use for searches.');
expect(tool.description).toContain('- coder: General coding.');
});
it('spawns a foreground subagent and returns its summary', async () => {
const host = mockSubagentHost({
spawn: vi.fn().mockResolvedValue({
agentId: 'agent-child',
profileName: 'explore',
resumed: false,
completion: Promise.resolve({ result: 'child result' }),
}),
});
const tool = agentTool(host);
const result = await executeTool(tool,
context({
prompt: 'Investigate',
description: 'Find cause',
subagent_type: 'explore',
}),
);
expect(host.spawn).toHaveBeenCalledWith(
expect.objectContaining({
profileName: 'explore',
parentToolCallId: 'call_agent',
prompt: 'Investigate',
description: 'Find cause',
runInBackground: false,
signal: expect.any(AbortSignal),
}),
);
expect(result.output).toContain('agent_id: agent-child');
expect(result.output).toContain('actual_subagent_type: explore');
expect(result.output).toContain('child result');
});
it('falls back to coder for an empty subagent type', async () => {
const host = mockSubagentHost({
spawn: vi.fn().mockResolvedValue({
agentId: 'agent-child',
profileName: 'coder',
resumed: false,
completion: Promise.resolve({ result: 'child result' }),
}),
});
const tool = agentTool(host);
await executeTool(tool,
context({
prompt: 'Investigate',
description: 'Find cause',
subagent_type: '',
}),
);
expect(host.spawn).toHaveBeenCalledWith(
expect.objectContaining({
parentToolCallId: 'call_agent',
profileName: 'coder',
}),
);
});
it('resumes a foreground subagent when resume is provided', async () => {
const host = mockSubagentHost({
spawn: vi.fn(),
resume: vi.fn().mockResolvedValue({
agentId: 'agent-existing',
profileName: 'explore',
resumed: true,
completion: Promise.resolve({ result: 'resumed result' }),
}),
});
const tool = agentTool(host);
const result = await executeTool(tool,
context({
prompt: 'Continue',
description: 'Continue work',
resume: 'agent-existing',
}),
);
expect(host.spawn).not.toHaveBeenCalled();
expect(host.resume).toHaveBeenCalledWith(
'agent-existing',
expect.objectContaining({
parentToolCallId: 'call_agent',
prompt: 'Continue',
description: 'Continue work',
runInBackground: false,
signal: expect.any(AbortSignal),
}),
);
expect(result.output).toContain('agent_id: agent-existing');
expect(result.output).toContain('actual_subagent_type: explore');
expect(result.output).toContain('resumed result');
});
it('returns an error when resuming with a subagent type', async () => {
const host = mockSubagentHost({
spawn: vi.fn(),
resume: vi.fn(),
});
const tool = agentTool(host);
const result = await executeTool(tool,
context({
prompt: 'Continue',
description: 'Continue work',
resume: 'agent-existing',
subagent_type: 'explore',
}),
);
expect(result).toMatchObject({
isError: true,
output: 'Cannot set subagent_type when resuming an existing agent. Resume by agent id only.',
});
expect(host.resume).not.toHaveBeenCalled();
});
it('does not consume a background task slot when validation fails before launch', async () => {
const completion = new Promise<{ result: string }>(() => {});
const background = createBackgroundManager({ maxRunningTasks: 1 }).manager;
const host = mockSubagentHost({
spawn: vi.fn().mockResolvedValue({
agentId: 'agent-child',
profileName: 'coder',
resumed: false,
completion,
}),
resume: vi.fn(),
});
const tool = new AgentTool(host, background);
const invalid = await executeTool(tool,
context({
prompt: 'Continue',
description: 'Invalid background resume',
resume: 'agent-existing',
subagent_type: 'explore',
run_in_background: true,
}),
);
const valid = await executeTool(tool,
context({
prompt: 'Investigate',
description: 'Find cause',
run_in_background: true,
}),
);
expect(invalid).toMatchObject({
isError: true,
output: 'Cannot set subagent_type when resuming an existing agent. Resume by agent id only.',
});
expect(valid.output).toContain('status: running');
expect(host.resume).not.toHaveBeenCalled();
expect(host.spawn).toHaveBeenCalledTimes(1);
});
it('resumes by id without constraining the subagent type', async () => {
const host = mockSubagentHost({
spawn: vi.fn(),
resume: vi.fn().mockResolvedValue({
agentId: 'agent-existing',
profileName: 'explore',
resumed: true,
completion: Promise.resolve({ result: 'resumed result' }),
}),
});
const tool = agentTool(host);
const result = await executeTool(tool,
context({
prompt: 'Continue',
description: 'Continue work',
resume: 'agent-existing',
}),
);
expect(host.spawn).not.toHaveBeenCalled();
expect(host.resume).toHaveBeenCalledWith(
'agent-existing',
expect.objectContaining({
parentToolCallId: 'call_agent',
prompt: 'Continue',
description: 'Continue work',
runInBackground: false,
signal: expect.any(AbortSignal),
}),
);
expect(result.output).toContain('actual_subagent_type: explore');
});
it('declares no resource accesses so concurrent Agent calls can run in parallel', async () => {
const host = mockSubagentHost({ spawn: vi.fn() });
const tool = agentTool(host);
const execution = await tool.resolveExecution({
prompt: 'Investigate',
description: 'Find cause',
subagent_type: 'explore',
});
if (execution.isError === true) throw new Error('expected runnable execution');
expect(execution.accesses).toEqual(ToolAccesses.none());
});
it('uses the resumed agent profile in the activity description', async () => {
const host = mockSubagentHost({
spawn: vi.fn(),
getProfileName: vi.fn().mockReturnValue('explore'),
});
const tool = agentTool(host);
const execution = await tool.resolveExecution({
prompt: 'Continue',
description: 'Continue work',
resume: ' agent-existing ',
});
expect(execution.isError).toBeFalsy();
if (execution.isError === true) throw new Error('expected runnable execution');
expect(execution.description).toBe('Launching explore agent: Continue work');
expect(host.getProfileName).toHaveBeenCalledWith('agent-existing');
});
it('registers background subagents with the background manager', async () => {
const completion = new Promise<{ result: string }>(() => {});
const host = mockSubagentHost({
spawn: vi.fn().mockResolvedValue({
agentId: 'agent-child',
profileName: 'coder',
resumed: false,
completion,
}),
});
const background = createBackgroundManager().manager;
const tool = new AgentTool(host, background);
const result = await executeTool(tool,
context({
prompt: 'Investigate',
description: 'Find cause',
run_in_background: true,
}),
);
expect(result.output).toContain('status: running');
expect(result.output).toContain('agent_id: agent-child');
if (typeof result.output !== 'string') throw new TypeError('expected string output');
const taskId = result.output.match(/task_id: (agent-[0-9a-z]{8})/)?.[1];
expect(taskId).toBeDefined();
expect(background.getTask(taskId!)).toMatchObject({
status: 'running',
description: 'Find cause',
timeoutMs: 30 * 60 * 1000,
});
});
it('can detach a foreground subagent through the background manager', async () => {
let resolveCompletion: (value: { result: string }) => void = () => {};
const completion = new Promise<{ result: string }>((resolve) => {
resolveCompletion = resolve;
});
const markActiveChildDetached = vi.fn();
const host = mockSubagentHost({
markActiveChildDetached,
spawn: vi.fn().mockResolvedValue({
agentId: 'agent-child',
profileName: 'coder',
resumed: false,
completion,
}),
});
const background = createBackgroundManager().manager;
const tool = new AgentTool(host, background);
const running = executeTool(tool,
context({
prompt: 'Investigate',
description: 'Find cause',
}),
);
await vi.waitFor(() => {
expect(background.list(false)).toHaveLength(1);
});
const task = background.list(false)[0]!;
expect(task).toMatchObject({
kind: 'agent',
detached: false,
agentId: 'agent-child',
});
background.detach(task.taskId);
const result = await running;
expect(markActiveChildDetached).toHaveBeenCalledWith('agent-child');
expect(result.output).toContain(`task_id: ${task.taskId}`);
expect(result.output).toContain('agent_id: agent-child');
expect(result.output).toContain('automatic_notification: true');
resolveCompletion({ result: 'finished later' });
await expect(background.wait(task.taskId)).resolves.toMatchObject({
status: 'completed',
detached: true,
});
});
it('does not recommend disabled task tools when a foreground subagent is detached', async () => {
let resolveCompletion: (value: { result: string }) => void = () => {};
const completion = new Promise<{ result: string }>((resolve) => {
resolveCompletion = resolve;
});
const host = mockSubagentHost({
spawn: vi.fn().mockResolvedValue({
agentId: 'agent-child',
profileName: 'coder',
resumed: false,
completion,
}),
});
const background = createBackgroundManager().manager;
const tool = agentTool(host, background, undefined, { allowBackground: false });
const running = executeTool(
tool,
context({
prompt: 'Investigate',
description: 'Find cause',
}),
);
await vi.waitFor(() => {
expect(background.list(false)).toHaveLength(1);
});
const task = background.list(false)[0]!;
background.detach(task.taskId);
const result = await running;
expect(result.output).toContain(`task_id: ${task.taskId}`);
expect(result.output).toContain('next_step: The completion arrives automatically');
expect(result.output).not.toContain('TaskOutput');
expect(result.output).not.toContain('TaskStop');
resolveCompletion({ result: 'finished later' });
await expect(background.wait(task.taskId)).resolves.toMatchObject({
status: 'completed',
detached: true,
});
});
it('guides the AI with a non-blocking query hint and a resume hint on background launch', async () => {
const host = mockSubagentHost({
spawn: vi.fn().mockResolvedValue({
agentId: 'agent-child',
profileName: 'coder',
resumed: false,
completion: new Promise<{ result: string }>(() => {}),
}),
});
const background = createBackgroundManager().manager;
const tool = new AgentTool(host, background);
const result = await executeTool(tool,
context({
prompt: 'Investigate',
description: 'Find cause',
run_in_background: true,
}),
);
if (typeof result.output !== 'string') throw new TypeError('expected string output');
const taskId = result.output.match(/task_id: (agent-[0-9a-z]{8})/)?.[1];
expect(taskId).toBeDefined();
// M9: next_step — non-blocking progress check via TaskOutput
expect(result.output).toContain('next_step:');
expect(result.output).toContain(`TaskOutput(task_id="${taskId!}", block=false)`);
// M9: resume_hint — continue the same subagent instance
expect(result.output).toContain('resume_hint:');
expect(result.output).toContain('Agent(resume="agent-child"');
// The hint disambiguates the two look-alike identifiers in this output:
// `agent_id` (what `subagentHost.resume` accepts) and `task_id` (the
// BackgroundManager ledger id, which also shows up as `source_id` in
// later <notification> entries). LLMs regularly copy the wrong one.
expect(result.output).toMatch(/agent_id.*not.*task_id|task_id.*not.*agent_id/i);
// Recovery scenario — `task.lost` etc. — must be called out so the
// model knows the hint is not only for happy-path follow-up work.
expect(result.output).toMatch(/task\.lost|task\.failed|task\.killed/);
});
it('rejects background subagents when background execution is disabled', async () => {
const host = mockSubagentHost({
spawn: vi.fn().mockResolvedValue({
agentId: 'agent-child',
profileName: 'coder',
resumed: false,
completion: new Promise<{ result: string }>(() => {}),
}),
});
const tool = agentTool(host, createBackgroundManager().manager, undefined, {
allowBackground: false,
});
expect(tool.description).toContain('Background agent execution is disabled for this agent.');
expect(tool.description).not.toContain('the subagent runs detached from this turn');
const result = await executeTool(tool,
context({
prompt: 'Investigate',
description: 'Find cause',
run_in_background: true,
}),
);
expect(result).toMatchObject({
isError: true,
output:
'Background agent execution is not available for this agent because TaskList, TaskOutput, and TaskStop are not enabled.',
});
expect(host.spawn).not.toHaveBeenCalled();
});
it('returns an error when background registration hits the task limit', async () => {
const background = createBackgroundManager({ maxRunningTasks: 1 }).manager;
background.registerTask(agentTask(new Promise(() => {}), 'existing agent'));
const host = mockSubagentHost({
spawn: vi.fn().mockResolvedValue({
agentId: 'agent-child',
profileName: 'coder',
resumed: false,
completion: new Promise<{ result: string }>(() => {}),
}),
});
const tool = new AgentTool(host, background);
const result = await executeTool(tool,
context({
prompt: 'Investigate',
description: 'Find cause',
run_in_background: true,
}),
);
expect(result).toMatchObject({
isError: true,
output: 'Too many background tasks are already running.',
});
expect(host.spawn).toHaveBeenCalledTimes(1);
});
it('rejects one of two concurrent background subagents when the task limit is reached', async () => {
const background = createBackgroundManager({ maxRunningTasks: 1 }).manager;
const host = mockSubagentHost({
spawn: vi
.fn()
.mockResolvedValueOnce({
agentId: 'agent-first',
profileName: 'coder',
resumed: false,
completion: new Promise<{ result: string }>(() => {}),
})
.mockResolvedValueOnce({
agentId: 'agent-second',
profileName: 'coder',
resumed: false,
completion: Promise.resolve({ result: 'second result' }),
}),
});
const tool = new AgentTool(host, background);
const first = executeTool(tool,
context({
prompt: 'Investigate first',
description: 'Find first',
run_in_background: true,
}),
);
const second = executeTool(tool,
context({
prompt: 'Investigate second',
description: 'Find second',
run_in_background: true,
}),
);
const results = await Promise.all([first, second]);
expect(host.spawn).toHaveBeenCalledTimes(2);
expect(results).toContainEqual(
expect.objectContaining({ output: expect.stringContaining('status: running') }),
);
expect(results).toContainEqual(
expect.objectContaining({
isError: true,
output: 'Too many background tasks are already running.',
}),
);
});
it('returns tool errors when spawning fails', async () => {
const error = new Error('missing subagent');
const { logger, entries } = captureLogs();
const host = mockSubagentHost({
spawn: vi.fn().mockRejectedValue(error),
});
const tool = agentTool(host, createBackgroundManager().manager, undefined, { log: logger });
const result = await executeTool(tool,
context({ prompt: 'Investigate', description: 'Find cause' }),
);
expect(result).toMatchObject({
isError: true,
output: 'subagent error: missing subagent',
});
expect(entries).toEqual([
{
level: 'warn',
message: 'subagent launch failed',
payload: expect.objectContaining({
toolCallId: 'call_agent',
runInBackground: false,
operation: 'spawn',
subagentType: 'coder',
error,
}),
},
]);
});
it('logs background registration failures', async () => {
const error = new Error('background unavailable');
const { logger, entries } = captureLogs();
const host = mockSubagentHost({
spawn: vi.fn().mockResolvedValue({
agentId: 'agent-child',
profileName: 'coder',
resumed: false,
completion: new Promise<{ result: string }>(() => {}),
}),
});
const background = createBackgroundManager().manager;
vi.spyOn(background, 'registerTask').mockImplementation(() => {
throw error;
});
const tool = new AgentTool(host, background, undefined, { log: logger });
const result = await executeTool(tool,
context({
prompt: 'Investigate',
description: 'Find cause',
run_in_background: true,
}),
);
expect(result).toMatchObject({
isError: true,
output: 'background unavailable',
});
expect(entries).toEqual([
{
level: 'warn',
message: 'background agent task registration failed',
payload: expect.objectContaining({
toolCallId: 'call_agent',
agentId: 'agent-child',
subagentType: 'coder',
error,
}),
},
]);
});
it('reports a deliberate user interruption when a foreground subagent is cancelled by the user', async () => {
const controller = new AbortController();
const host = mockSubagentHost({
spawn: vi.fn(
(
profileNameOrOptions: string | { readonly signal: AbortSignal },
legacyOptions?: { readonly signal: AbortSignal },
) =>
Promise.resolve({
agentId: 'agent-child',
profileName: 'coder',
resumed: false,
completion: new Promise<{ result: string }>((_resolve, reject) => {
const signal =
typeof profileNameOrOptions === 'string'
? legacyOptions!.signal
: profileNameOrOptions.signal;
const onAbort = (): void => {
reject(signal.reason);
};
if (signal.aborted) onAbort();
else signal.addEventListener('abort', onAbort, { once: true });
}),
}),
),
});
const tool = agentTool(host);
const resultPromise = executeTool(tool, {
turnId: '0',
toolCallId: 'call_agent',
args: { prompt: 'Investigate', description: 'Find cause' },
signal: controller.signal,
});
// Let spawn wire up and the tool reach `await handle.completion`.
await new Promise((resolve) => setTimeout(resolve, 0));
controller.abort(userCancellationReason());
const result = await resultPromise;
expect(result.isError).toBe(true);
expect(result.output).toContain('status: failed');
// The old message ("The subagent was stopped by the user.") is too weak —
// the model still blamed a "system limit". The new message rules that out.
expect(result.output).not.toContain('was stopped by the user');
expect(result.output).toContain('not a system error');
expect(result.output).toContain('capacity');
expect(result.output).toContain('wait for the user');
});
it('returns the spawned agent id when a foreground subagent times out', async () => {
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
try {
const host = mockSubagentHost({
spawn: vi.fn(
(
profileNameOrOptions: string | { readonly signal: AbortSignal },
legacyOptions?: { readonly signal: AbortSignal },
) =>
Promise.resolve({
agentId: 'agent-child',
profileName: 'coder',
resumed: false,
completion: new Promise<{ result: string }>((_resolve, reject) => {
const signal =
typeof profileNameOrOptions === 'string'
? legacyOptions!.signal
: profileNameOrOptions.signal;
signal.addEventListener(
'abort',
() => {
reject(signal.reason);
},
{ once: true },
);
}),
}),
),
});
const tool = agentTool(host);
const resultPromise = executeTool(tool,
context({
prompt: 'Investigate',
description: 'Find cause',
}),
);
await vi.advanceTimersByTimeAsync(30 * 60 * 1000);
const result = await resultPromise;
expect(result).toMatchObject({ isError: true });
expect(result.output).toContain('agent_id: agent-child');
expect(result.output).toContain('actual_subagent_type: coder');
expect(result.output).toContain('status: failed');
expect(result.output).toContain('subagent error: Agent timed out after 30 minutes.');
expect(result.output).toContain('resume_hint:');
expect(result.output).toContain('Agent(resume="agent-child", prompt="continue")');
expect(result.output).toContain('do not set subagent_type');
expect(result.output).toContain('retains its prior context');
} finally {
vi.useRealTimers();
}
});
});
function profile(input: {
readonly name: string;
readonly description?: string;
readonly whenToUse?: string;
readonly tools?: readonly string[];
}): ResolvedAgentProfile {
return {
name: input.name,
description: input.description,
whenToUse: input.whenToUse,
systemPrompt: () => `${input.name} prompt`,
tools: [...(input.tools ?? [])],
};
}