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
|
agentCommand
|
||||||
.command('resume <name> <response>')
|
.command('resume <name> <answers>')
|
||||||
.description('Resume an agent that is waiting for input')
|
.description('Resume an agent with answers (JSON object or single answer string)')
|
||||||
.action(async (name: string, response: string) => {
|
.action(async (name: string, answersInput: string) => {
|
||||||
try {
|
try {
|
||||||
const client = createDefaultTrpcClient();
|
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`);
|
console.log(`Agent '${result.name}' resumed`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to resume agent:', (error as Error).message);
|
console.error('Failed to resume agent:', (error as Error).message);
|
||||||
|
|||||||
@@ -203,8 +203,8 @@ describe('E2E Edge Cases', () => {
|
|||||||
// Clear events to check resume events
|
// Clear events to check resume events
|
||||||
harness.clearEvents();
|
harness.clearEvents();
|
||||||
|
|
||||||
// Resume agent with response
|
// Resume agent with answers map
|
||||||
await harness.agentManager.resume(dispatchResult.agentId!, 'PostgreSQL');
|
await harness.agentManager.resume(dispatchResult.agentId!, { q1: 'PostgreSQL' });
|
||||||
await vi.runAllTimersAsync();
|
await vi.runAllTimersAsync();
|
||||||
|
|
||||||
// Verify: agent:resumed event emitted
|
// Verify: agent:resumed event emitted
|
||||||
@@ -253,8 +253,8 @@ describe('E2E Edge Cases', () => {
|
|||||||
agent = await harness.agentManager.get(dispatchResult.agentId!);
|
agent = await harness.agentManager.get(dispatchResult.agentId!);
|
||||||
expect(agent?.status).toBe('waiting_for_input');
|
expect(agent?.status).toBe('waiting_for_input');
|
||||||
|
|
||||||
// Resume
|
// Resume with answers map
|
||||||
await harness.agentManager.resume(dispatchResult.agentId!, 'PostgreSQL');
|
await harness.agentManager.resume(dispatchResult.agentId!, { q1: 'PostgreSQL' });
|
||||||
|
|
||||||
// After resume: running again
|
// After resume: running again
|
||||||
agent = await harness.agentManager.get(dispatchResult.agentId!);
|
agent = await harness.agentManager.get(dispatchResult.agentId!);
|
||||||
|
|||||||
@@ -245,9 +245,9 @@ describe('E2E Recovery Scenarios', () => {
|
|||||||
expect(waitingPayload.taskId).toBe(taskAId);
|
expect(waitingPayload.taskId).toBe(taskAId);
|
||||||
expect(waitingPayload.questions[0].question).toBe('Which database should I use?');
|
expect(waitingPayload.questions[0].question).toBe('Which database should I use?');
|
||||||
|
|
||||||
// Clear and resume
|
// Clear and resume with answers map
|
||||||
harness.clearEvents();
|
harness.clearEvents();
|
||||||
await harness.agentManager.resume(dispatchResult.agentId!, 'PostgreSQL');
|
await harness.agentManager.resume(dispatchResult.agentId!, { q1: 'PostgreSQL' });
|
||||||
await vi.runAllTimersAsync();
|
await vi.runAllTimersAsync();
|
||||||
|
|
||||||
// Verify: resumed and stopped events
|
// Verify: resumed and stopped events
|
||||||
@@ -342,8 +342,8 @@ describe('E2E Recovery Scenarios', () => {
|
|||||||
const agent = await harness.agentManager.get(dispatchResult.agentId!);
|
const agent = await harness.agentManager.get(dispatchResult.agentId!);
|
||||||
expect(agent?.status).toBe('waiting_for_input');
|
expect(agent?.status).toBe('waiting_for_input');
|
||||||
|
|
||||||
// Resume with specific answer
|
// Resume with answers map
|
||||||
await harness.agentManager.resume(dispatchResult.agentId!, 'PostgreSQL');
|
await harness.agentManager.resume(dispatchResult.agentId!, { q1: 'PostgreSQL' });
|
||||||
await vi.runAllTimersAsync();
|
await vi.runAllTimersAsync();
|
||||||
|
|
||||||
// Verify: agent completed successfully
|
// Verify: agent completed successfully
|
||||||
@@ -393,8 +393,8 @@ describe('E2E Recovery Scenarios', () => {
|
|||||||
const pendingQuestions = await harness.getPendingQuestions(dispatchResult.agentId!);
|
const pendingQuestions = await harness.getPendingQuestions(dispatchResult.agentId!);
|
||||||
expect(pendingQuestions?.questions[0].question).toBe('API key format?');
|
expect(pendingQuestions?.questions[0].question).toBe('API key format?');
|
||||||
|
|
||||||
// Phase 3: Resume
|
// Phase 3: Resume with answers map
|
||||||
await harness.agentManager.resume(dispatchResult.agentId!, 'Bearer token');
|
await harness.agentManager.resume(dispatchResult.agentId!, { q1: 'Bearer token' });
|
||||||
|
|
||||||
// After resume: running again briefly
|
// After resume: running again briefly
|
||||||
agent = await harness.agentManager.get(dispatchResult.agentId!);
|
agent = await harness.agentManager.get(dispatchResult.agentId!);
|
||||||
|
|||||||
@@ -43,12 +43,43 @@ describe('TestHarness', () => {
|
|||||||
it('provides helper methods', () => {
|
it('provides helper methods', () => {
|
||||||
expect(typeof harness.seedFixture).toBe('function');
|
expect(typeof harness.seedFixture).toBe('function');
|
||||||
expect(typeof harness.setAgentScenario).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.getEventsByType).toBe('function');
|
||||||
expect(typeof harness.clearEvents).toBe('function');
|
expect(typeof harness.clearEvents).toBe('function');
|
||||||
expect(typeof harness.cleanup).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', () => {
|
describe('seedFixture', () => {
|
||||||
it('creates task hierarchy from SIMPLE_FIXTURE', async () => {
|
it('creates task hierarchy from SIMPLE_FIXTURE', async () => {
|
||||||
const seeded = await harness.seedFixture(SIMPLE_FIXTURE);
|
const seeded = await harness.seedFixture(SIMPLE_FIXTURE);
|
||||||
|
|||||||
@@ -119,15 +119,15 @@ export const agentIdentifierSchema = z.object({
|
|||||||
export type AgentIdentifier = z.infer<typeof agentIdentifierSchema>;
|
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({
|
export const resumeAgentInputSchema = z.object({
|
||||||
/** Lookup by human-readable name (preferred) */
|
/** Lookup by human-readable name (preferred) */
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
/** Or lookup by ID */
|
/** Or lookup by ID */
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
/** User response to continue the agent */
|
/** Map of question ID to user's answer */
|
||||||
prompt: z.string().min(1),
|
answers: z.record(z.string(), z.string()),
|
||||||
}).refine(data => data.name || data.id, {
|
}).refine(data => data.name || data.id, {
|
||||||
message: 'Either name or id must be provided',
|
message: 'Either name or id must be provided',
|
||||||
});
|
});
|
||||||
@@ -378,7 +378,7 @@ export const appRouter = router({
|
|||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const agentManager = requireAgentManager(ctx);
|
const agentManager = requireAgentManager(ctx);
|
||||||
const agent = await resolveAgent(ctx, input);
|
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 };
|
return { success: true, name: agent.name };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user