Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions packages/core/src/agents/background-agent-resume.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,185 @@ describe('BackgroundAgentResumeService', () => {
});
});

it('returns only model-visible subagent output when resumed background agents complete', async () => {
const sessionId = 'session-resume-sanitized';
const agentId = 'agent-resume-sanitized';
const metaPath = getAgentMetaPath(tempDir, sessionId, agentId);
const outputFile = getAgentJsonlPath(tempDir, sessionId, agentId);

writeAgentMeta(metaPath, {
agentId,
agentType: 'researcher',
description: 'Resume with tagged result',
parentSessionId: sessionId,
parentAgentId: null,
createdAt: '2026-04-20T00:00:00.000Z',
status: 'running',
subagentName: 'researcher',
resolvedApprovalMode: 'auto-edit',
});
fs.writeFileSync(
outputFile,
JSON.stringify({
uuid: 'u1',
parentUuid: null,
sessionId,
timestamp: '2026-04-20T00:00:00.000Z',
type: 'user',
message: {
role: 'user',
parts: [{ text: 'Resume with tagged result' }],
},
}) + '\n',
'utf8',
);

registry.register({
agentId,
description: 'Resume with tagged result',
subagentType: 'researcher',
isBackgrounded: true,
status: 'paused',
startTime: Date.now(),
abortController: new AbortController(),
prompt: 'Resume with tagged result',
outputFile,
metaPath,
});

const subagent = {
execute: vi.fn(async () => undefined),
setExternalMessageProvider: vi.fn(),
getCore: () => ({ getEventEmitter: () => new AgentEventEmitter() }),
getExecutionSummary: () => ({
rounds: 0,
totalToolCalls: 0,
successfulToolCalls: 0,
failedToolCalls: 0,
successRate: 0,
inputTokens: 0,
outputTokens: 0,
thoughtTokens: 0,
cachedTokens: 0,
totalTokens: 0,
toolUsage: [],
totalDurationMs: 0,
}),
getTerminateMode: () => AgentTerminateMode.GOAL,
getFinalText: () =>
[
'<analysis>',
'Scratchpad details should stay out of the parent context.',
'</analysis>',
'',
'<summary>',
'Resume completed successfully',
'</summary>',
].join('\n'),
};

const { service, subagentManager } = createService();
subagentManager.createAgentHeadless.mockResolvedValue({
subagent,
dispose: vi.fn().mockResolvedValue(undefined),
});

const resumed = await service.resumeBackgroundAgent(agentId, 'continue');

expect(resumed).toBeDefined();
await vi.waitFor(() => {
expect(registry.get(agentId)?.status).toBe('completed');
});
expect(registry.get(agentId)?.result).toBe(
'Resume completed successfully',
);
});

it('stores a fallback when resumed output has no model-visible text', async () => {
const sessionId = 'session-resume-empty-visible';
const agentId = 'agent-resume-empty-visible';
const metaPath = getAgentMetaPath(tempDir, sessionId, agentId);
const outputFile = getAgentJsonlPath(tempDir, sessionId, agentId);

writeAgentMeta(metaPath, {
agentId,
agentType: 'researcher',
description: 'Resume with scratchpad-only result',
parentSessionId: sessionId,
parentAgentId: null,
createdAt: '2026-04-20T00:00:00.000Z',
status: 'running',
subagentName: 'researcher',
resolvedApprovalMode: 'auto-edit',
});
fs.writeFileSync(
outputFile,
JSON.stringify({
uuid: 'u1',
parentUuid: null,
sessionId,
timestamp: '2026-04-20T00:00:00.000Z',
type: 'user',
message: {
role: 'user',
parts: [{ text: 'Resume with scratchpad-only result' }],
},
}) + '\n',
'utf8',
);

registry.register({
agentId,
description: 'Resume with scratchpad-only result',
subagentType: 'researcher',
isBackgrounded: true,
status: 'paused',
startTime: Date.now(),
abortController: new AbortController(),
prompt: 'Resume with scratchpad-only result',
outputFile,
metaPath,
});

const subagent = {
execute: vi.fn(async () => undefined),
setExternalMessageProvider: vi.fn(),
getCore: () => ({ getEventEmitter: () => new AgentEventEmitter() }),
getExecutionSummary: () => ({
rounds: 0,
totalToolCalls: 0,
successfulToolCalls: 0,
failedToolCalls: 0,
successRate: 0,
inputTokens: 0,
outputTokens: 0,
thoughtTokens: 0,
cachedTokens: 0,
totalTokens: 0,
toolUsage: [],
totalDurationMs: 0,
}),
getTerminateMode: () => AgentTerminateMode.GOAL,
getFinalText: () => '<analysis>scratch only</analysis>',
};

const { service, subagentManager } = createService();
subagentManager.createAgentHeadless.mockResolvedValue({
subagent,
dispose: vi.fn().mockResolvedValue(undefined),
});

const resumed = await service.resumeBackgroundAgent(agentId, 'continue');

expect(resumed).toBeDefined();
await vi.waitFor(() => {
expect(registry.get(agentId)?.status).toBe('completed');
});
expect(registry.get(agentId)?.result).toBe(
'(subagent produced no model-visible output)',
);
});

it('can resume into the final background concurrency slot', async () => {
registry = new BackgroundTaskRegistry({
maxConcurrentBackgroundAgents: 1,
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/agents/background-agent-resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
appendStopHookBlockingCapWarning,
formatStopHookBlockingCapWarning,
} from '../hooks/stopHookCap.js';
import { toModelVisibleSubagentResult } from './subagent-result.js';
import { runWithAgentContext } from './runtime/agent-context.js';
import { createApprovalModeOverride } from '../tools/agent/agent.js';
import type { ApprovalMode } from '../config/config.js';
Expand Down Expand Up @@ -934,8 +935,15 @@ export class BackgroundAgentResumeService {
}

const terminateMode = subagent.getTerminateMode();
const finalText = appendStopHookBlockingCapWarning(
const modelVisibleText = toModelVisibleSubagentResult(
subagent.getFinalText(),
terminateMode,
);
const finalText = appendStopHookBlockingCapWarning(
terminateMode === AgentTerminateMode.GOAL
? modelVisibleText ||
'(subagent produced no model-visible output)'
: modelVisibleText,
stopHookWarning,
);
const stats = getCompletionStats(subagent, liveToolCallCount);
Expand Down
Loading
Loading