fix(10-02): update downstream code for batched answers API
- Update resumeAgentInputSchema: prompt → answers (Record<string, string>) - Update tRPC router to pass answers map - Update CLI to accept JSON or single answer (fallback to q1 key) - Update E2E tests for new resume signature
This commit is contained in:
@@ -193,14 +193,29 @@ export function createCli(serverHandler?: (port?: number) => Promise<void>): Com
|
||||
}
|
||||
});
|
||||
|
||||
// cw agent resume <name> <response>
|
||||
// cw agent resume <name> <answers>
|
||||
// Accepts either JSON object {"q1": "answer1", "q2": "answer2"} or single answer
|
||||
agentCommand
|
||||
.command('resume <name> <response>')
|
||||
.description('Resume an agent that is waiting for input')
|
||||
.action(async (name: string, response: string) => {
|
||||
.command('resume <name> <answers>')
|
||||
.description('Resume an agent with answers (JSON object or single answer string)')
|
||||
.action(async (name: string, answersInput: string) => {
|
||||
try {
|
||||
const client = createDefaultTrpcClient();
|
||||
const result = await client.resumeAgent.mutate({ name, prompt: response });
|
||||
// Try parsing as JSON first, fallback to single answer format
|
||||
let answers: Record<string, string>;
|
||||
try {
|
||||
const parsed = JSON.parse(answersInput);
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
answers = parsed;
|
||||
} else {
|
||||
// Not a valid object, treat as single answer
|
||||
answers = { q1: answersInput };
|
||||
}
|
||||
} catch {
|
||||
// Not valid JSON, treat as single answer with default question ID
|
||||
answers = { q1: answersInput };
|
||||
}
|
||||
const result = await client.resumeAgent.mutate({ name, answers });
|
||||
console.log(`Agent '${result.name}' resumed`);
|
||||
} catch (error) {
|
||||
console.error('Failed to resume agent:', (error as Error).message);
|
||||
|
||||
@@ -203,8 +203,8 @@ describe('E2E Edge Cases', () => {
|
||||
// Clear events to check resume events
|
||||
harness.clearEvents();
|
||||
|
||||
// Resume agent with response
|
||||
await harness.agentManager.resume(dispatchResult.agentId!, 'PostgreSQL');
|
||||
// Resume agent with answers map
|
||||
await harness.agentManager.resume(dispatchResult.agentId!, { q1: 'PostgreSQL' });
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
// Verify: agent:resumed event emitted
|
||||
@@ -253,8 +253,8 @@ describe('E2E Edge Cases', () => {
|
||||
agent = await harness.agentManager.get(dispatchResult.agentId!);
|
||||
expect(agent?.status).toBe('waiting_for_input');
|
||||
|
||||
// Resume
|
||||
await harness.agentManager.resume(dispatchResult.agentId!, 'PostgreSQL');
|
||||
// Resume with answers map
|
||||
await harness.agentManager.resume(dispatchResult.agentId!, { q1: 'PostgreSQL' });
|
||||
|
||||
// After resume: running again
|
||||
agent = await harness.agentManager.get(dispatchResult.agentId!);
|
||||
|
||||
@@ -245,9 +245,9 @@ describe('E2E Recovery Scenarios', () => {
|
||||
expect(waitingPayload.taskId).toBe(taskAId);
|
||||
expect(waitingPayload.questions[0].question).toBe('Which database should I use?');
|
||||
|
||||
// Clear and resume
|
||||
// Clear and resume with answers map
|
||||
harness.clearEvents();
|
||||
await harness.agentManager.resume(dispatchResult.agentId!, 'PostgreSQL');
|
||||
await harness.agentManager.resume(dispatchResult.agentId!, { q1: 'PostgreSQL' });
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
// Verify: resumed and stopped events
|
||||
@@ -342,8 +342,8 @@ describe('E2E Recovery Scenarios', () => {
|
||||
const agent = await harness.agentManager.get(dispatchResult.agentId!);
|
||||
expect(agent?.status).toBe('waiting_for_input');
|
||||
|
||||
// Resume with specific answer
|
||||
await harness.agentManager.resume(dispatchResult.agentId!, 'PostgreSQL');
|
||||
// Resume with answers map
|
||||
await harness.agentManager.resume(dispatchResult.agentId!, { q1: 'PostgreSQL' });
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
// Verify: agent completed successfully
|
||||
@@ -393,8 +393,8 @@ describe('E2E Recovery Scenarios', () => {
|
||||
const pendingQuestions = await harness.getPendingQuestions(dispatchResult.agentId!);
|
||||
expect(pendingQuestions?.questions[0].question).toBe('API key format?');
|
||||
|
||||
// Phase 3: Resume
|
||||
await harness.agentManager.resume(dispatchResult.agentId!, 'Bearer token');
|
||||
// Phase 3: Resume with answers map
|
||||
await harness.agentManager.resume(dispatchResult.agentId!, { q1: 'Bearer token' });
|
||||
|
||||
// After resume: running again briefly
|
||||
agent = await harness.agentManager.get(dispatchResult.agentId!);
|
||||
|
||||
@@ -43,12 +43,43 @@ describe('TestHarness', () => {
|
||||
it('provides helper methods', () => {
|
||||
expect(typeof harness.seedFixture).toBe('function');
|
||||
expect(typeof harness.setAgentScenario).toBe('function');
|
||||
expect(typeof harness.setAgentQuestion).toBe('function');
|
||||
expect(typeof harness.setAgentQuestions).toBe('function');
|
||||
expect(typeof harness.getEventsByType).toBe('function');
|
||||
expect(typeof harness.clearEvents).toBe('function');
|
||||
expect(typeof harness.cleanup).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAgentQuestion convenience helper', () => {
|
||||
it('wraps single question in array format', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
// Set single question using convenience method
|
||||
harness.setAgentQuestion('test-agent', 'q1', 'Which option?', [
|
||||
{ label: 'Option A', description: 'First option' },
|
||||
{ label: 'Option B', description: 'Second option' },
|
||||
]);
|
||||
|
||||
// Spawn agent with that scenario
|
||||
const agent = await harness.agentManager.spawn({
|
||||
name: 'test-agent',
|
||||
taskId: 'task-1',
|
||||
prompt: 'test',
|
||||
});
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
// Verify questions array format
|
||||
const pending = await harness.getPendingQuestions(agent.id);
|
||||
expect(pending).not.toBeNull();
|
||||
expect(pending?.questions).toHaveLength(1);
|
||||
expect(pending?.questions[0].id).toBe('q1');
|
||||
expect(pending?.questions[0].question).toBe('Which option?');
|
||||
expect(pending?.questions[0].options).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('seedFixture', () => {
|
||||
it('creates task hierarchy from SIMPLE_FIXTURE', async () => {
|
||||
const seeded = await harness.seedFixture(SIMPLE_FIXTURE);
|
||||
|
||||
@@ -119,15 +119,15 @@ export const agentIdentifierSchema = z.object({
|
||||
export type AgentIdentifier = z.infer<typeof agentIdentifierSchema>;
|
||||
|
||||
/**
|
||||
* Schema for resuming an agent with a prompt.
|
||||
* Schema for resuming an agent with batched answers.
|
||||
*/
|
||||
export const resumeAgentInputSchema = z.object({
|
||||
/** Lookup by human-readable name (preferred) */
|
||||
name: z.string().optional(),
|
||||
/** Or lookup by ID */
|
||||
id: z.string().optional(),
|
||||
/** User response to continue the agent */
|
||||
prompt: z.string().min(1),
|
||||
/** Map of question ID to user's answer */
|
||||
answers: z.record(z.string(), z.string()),
|
||||
}).refine(data => data.name || data.id, {
|
||||
message: 'Either name or id must be provided',
|
||||
});
|
||||
@@ -378,7 +378,7 @@ export const appRouter = router({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const agentManager = requireAgentManager(ctx);
|
||||
const agent = await resolveAgent(ctx, input);
|
||||
await agentManager.resume(agent.id, input.prompt);
|
||||
await agentManager.resume(agent.id, input.answers);
|
||||
return { success: true, name: agent.name };
|
||||
}),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user