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:
Lukas May
2026-01-31 18:03:00 +01:00
parent a9e46a2843
commit 2c41e52029
5 changed files with 65 additions and 19 deletions

View File

@@ -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);

View File

@@ -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!);

View File

@@ -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!);

View File

@@ -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);

View File

@@ -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 };
}),