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:
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user