refactor: make sendPrompt non-blocking by default with optional maxWait

Change sendPrompt to return immediately by default instead of blocking, allowing the orchestrator to continue while agents process in the background. Add optional maxWait parameter for cases where waiting is needed.

Changes:
- sendPrompt now returns { didComplete, stopReason? } immediately if no maxWait
- Background promise handles completion and updates agent status via protocol
- Optional maxWait parameter races completion vs timeout, returns didComplete status
- Update send_agent_prompt MCP tool to expose maxWait parameter
- Agent continues processing in background even if maxWait expires
- Orchestrator can poll get_agent_status or get_agent_activity to check progress

This prevents orchestrator from blocking on long-running agent tasks while maintaining protocol-based completion tracking.

🤖 Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
Mohamed Boudra
2025-10-21 16:54:47 +02:00
co-authored by Claude Happy
parent cba8780bca
commit 5b53f4a7ce
2 changed files with 78 additions and 32 deletions
@@ -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<void> {
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<never>((_, 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;
}
}
@@ -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 {