diff --git a/src/cli/index.ts b/src/cli/index.ts index ad1090c..6879b6e 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -193,14 +193,29 @@ export function createCli(serverHandler?: (port?: number) => Promise): Com } }); - // cw agent resume + // cw agent resume + // Accepts either JSON object {"q1": "answer1", "q2": "answer2"} or single answer agentCommand - .command('resume ') - .description('Resume an agent that is waiting for input') - .action(async (name: string, response: string) => { + .command('resume ') + .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; + 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); diff --git a/src/test/e2e/edge-cases.test.ts b/src/test/e2e/edge-cases.test.ts index 8a47859..f6296c7 100644 --- a/src/test/e2e/edge-cases.test.ts +++ b/src/test/e2e/edge-cases.test.ts @@ -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!); diff --git a/src/test/e2e/recovery-scenarios.test.ts b/src/test/e2e/recovery-scenarios.test.ts index a19065a..0788523 100644 --- a/src/test/e2e/recovery-scenarios.test.ts +++ b/src/test/e2e/recovery-scenarios.test.ts @@ -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!); diff --git a/src/test/harness.test.ts b/src/test/harness.test.ts index fb270af..dea67b2 100644 --- a/src/test/harness.test.ts +++ b/src/test/harness.test.ts @@ -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); diff --git a/src/trpc/router.ts b/src/trpc/router.ts index 9f9aaf0..86a9b2b 100644 --- a/src/trpc/router.ts +++ b/src/trpc/router.ts @@ -119,15 +119,15 @@ export const agentIdentifierSchema = z.object({ export type AgentIdentifier = z.infer; /** - * 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 }; }),