feat(10-02): update ClaudeAgentManager for batched answers

- Change resume() signature from (agentId, prompt) to (agentId, answers)
- Accept Record<string, string> mapping question IDs to user answers
- Format answers as structured prompt for Claude CLI
- Update AgentManager interface in types.ts
- Update manager tests for new signature
This commit is contained in:
Lukas May
2026-01-31 18:02:51 +01:00
parent 185a125307
commit d012680dbe
3 changed files with 37 additions and 12 deletions

View File

@@ -316,7 +316,7 @@ describe('ClaudeAgentManager', () => {
}); });
describe('resume', () => { describe('resume', () => {
it('resumes agent waiting for input', async () => { it('resumes agent waiting for input with answers map', async () => {
mockRepository.findById = vi.fn().mockResolvedValue({ mockRepository.findById = vi.fn().mockResolvedValue({
...mockAgent, ...mockAgent,
status: 'waiting_for_input', status: 'waiting_for_input',
@@ -335,11 +335,19 @@ describe('ClaudeAgentManager', () => {
}; };
mockExeca.mockReturnValue(mockSubprocess as unknown as ReturnType<typeof execa>); mockExeca.mockReturnValue(mockSubprocess as unknown as ReturnType<typeof execa>);
await manager.resume(mockAgent.id, 'User response'); await manager.resume(mockAgent.id, { q1: 'Answer one', q2: 'Answer two' });
expect(mockExeca).toHaveBeenCalledWith( expect(mockExeca).toHaveBeenCalledWith(
'claude', 'claude',
expect.arrayContaining(['-p', 'User response', '--resume', 'session-789', '--output-format', 'json', '--json-schema']), expect.arrayContaining([
'-p',
'Here are my answers to your questions:\n[q1]: Answer one\n[q2]: Answer two',
'--resume',
'session-789',
'--output-format',
'json',
'--json-schema',
]),
expect.any(Object) expect.any(Object)
); );
}); });
@@ -350,7 +358,7 @@ describe('ClaudeAgentManager', () => {
status: 'running', status: 'running',
}); });
await expect(manager.resume(mockAgent.id, 'Response')).rejects.toThrow( await expect(manager.resume(mockAgent.id, { q1: 'Answer' })).rejects.toThrow(
'not waiting for input' 'not waiting for input'
); );
}); });
@@ -362,7 +370,7 @@ describe('ClaudeAgentManager', () => {
sessionId: null, sessionId: null,
}); });
await expect(manager.resume(mockAgent.id, 'Response')).rejects.toThrow( await expect(manager.resume(mockAgent.id, { q1: 'Answer' })).rejects.toThrow(
'has no session to resume' 'has no session to resume'
); );
}); });
@@ -374,7 +382,7 @@ describe('ClaudeAgentManager', () => {
}); });
mockWorktreeManager.get = vi.fn().mockResolvedValue(null); mockWorktreeManager.get = vi.fn().mockResolvedValue(null);
await expect(manager.resume(mockAgent.id, 'Response')).rejects.toThrow( await expect(manager.resume(mockAgent.id, { q1: 'Answer' })).rejects.toThrow(
'Worktree' 'Worktree'
); );
}); });
@@ -398,7 +406,7 @@ describe('ClaudeAgentManager', () => {
}; };
mockExeca.mockReturnValue(mockSubprocess as unknown as ReturnType<typeof execa>); mockExeca.mockReturnValue(mockSubprocess as unknown as ReturnType<typeof execa>);
await manager.resume(mockAgent.id, 'User response'); await manager.resume(mockAgent.id, { q1: 'User answer' });
const resumedEvent = capturedEvents.find( const resumedEvent = capturedEvents.find(
(e) => e.type === 'agent:resumed' (e) => e.type === 'agent:resumed'

View File

@@ -345,8 +345,11 @@ export class ClaudeAgentManager implements AgentManager {
/** /**
* Resume an agent that's waiting for input. * Resume an agent that's waiting for input.
* Uses stored session ID to continue with full context. * Uses stored session ID to continue with full context.
*
* @param agentId - Agent to resume
* @param answers - Map of question ID to user's answer
*/ */
async resume(agentId: string, prompt: string): Promise<void> { async resume(agentId: string, answers: Record<string, string>): Promise<void> {
const agent = await this.repository.findById(agentId); const agent = await this.repository.findById(agentId);
if (!agent) { if (!agent) {
throw new Error(`Agent '${agentId}' not found`); throw new Error(`Agent '${agentId}' not found`);
@@ -368,6 +371,9 @@ export class ClaudeAgentManager implements AgentManager {
throw new Error(`Worktree '${agent.worktreeId}' not found`); throw new Error(`Worktree '${agent.worktreeId}' not found`);
} }
// Format answers map as structured prompt for Claude
const prompt = this.formatAnswersAsPrompt(answers);
await this.repository.updateStatus(agentId, 'running'); await this.repository.updateStatus(agentId, 'running');
// Start CLI with --resume flag and same JSON schema // Start CLI with --resume flag and same JSON schema
@@ -390,7 +396,7 @@ export class ClaudeAgentManager implements AgentManager {
} }
); );
// Clear any previous pending question when resuming // Clear any previous pending questions when resuming
this.activeAgents.set(agentId, { subprocess }); this.activeAgents.set(agentId, { subprocess });
if (this.eventBus) { if (this.eventBus) {
@@ -410,6 +416,17 @@ export class ClaudeAgentManager implements AgentManager {
this.handleAgentCompletion(agentId, subprocess); this.handleAgentCompletion(agentId, subprocess);
} }
/**
* Format answers map as structured prompt for Claude.
* One line per answer in format: "[id]: answer"
*/
private formatAnswersAsPrompt(answers: Record<string, string>): string {
const lines = Object.entries(answers).map(
([questionId, answer]) => `[${questionId}]: ${answer}`
);
return `Here are my answers to your questions:\n${lines.join('\n')}`;
}
/** /**
* Get the result of an agent's work. * Get the result of an agent's work.
*/ */

View File

@@ -137,14 +137,14 @@ export interface AgentManager {
/** /**
* Resume an agent that's waiting for input. * Resume an agent that's waiting for input.
* *
* Used when agent paused on AskUserQuestion and user provides response. * Used when agent paused on questions and user provides responses.
* Uses stored session ID to continue with full context. * Uses stored session ID to continue with full context.
* Agent must be in 'waiting_for_input' status. * Agent must be in 'waiting_for_input' status.
* *
* @param agentId - Agent to resume * @param agentId - Agent to resume
* @param prompt - User's response to continue the agent * @param answers - Map of question ID to user's answer
*/ */
resume(agentId: string, prompt: string): Promise<void>; resume(agentId: string, answers: Record<string, string>): Promise<void>;
/** /**
* Get the result of an agent's work. * Get the result of an agent's work.