Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions packages/agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,35 @@ prompt("Read config.json")

Tool execution mode is configurable:

- `parallel` (default): preflight tool calls sequentially, execute allowed tools concurrently, emit `tool_execution_end` as soon as each tool is finalized, then emit toolResult messages and `turn_end.toolResults` in assistant source order
- `parallel` (default): execute consecutive groups of parallel-capable calls concurrently, separated by individual sequential tool barriers
- `sequential`: execute tool calls one by one, matching the historical behavior

In parallel mode, tool completion events follow tool completion order, but persisted toolResult messages still follow assistant source order.
The global mode is set through `toolExecution` in the agent config.
In parallel mode, a tool declaring `executionMode: "sequential"` executes alone at its position in the assistant message.
Calls never move across that barrier.
Tools declaring `"parallel"` or omitting `executionMode` can overlap within their consecutive group.
Global sequential mode runs every call one by one, including tools explicitly declaring `"parallel"`.

The mode can be set globally via `toolExecution` in the agent config, or per-tool via `executionMode` on `AgentTool`. If any tool call in a batch targets a tool with `executionMode: "sequential"`, the entire batch executes sequentially regardless of the global setting.
For example, `read A, read B, update_plan, read C, read D` executes as `[read A + read B]`, then `update_plan`, then `[read C + read D]` when `update_plan` declares sequential execution.

The `beforeToolCall` hook runs after `tool_execution_start` and validated argument parsing. It can block execution and attach `terminate: true` to the blocked result. The `afterToolCall` hook runs after tool execution finishes and before `tool_execution_end` and final tool result message events are emitted.
Each parallel group preflights its calls sequentially before executing permitted calls concurrently.
The `beforeToolCall` hook runs after `tool_execution_start` and argument validation, and can block execution or attach `terminate: true` to a blocked result.
The `afterToolCall` hook runs after execution and before `tool_execution_end` and result-message events.
Completion events follow finalization order within a group; result-message events, persisted results, and `turn_end.toolResults` follow assistant source order.
Execution, result hooks, completion events, and ordered result-message events all settle before the next phase begins preflight.
Async subscribers are awaited when using `Agent` or an awaited event sink with `runAgentLoop`; raw event streams remain observational.

Tools, blocked `beforeToolCall` results, and `afterToolCall` overrides can return `terminate: true` to hint that the automatic follow-up LLM call should be skipped. The loop only stops early when every finalized tool result in that batch sets `terminate: true`. Mixed batches continue normally.
Tools, blocked `beforeToolCall` results, and `afterToolCall` overrides can return `terminate: true` to skip the automatic follow-up LLM call when every finalized result in the entire assistant batch requests termination.
A terminating phase alone does not skip later phases.
Tool errors and blocked calls also allow subsequent phases to run.
Cancellation stops scheduling later phases while preserving active-phase cancellation handling.
Steering messages and the next model request remain at the batch boundary.

The `Agent` class accepts `shouldStopAfterTurn` in `AgentOptions`. Low-level loop callers can set the same hook in `AgentLoopConfig`:
The model selects independent calls; the scheduler does not infer file dependencies or shell side effects.
Existing file-mutation queues still coordinate calls within each parallel group.

The `Agent` class accepts `shouldStopAfterTurn` in `AgentOptions`.
Low-level loop callers can set the same hook in `AgentLoopConfig`:

```typescript
const stream = agentLoop(
Expand Down Expand Up @@ -423,10 +440,10 @@ const readFileTool: AgentTool = {
parameters: Type.Object({
path: Type.String({ description: "File path" }),
}),
// Override execution mode for this tool (optional).
// "sequential" forces the entire batch to run one at a time.
// "parallel" allows concurrent execution with other tool calls.
// If omitted, the global toolExecution config applies.
// Scheduling metadata for global parallel mode (optional).
// "sequential" makes this call an exclusive barrier in source order.
// "parallel" or omitted metadata allows overlap within its group.
// Global sequential mode always runs calls one by one.
executionMode: "sequential",
execute: async (toolCallId, params, signal, onUpdate) => {
const content = await fs.readFile(params.path, "utf-8");
Expand Down Expand Up @@ -497,7 +514,7 @@ const context: AgentContext = {
const config: AgentLoopConfig = {
model: getModel("openai", "gpt-4o"),
convertToLlm: (msgs) => msgs.filter(m => ["user", "assistant", "toolResult"].includes(m.role)),
toolExecution: "parallel", // overridden by per-tool executionMode if set
toolExecution: "parallel", // sequential tools form ordered barriers
beforeToolCall: async ({ toolCall, args, context }) => undefined,
afterToolCall: async ({ toolCall, result, isError, context }) => undefined,
};
Expand Down
36 changes: 31 additions & 5 deletions packages/agent/src/agent-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -682,13 +682,39 @@ async function executeToolCalls(
emit: AgentEventSink,
): Promise<ExecutedToolCallBatch> {
const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
const hasSequentialToolCall = toolCalls.some(
(tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === "sequential",
);
if (config.toolExecution === "sequential" || hasSequentialToolCall) {
if (config.toolExecution === "sequential") {
return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit);
}
return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);

const messages: ToolResultMessage[] = [];
let terminate = true;
for (let index = 0; index < toolCalls.length; ) {
if (signal?.aborted) break;
const sequential =
currentContext.tools?.find((tool) => tool.name === toolCalls[index].name)?.executionMode === "sequential";
let end = index + 1;
if (!sequential) {
while (
end < toolCalls.length &&
currentContext.tools?.find((tool) => tool.name === toolCalls[end].name)?.executionMode !== "sequential"
) {
end++;
}
}
const executePhase = sequential ? executeToolCallsSequential : executeToolCallsParallel;
const phase = await executePhase(
currentContext,
assistantMessage,
toolCalls.slice(index, end),
config,
signal,
emit,
);
messages.push(...phase.messages);
terminate = terminate && phase.terminate;
index = end;
}
return { messages, terminate: messages.length > 0 && terminate };
}

type ExecutedToolCallBatch = {
Expand Down
27 changes: 17 additions & 10 deletions packages/agent/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ export type StreamFn = (
* Configuration for how tool calls from a single assistant message are executed.
*
* - "sequential": each tool call is prepared, executed, and finalized before the next one starts.
* - "parallel": tool calls are prepared sequentially, then allowed tools execute concurrently.
* `tool_execution_end` is emitted in tool completion order after each tool is finalized,
* while tool-result message artifacts are emitted later in assistant source order.
* - "parallel": consecutive parallel-capable calls form groups separated by individual
* tools declaring `executionMode: "sequential"`. Each group is prepared sequentially,
* then allowed tools execute concurrently. Completion events follow finalization order;
* tool-result message events follow assistant source order within each phase.
* Hooks and awaited events settle before the next phase is prepared.
*/
export type ToolExecutionMode = "sequential" | "parallel";

Expand Down Expand Up @@ -280,9 +282,9 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
/**
* Tool execution mode.
* - "sequential": execute tool calls one by one
* - "parallel": preflight tool calls sequentially, then execute allowed tools concurrently;
* emit `tool_execution_end` in tool completion order after each tool is finalized,
* then emit tool-result message artifacts later in assistant source order
* - "parallel": execute consecutive parallel-capable calls concurrently, separated by
* sequential tool barriers. Preflight each phase sequentially and await execution,
* result hooks, completion events, and source-ordered result events before the next phase.
*
* Default: "parallel"
*/
Expand All @@ -294,6 +296,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
* Return `{ block: true }` to prevent execution. The loop emits an error tool result instead.
* A blocked result can also set `terminate: true` to participate in the batch early-termination rule.
* The hook receives the agent abort signal and is responsible for honoring it.
* In parallel mode, preflight runs sequentially within the current phase, after all
* prior phases have settled. Sibling results within that phase are not yet available.
*/
beforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined>;

Expand All @@ -309,6 +313,8 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
*
* Any omitted fields keep their original values. No deep merge is performed.
* The hook receives the agent abort signal and is responsible for honoring it.
* Parallel siblings can finalize concurrently. All hooks and awaited result events
* in the current phase settle before preflight begins for the next phase.
*/
afterToolCall?: (context: AfterToolCallContext, signal?: AbortSignal) => Promise<AfterToolCallResult | undefined>;
}
Expand Down Expand Up @@ -420,11 +426,12 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
onUpdate?: AgentToolUpdateCallback<TDetails>,
) => Promise<AgentToolResult<TDetails>>;
/**
* Per-tool execution mode override.
* - "sequential": this tool must execute one at a time with other tool calls.
* - "parallel": this tool can execute concurrently with other tool calls.
* Per-tool scheduling metadata under global parallel execution.
* - "sequential": this call is an exclusive barrier in assistant source order.
* - "parallel": this call can execute concurrently within its consecutive group.
*
* If omitted, the default execution mode applies.
* Omitted metadata is parallel-capable. Global sequential execution always runs
* calls one by one, including tools explicitly declaring "parallel".
*/
executionMode?: ToolExecutionMode;
}
Expand Down
Loading
Loading