diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index 2c23adc6..1b04e809 100644 --- a/packages/agent/src/agent-loop.ts +++ b/packages/agent/src/agent-loop.ts @@ -167,7 +167,6 @@ async function runLoop( // Outer loop: continues when queued follow-up messages arrive after agent would stop while (true) { let hasMoreToolCalls = true; - let steeringAfterTools: AgentMessage[] | null = null; // Inner loop: process tool calls and steering messages while (hasMoreToolCalls || pendingMessages.length > 0) { @@ -204,9 +203,7 @@ async function runLoop( const toolResults: ToolResultMessage[] = []; if (hasMoreToolCalls) { - const toolExecution = await executeToolCalls(currentContext, message, config, signal, emit); - toolResults.push(...toolExecution.toolResults); - steeringAfterTools = toolExecution.steeringMessages ?? null; + toolResults.push(...(await executeToolCalls(currentContext, message, config, signal, emit))); for (const result of toolResults) { currentContext.messages.push(result); @@ -216,13 +213,7 @@ async function runLoop( await emit({ type: "turn_end", message, toolResults }); - // Get steering messages after turn completes - if (steeringAfterTools && steeringAfterTools.length > 0) { - pendingMessages = steeringAfterTools; - steeringAfterTools = null; - } else { - pendingMessages = (await config.getSteeringMessages?.()) || []; - } + pendingMessages = (await config.getSteeringMessages?.()) || []; } // Agent would stop here. Check for follow-up messages. @@ -348,7 +339,7 @@ async function executeToolCalls( config: AgentLoopConfig, signal: AbortSignal | undefined, emit: AgentEventSink, -): Promise<{ toolResults: ToolResultMessage[]; steeringMessages?: AgentMessage[] }> { +): Promise { const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall"); if (config.toolExecution === "sequential") { return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit); @@ -363,12 +354,10 @@ async function executeToolCallsSequential( config: AgentLoopConfig, signal: AbortSignal | undefined, emit: AgentEventSink, -): Promise<{ toolResults: ToolResultMessage[]; steeringMessages?: AgentMessage[] }> { +): Promise { const results: ToolResultMessage[] = []; - let steeringMessages: AgentMessage[] | undefined; - for (let index = 0; index < toolCalls.length; index++) { - const toolCall = toolCalls[index]; + for (const toolCall of toolCalls) { await emit({ type: "tool_execution_start", toolCallId: toolCall.id, @@ -393,21 +382,9 @@ async function executeToolCallsSequential( ), ); } - - if (config.getSteeringMessages) { - const steering = await config.getSteeringMessages(); - if (steering.length > 0) { - steeringMessages = steering; - const remainingCalls = toolCalls.slice(index + 1); - for (const skipped of remainingCalls) { - results.push(await skipToolCall(skipped, emit)); - } - break; - } - } } - return { toolResults: results, steeringMessages }; + return results; } async function executeToolCallsParallel( @@ -417,13 +394,11 @@ async function executeToolCallsParallel( config: AgentLoopConfig, signal: AbortSignal | undefined, emit: AgentEventSink, -): Promise<{ toolResults: ToolResultMessage[]; steeringMessages?: AgentMessage[] }> { +): Promise { const results: ToolResultMessage[] = []; const runnableCalls: PreparedToolCall[] = []; - let steeringMessages: AgentMessage[] | undefined; - for (let index = 0; index < toolCalls.length; index++) { - const toolCall = toolCalls[index]; + for (const toolCall of toolCalls) { await emit({ type: "tool_execution_start", toolCallId: toolCall.id, @@ -437,21 +412,6 @@ async function executeToolCallsParallel( } else { runnableCalls.push(preparation); } - - if (config.getSteeringMessages) { - const steering = await config.getSteeringMessages(); - if (steering.length > 0) { - steeringMessages = steering; - for (const runnable of runnableCalls) { - results.push(await skipToolCall(runnable.toolCall, emit, { emitStart: false })); - } - const remainingCalls = toolCalls.slice(index + 1); - for (const skipped of remainingCalls) { - results.push(await skipToolCall(skipped, emit)); - } - return { toolResults: results, steeringMessages }; - } - } } const runningCalls = runnableCalls.map((prepared) => ({ @@ -474,14 +434,7 @@ async function executeToolCallsParallel( ); } - if (!steeringMessages && config.getSteeringMessages) { - const steering = await config.getSteeringMessages(); - if (steering.length > 0) { - steeringMessages = steering; - } - } - - return { toolResults: results, steeringMessages }; + return results; } type PreparedToolCall = { @@ -661,22 +614,3 @@ async function emitToolCallOutcome( await emit({ type: "message_end", message: toolResultMessage }); return toolResultMessage; } - -async function skipToolCall( - toolCall: AgentToolCall, - emit: AgentEventSink, - options?: { emitStart?: boolean }, -): Promise { - const result = createErrorToolResult("Skipped due to queued user message."); - - if (options?.emitStart !== false) { - await emit({ - type: "tool_execution_start", - toolCallId: toolCall.id, - toolName: toolCall.name, - args: toolCall.arguments, - }); - } - - return await emitToolCallOutcome(toolCall, result, true, emit); -} diff --git a/packages/agent/test/agent-loop.test.ts b/packages/agent/test/agent-loop.test.ts index a16708d1..5de15250 100644 --- a/packages/agent/test/agent-loop.test.ts +++ b/packages/agent/test/agent-loop.test.ts @@ -388,7 +388,7 @@ describe("agentLoop with AgentMessage", () => { expect(toolResultIds).toEqual(["tool-1", "tool-2"]); }); - it("should inject queued messages and skip remaining tool calls", async () => { + it("should inject queued messages after all tool calls complete", async () => { const toolSchema = Type.Object({ value: Type.String() }); const executed: string[] = []; const tool: AgentTool = { @@ -423,8 +423,8 @@ describe("agentLoop with AgentMessage", () => { convertToLlm: identityConverter, toolExecution: "sequential", getSteeringMessages: async () => { - // Return steering message after first tool executes - if (executed.length === 1 && !queuedDelivered) { + // Return steering message after tool execution has started. + if (executed.length >= 1 && !queuedDelivered) { queuedDelivered = true; return [queuedUserMessage]; } @@ -467,29 +467,28 @@ describe("agentLoop with AgentMessage", () => { events.push(event); } - // Only first tool should have executed - expect(executed).toEqual(["first"]); + // Both tools should execute before steering is injected + expect(executed).toEqual(["first", "second"]); - // Second tool should be skipped const toolEnds = events.filter( (e): e is Extract => e.type === "tool_execution_end", ); expect(toolEnds.length).toBe(2); expect(toolEnds[0].isError).toBe(false); - expect(toolEnds[1].isError).toBe(true); - if (toolEnds[1].result.content[0]?.type === "text") { - expect(toolEnds[1].result.content[0].text).toContain("Skipped due to queued user message"); - } + expect(toolEnds[1].isError).toBe(false); - // Queued message should appear in events - const queuedMessageEvent = events.find( - (e) => - e.type === "message_start" && - e.message.role === "user" && - typeof e.message.content === "string" && - e.message.content === "interrupt", - ); - expect(queuedMessageEvent).toBeDefined(); + // Queued message should appear in events after both tool result messages + const eventSequence = events.flatMap((event) => { + if (event.type !== "message_start") return []; + if (event.message.role === "toolResult") return [`tool:${event.message.toolCallId}`]; + if (event.message.role === "user" && typeof event.message.content === "string") { + return [event.message.content]; + } + return []; + }); + expect(eventSequence).toContain("interrupt"); + expect(eventSequence.indexOf("tool:tool-1")).toBeLessThan(eventSequence.indexOf("interrupt")); + expect(eventSequence.indexOf("tool:tool-2")).toBeLessThan(eventSequence.indexOf("interrupt")); // Interrupt message should be in context when second LLM call is made expect(sawInterruptInContext).toBe(true);