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', () => {
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<typeof execa>);
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<typeof execa>);
await manager.resume(mockAgent.id, 'User response');
await manager.resume(mockAgent.id, { q1: 'User answer' });
const resumedEvent = capturedEvents.find(
(e) => e.type === 'agent:resumed'

View File

@@ -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<void> {
async resume(agentId: string, answers: Record<string, string>): Promise<void> {
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, 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.
*/

View File

@@ -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<void>;
resume(agentId: string, answers: Record<string, string>): Promise<void>;
/**
* Get the result of an agent's work.