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', () => {
|
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'
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user