From d012680dbe02da5a228116d39db42df4b3a506c4 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Sat, 31 Jan 2026 18:02:51 +0100 Subject: [PATCH] feat(10-02): update ClaudeAgentManager for batched answers - Change resume() signature from (agentId, prompt) to (agentId, answers) - Accept Record 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 --- src/agent/manager.test.ts | 22 +++++++++++++++------- src/agent/manager.ts | 21 +++++++++++++++++++-- src/agent/types.ts | 6 +++--- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/agent/manager.test.ts b/src/agent/manager.test.ts index fcda62d..8b4beac 100644 --- a/src/agent/manager.test.ts +++ b/src/agent/manager.test.ts @@ -316,7 +316,7 @@ describe('ClaudeAgentManager', () => { }); describe('resume', () => { - it('resumes agent waiting for input', async () => { + it('resumes agent waiting for input with answers map', async () => { mockRepository.findById = vi.fn().mockResolvedValue({ ...mockAgent, status: 'waiting_for_input', @@ -335,11 +335,19 @@ describe('ClaudeAgentManager', () => { }; mockExeca.mockReturnValue(mockSubprocess as unknown as ReturnType); - await manager.resume(mockAgent.id, 'User response'); + await manager.resume(mockAgent.id, { q1: 'Answer one', q2: 'Answer two' }); expect(mockExeca).toHaveBeenCalledWith( '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) ); }); @@ -350,7 +358,7 @@ describe('ClaudeAgentManager', () => { 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' ); }); @@ -362,7 +370,7 @@ describe('ClaudeAgentManager', () => { 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' ); }); @@ -374,7 +382,7 @@ describe('ClaudeAgentManager', () => { }); 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' ); }); @@ -398,7 +406,7 @@ describe('ClaudeAgentManager', () => { }; mockExeca.mockReturnValue(mockSubprocess as unknown as ReturnType); - await manager.resume(mockAgent.id, 'User response'); + await manager.resume(mockAgent.id, { q1: 'User answer' }); const resumedEvent = capturedEvents.find( (e) => e.type === 'agent:resumed' diff --git a/src/agent/manager.ts b/src/agent/manager.ts index 8bebaf6..9b017e2 100644 --- a/src/agent/manager.ts +++ b/src/agent/manager.ts @@ -345,8 +345,11 @@ export class ClaudeAgentManager implements AgentManager { /** * Resume an agent that's waiting for input. * 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 { + async resume(agentId: string, answers: Record): Promise { const agent = await this.repository.findById(agentId); if (!agent) { throw new Error(`Agent '${agentId}' not found`); @@ -368,6 +371,9 @@ export class ClaudeAgentManager implements AgentManager { 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'); // 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 }); if (this.eventBus) { @@ -410,6 +416,17 @@ export class ClaudeAgentManager implements AgentManager { 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 { + 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. */ diff --git a/src/agent/types.ts b/src/agent/types.ts index f0545c2..1453cc9 100644 --- a/src/agent/types.ts +++ b/src/agent/types.ts @@ -137,14 +137,14 @@ export interface AgentManager { /** * 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. * Agent must be in 'waiting_for_input' status. * * @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; + resume(agentId: string, answers: Record): Promise; /** * Get the result of an agent's work.