diff --git a/packages/voice-assistant/src/server/acp/agent-manager.ts b/packages/voice-assistant/src/server/acp/agent-manager.ts index a2210fd13..89554c565 100644 --- a/packages/voice-assistant/src/server/acp/agent-manager.ts +++ b/packages/voice-assistant/src/server/acp/agent-manager.ts @@ -210,8 +210,16 @@ export class AgentManager { /** * Send a prompt to an agent + * @param agentId - Agent ID + * @param prompt - The prompt text + * @param maxWait - Optional max milliseconds to wait for completion. If not provided, returns immediately without waiting. + * @returns Object with didComplete boolean indicating if agent finished within maxWait time */ - async sendPrompt(agentId: string, prompt: string): Promise { + async sendPrompt( + agentId: string, + prompt: string, + maxWait?: number + ): Promise<{ didComplete: boolean; stopReason?: string }> { const agent = this.agents.get(agentId); if (!agent) { throw new Error(`Agent ${agentId} not found`); @@ -224,38 +232,66 @@ export class AgentManager { agent.status = "processing"; this.notifySubscribers(agentId); - try { - const response = await agent.connection.prompt({ - sessionId: agent.sessionId!, - prompt: [ - { - type: "text", - text: prompt, - }, - ], + // Start the prompt (this will eventually complete and update status) + const promptPromise = agent.connection.prompt({ + sessionId: agent.sessionId!, + prompt: [ + { + type: "text", + text: prompt, + }, + ], + }); + + // Handle completion in background + promptPromise + .then((response) => { + // Handle completion based on stopReason from the protocol + console.log(`[Agent ${agentId}] Prompt completed with stopReason: ${response.stopReason}`); + + if (response.stopReason === "end_turn") { + agent.status = "completed"; + } else if (response.stopReason === "refusal") { + agent.status = "failed"; + agent.error = "Agent refused to process the prompt"; + } else if (response.stopReason === "cancelled") { + agent.status = "ready"; + } else { + // max_tokens, max_turn_requests - still completed but may be truncated + agent.status = "completed"; + } + + this.notifyStatusChange(agentId); + }) + .catch((error) => { + this.handleAgentError( + agentId, + `Prompt failed: ${error instanceof Error ? error.message : String(error)}` + ); }); - // Handle completion based on stopReason from the protocol - console.log(`[Agent ${agentId}] Prompt completed with stopReason: ${response.stopReason}`); + // If no maxWait specified, return immediately + if (maxWait === undefined) { + console.log(`[Agent ${agentId}] Prompt sent (non-blocking, use get_agent_status to check completion)`); + return { didComplete: false }; + } - if (response.stopReason === "end_turn") { - agent.status = "completed"; - } else if (response.stopReason === "refusal") { - agent.status = "failed"; - agent.error = "Agent refused to process the prompt"; - } else if (response.stopReason === "cancelled") { - agent.status = "ready"; - } else { - // max_tokens, max_turn_requests - still completed but may be truncated - agent.status = "completed"; - } - - this.notifyStatusChange(agentId); - } catch (error) { - this.handleAgentError( - agentId, - `Prompt failed: ${error instanceof Error ? error.message : String(error)}` + // If maxWait specified, race between completion and timeout + try { + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error("timeout")), maxWait) ); + + const response = await Promise.race([promptPromise, timeoutPromise]); + + console.log(`[Agent ${agentId}] Prompt completed within ${maxWait}ms with stopReason: ${response.stopReason}`); + return { didComplete: true, stopReason: response.stopReason }; + } catch (error) { + if (error instanceof Error && error.message === "timeout") { + console.log(`[Agent ${agentId}] Prompt did not complete within ${maxWait}ms (still processing)`); + return { didComplete: false }; + } + // Real error - rethrow throw error; } } diff --git a/packages/voice-assistant/src/server/acp/mcp-server.ts b/packages/voice-assistant/src/server/acp/mcp-server.ts index cca1aebf9..659002162 100644 --- a/packages/voice-assistant/src/server/acp/mcp-server.ts +++ b/packages/voice-assistant/src/server/acp/mcp-server.ts @@ -117,7 +117,7 @@ export async function createAgentMcpServer( { title: "Send Agent Prompt", description: - "Sends a task or prompt to an existing agent. The agent will process the prompt and execute the required actions. Use this to give the agent new tasks, provide feedback, or steer its work. Only works if agent is in 'ready' or 'completed' state.", + "Sends a task or prompt to an existing agent. By default, returns immediately without waiting (non-blocking). The agent will process the prompt in the background. Use get_agent_status or get_agent_activity to check progress. Optionally, you can specify maxWait to wait up to that many milliseconds for completion before returning.", inputSchema: { agentId: z.string().describe("Agent ID returned from create_coding_agent"), prompt: z @@ -125,16 +125,26 @@ export async function createAgentMcpServer( .describe( "The task, instruction, or feedback to send to the agent. Be specific about what you want the agent to accomplish." ), + maxWait: z + .number() + .optional() + .describe( + "Optional: Maximum milliseconds to wait for agent to complete. If not provided, returns immediately (non-blocking). If agent doesn't finish within this time, returns with didComplete=false and agent continues processing in background." + ), }, outputSchema: { success: z.boolean().describe("Whether the prompt was sent successfully"), + didComplete: z.boolean().describe("Whether the agent completed within maxWait time (always false if maxWait not specified)"), + stopReason: z.string().nullable().describe("Reason agent stopped if it completed: 'end_turn', 'max_tokens', 'max_turn_requests', 'refusal', 'cancelled'"), }, }, - async ({ agentId, prompt }) => { - await agentManager.sendPrompt(agentId, prompt); + async ({ agentId, prompt, maxWait }) => { + const response = await agentManager.sendPrompt(agentId, prompt, maxWait); const result = { success: true, + didComplete: response.didComplete, + stopReason: response.stopReason ?? null, }; return {