Joiner with replan (default)

LLMCompiler diagramQuestion, Planner, Scheduler, Flights, Weather, Hotel, Joiner, Answer. Step captions follow the diagram.QuestionPlannerSchedulerFlightsWeatherHotelJoinerAnswerLLMCompiler diagramQuestion, Planner, Scheduler, Flights, Weather, Hotel, Joiner, Answer. Step captions follow the diagram.QuestionPlannerSchedulerFlightsWeatherHotelJoinerAnswer

All steps

"Plan a weekend in Lisbon"

A result is missing, so the joiner replans

Parts of this pattern
Question Input / output
The request to fulfil, such as 'Plan a weekend in Lisbon'.
Planner LLM call
Breaks the request into numbered tool calls with $1-style dependencies, and replans if the joiner asks.
Scheduler Router
Plain code, no model: runs every task whose dependencies are resolved, in parallel, filling in $N values.
Flights Tool
Flight search task for the Lisbon trip; it has no dependencies, so it starts immediately.
Weather Tool
Weather forecast task for Lisbon; independent of flights, so it runs at the same time.
Hotel Tool
Hotel booking task that waits for the arrival date from the flights task ($1) before it can run.
Joiner Aggregator
Reads all task results and returns either an answer or a replan request saying what is missing.
Answer Input / output
The finished itinerary built from the flights, weather and hotel results.
Question → Planner
The question goes to the planner, along with any context from earlier failed plans.Question: Plan a weekend in Lisbon.
Planner → Schedulertask DAG
The numbered task list, with dependencies marked as $N, goes to the scheduler.1. flights(to="Lisbon") 2. weather(city="Lisbon") 3. hotel(city="Lisbon", date=$1) end_of_plan
Scheduler → Flights
The scheduler starts the flights task right away, since it needs no other result.flights(to="Lisbon")
Scheduler → Weather
The scheduler starts the weather task in parallel with flights.weather(city="Lisbon")
Flights → Hotel$1 = date
Flights' output fills $1 in the hotel task, which releases it to run.$1 = Fri 14 Mar (arrival date)
Flights → Joiner
The flights result is collected for the joiner.Arrive Lisbon Fri 14 Mar, 10:40; return Sun 16 Mar.
Weather → Joiner
The weather result is collected for the joiner.Sat sunny 22C, Sun partly cloudy 19C.
Hotel → Joiner
The hotel result is collected for the joiner.Hotel Alfama booked, 2 nights from Fri 14 Mar.
Joiner → Answeranswer
If the results answer the question, the joiner returns an answer.{"answer": "Fly in Friday morning, stay at Hotel Alfama, and plan outdoor sights for sunny Saturday."}
Joiner → Plannerreplan
If a task failed or something is missing, the joiner sends its reason back so the planner plans only the rest.{"replan": true, "reason": "flights returned no data, so the hotel date is unknown"}

Native parallel calls

No planner, scheduler or joiner: one model asks for several independent tool calls in a single turn, and a dependent call has to wait for the next turn.

LLMCompiler: Native parallel calls diagramQuestion, Model, Flights, Weather, Hotel, Answer. Step captions follow the diagram.QuestionModelparallel callsFlightsWeatherHotelAnswerLLMCompiler: Native parallel calls diagramQuestion, Model, Flights, Weather, Hotel, Answer. Step captions follow the diagram.QuestionModelparallelcallsFlightsWeatherHotelAnswer

All steps
Parts of this pattern
Question Input / output
The request to fulfil, such as 'Plan a weekend in Lisbon'.
Model LLM call
One tool-using model: it asks for every independent call in one turn, then reads all the results in the next.
Flights Tool
Flight search, requested in the first turn because it needs no other result.
Weather Tool
Weather forecast, requested in the same first turn, so it runs alongside flights.
Hotel Tool
Hotel booking, which needs the arrival date, so the model can only request it in its second turn.
Answer Input / output
The itinerary, written once the model's reply asks for no more tool calls.
Question → Model
The question goes to the model with the tool definitions.Question: Plan a weekend in Lisbon.
Model → Flights
One of two tool calls the model returns in its first turn.{"id": "call_1", "name": "flights", "arguments": {"to": "Lisbon"}}
Model → Weather
The second call in the same turn; the runtime runs both at once.{"id": "call_2", "name": "weather", "arguments": {"city": "Lisbon"}}
Flights → Model
The flights result returns to the model as a message matched to call_1.call_1: Arrive Lisbon Fri 14 Mar, 10:40; return Sun 16 Mar.
Weather → Model
The weather result returns alongside it, matched to call_2.call_2: Sat sunny 22C, Sun partly cloudy 19C.
Model → Hotelnext turn
Only after reading the flight result can the model request the hotel, a whole turn later.{"id": "call_3", "name": "hotel", "arguments": {"city": "Lisbon", "date": "Fri 14 Mar"}}
Hotel → Model
The hotel result returns, and the model now has everything it needs.call_3: Hotel Alfama booked, 2 nights from Fri 14 Mar.
Model → Answer
A reply with no tool calls is the answer.Fly in Friday morning, stay at Hotel Alfama, and plan outdoor sights for sunny Saturday.

← → neighboursesc atlas

Planning & Search

LLMCompiler

Also known as Parallel function calling · Task-DAG planner

The planner streams a dependency graph (DAG) of tool calls; independent ones run in parallel; a joiner either answers or triggers a replan.

Joiner with replan: Introduced by Kim et al. (2023) · UC Berkeley, ICSI and others

An LLM Compiler for Parallel Function Calling (ICML 2024).

Native parallel calls: Popularized by OpenAI (2023) · OpenAI

Introduced in November 2023 alongside GPT-4 Turbo (DevDay): parallel function calling, in which a model may return several tool calls in one turn for the runtime to run together. Most model APIs with tool use now offer the same.

Measured over one run

Joiner with replan

Nodes
8. Boxes in the diagram: models, tools, stores, people and plain code.
LLM calls
1. Model invocations in the first scenario: each time a model or agent node starts thinking, plus each branch it decides.
Tool calls
3. Messages sent to a tool node in the first scenario.
Steps
6. Steps in the first scenario, with repeated steps counted every time they run.
Scenarios
2. Distinct example runs you can switch between on the diagram.
Loop
yes. Whether the flow cycles back on itself until a condition is met.
Parallel
yes. Whether two or more messages are in flight at the same time.
Branch
yes. Whether the flow takes one path out of several, chosen at run time.
Human in the loop
no. Whether a person appears as a node, to approve, correct or answer.
Autonomy
medium. How much the model decides the control flow itself, from fixed code paths (low) to open-ended (high).
Cost
medium. Relative token spend per request.
Latency
low. Relative wall-clock time per request.

Native parallel calls

Nodes
6. Boxes in the diagram: models, tools, stores, people and plain code.
LLM calls
3. Model invocations in the first scenario: each time a model or agent node starts thinking, plus each branch it decides.
Tool calls
3. Messages sent to a tool node in the first scenario.
Steps
4. Steps in the first scenario, with repeated steps counted every time they run.
Scenarios
1. Distinct example runs you can switch between on the diagram.
Loop
no. Whether the flow cycles back on itself until a condition is met.
Parallel
yes. Whether two or more messages are in flight at the same time.
Branch
no. Whether the flow takes one path out of several, chosen at run time.
Human in the loop
no. Whether a person appears as a node, to approve, correct or answer.
Autonomy
medium. How much the model decides the control flow itself, from fixed code paths (low) to open-ended (high).
Cost
medium. Relative token spend per request.
Latency
low. Relative wall-clock time per request.

Counted from the first scenario's timeline when the site is built.

Problem

Many questions need several lookups, and most of them do not depend on each other. Checking flights, weather and hotel prices for a weekend trip are three separate calls. A ReAct agent or ReWOO runs them one after another, so the wall-clock time is the sum of all tool latencies, even though most of the waiting could overlap.

How it works

LLMCompiler borrows its idea from compilers and operating systems. The planner reads the question and produces not a list but a dependency graph (a DAG): each task names the tool it calls and which earlier tasks it needs. In the diagram, flights, weather and hotel are three tasks, and the hotel task needs the arrival date from flights, written as $1 in its arguments.

The scheduler is plain code, not a model. It repeatedly fires every task whose dependencies are resolved. Here, flights and weather start together. The hotel task waits, then fires as soon as its inputs are ready, which is when flights return and its argument can be filled in. In the paper this role is split between a task-fetching unit and an executor. The diagram folds both into the scheduler.

When everything has finished, the joiner decides. If the results answer the question, it sends the answer. If something failed or turned out to be missing, it sends the planner back to produce a revised graph that accounts for what was learned. The authors also describe streaming the plan so tasks can begin before the planner has finished writing.

In the paper’s evaluations, this parallelism reduced latency substantially compared with sequential ReAct-style execution.

Many model APIs now offer a lighter version, shown in the native-parallel-calls variant: the model itself returns several tool calls in one turn and your runtime runs them together. There is no planner, scheduler or joiner, but also no dependency graph, so a call that needs an earlier result, like the hotel, has to wait a whole turn.

When to use

  • A question needs several tool calls, and at least some are independent.
  • Tool latency dominates, as with web APIs, databases or search.
  • You can describe tool inputs well enough for a planner to wire the dependencies correctly.

When to avoid

  • The next step depends on interpreting the previous result, such as “open the best match”. The graph cannot be drawn upfront. Use ReAct.
  • There is only one call, or a strict chain with no parallelism. The machinery adds nothing.
  • Your tools are rate-limited, so firing them together backfires.

Trade-offs

You trade planning difficulty for speed. The planner must get dependencies right: a missed dependency runs a task with a missing argument, and an invented one removes parallelism. The joiner adds one more model call, and replanning costs another full planning pass, but it gives you a recovery path that ReWOO lacks.

Failure modes

  • Missed dependency: a task starts before its input exists. Validate the graph and check that every $n refers to an earlier task.
  • Over-serialization: the planner chains independent tasks out of habit. Tell it explicitly that independent tasks run concurrently.
  • Replan loops: the joiner keeps asking for another pass. Cap the replans.
  • Partial failure: one task errors and poisons its dependents. Pass the error to the joiner so it can decide.

Build it with your AI

Joiner with replan

Paste this into Claude Code, Cursor, ChatGPT or any AI coding assistant.

Implement the LLMCompiler pattern in this codebase, using the LLM client and language we already use.

## Goal

Answer a question by having a planner emit a dependency graph of tool calls, running every call whose inputs are ready in parallel, and letting a joiner answer or ask for a replan.

## How it works

1. Call the planner model with the Planner prompt, passing the question, the tool descriptions and, on a replan, the joiner's reason.
2. Parse each numbered line into a task (id, tool, arguments) and collect the $N references in its arguments as its dependencies; stop parsing at end_of_plan.
3. Run a scheduler in plain code: start every task whose dependencies all have results, concurrently, with each $N replaced by task N's result.
4. As each task finishes, store its result or its error under its id and start any task that has just become ready, until every task has finished or been skipped.
5. Call the joiner model with the Joiner prompt, passing the question and every task result, and parse its JSON reply.
6. If the reply has answer, return it. If it has replan, go back to the planner with the reason and the results so far, so it plans only what is missing.

## Prompts

### Planner prompt

```markdown
Break the question into tool calls and list them as a dependency graph.
Give every task a number. If a task needs the output of an earlier task,
write its number as $1, $2 and so on in the arguments. Tasks that do not
reference each other will run at the same time, so do not add a
dependency unless it is really needed.

Tools:
{{tool_descriptions}}

Format:
1. tool_name(arguments)
2. tool_name(arguments)
3. tool_name(arguments using $1)
end_of_plan

{{#if context}}
An earlier plan did not answer the question:
{{context}}
Plan only what is still missing.
{{/if}}

Question: {{question}}
```

### Joiner prompt

```markdown
Decide whether the results below answer the question.

Question: {{question}}
Results: {{results}}

Respond in JSON, either
{"answer": "..."}
or, if something is missing or a task failed,
{"replan": true, "reason": "what is missing and why"}
```

## Stop rule

Stop when the joiner returns an answer, or after 2 replans. On the cap, return the best partial results with the joiner's last reason and a status saying the question was not fully answered.

## Requirements

- Optionally parse the planner's output as it streams, and start each task as soon as its line is complete and its dependencies are met.
- Make the replan cap (default 2), the tasks per plan (default 10) and the concurrent tool calls (default 5) parameters, and give every task a timeout (default 30 seconds).
- Reject a plan in which a $N points to a missing or later task, or the dependencies form a cycle; retry the planner once with the error, then stop with a failed status and the validation error.
- When a task errors, store the error as its result, skip every task that depends on it, and let the joiner see both.
- If the joiner's reply is not valid JSON, retry it once, then treat it as a replan with the raw reply as the reason.
- Add a test with a stub LLM whose planner returns 3 tasks where task 3 uses $1 and whose joiner returns an answer, and fake 100 ms tools; assert 1 planner call, 1 joiner call, tasks 1 and 2 overlap, and task 3 starts after task 1 ends with its result.

Native parallel calls

Paste this into Claude Code, Cursor, ChatGPT or any AI coding assistant.

Implement the LLMCompiler pattern (Native parallel calls variant) in this codebase, using the LLM client and language we already use.

## Goal

Answer a question with one tool-using model that may request several tool calls per turn, running each turn's calls concurrently and feeding all their results back in the next turn.

## How it works

1. Send the question to the model with the System prompt and the tool definitions, with multiple tool calls per turn enabled.
2. If the reply contains tool calls, run all of them concurrently, each with a timeout, and wait for every one to finish.
3. Append the model's turn and one result message per call, matched to its call id, to the conversation; send an error as that call's result instead of dropping it.
4. Call the model again with the whole conversation; it may request further calls that need the earlier results.
5. When a reply contains no tool calls, return its text as the answer.

## Prompts

### System prompt

```markdown
You can call tools. When you need several pieces of information that
do not depend on each other, request all of those tool calls in the
same turn so they run together. Wait for a result only when a later
call needs it as an argument.

When you have everything you need, answer without calling a tool.
```

## Stop rule

Stop when the model replies without tool calls, or after 5 turns. On the cap, return the last reply and the tool results gathered so far, with a status saying the turn budget ran out.

## Requirements

- Make the turn cap (default 5), the calls per turn (default 8) and the per-call timeout (default 30 seconds) parameters; beyond 8 calls, run the first 8 and return a call-limit-exceeded error as each extra call's result.
- Run one turn's calls concurrently, but never start a call from the next turn until every result of the current turn has been returned to the model.
- If a tool call's arguments are not valid JSON for its schema, return the validation error as that call's result and let the model correct it.
- Log each turn with its calls, their start and end times, and their results.
- Add a test with a stub model that requests flights and weather in turn 1, hotel in turn 2, then answers; assert 3 model calls and that flights and weather overlapped.

Prompt templates

Joiner with replan

Planner prompt
Break the question into tool calls and list them as a dependency graph.
Give every task a number. If a task needs the output of an earlier task,
write its number as $1, $2 and so on in the arguments. Tasks that do not
reference each other will run at the same time, so do not add a
dependency unless it is really needed.
Tools:
{{tool_descriptions}}
Format:
1. tool_name(arguments)
2. tool_name(arguments)
3. tool_name(arguments using $1)
end_of_plan
{{#if context}}
An earlier plan did not answer the question:
{{context}}
Plan only what is still missing.
{{/if}}
Question: {{question}}
Joiner prompt
Decide whether the results below answer the question.
Question: {{question}}
Results: {{results}}
Respond in JSON, either
{"answer": "..."}
or, if something is missing or a task failed,
{"replan": true, "reason": "what is missing and why"}

Native parallel calls

System prompt
You can call tools. When you need several pieces of information that
do not depend on each other, request all of those tool calls in the
same turn so they run together. Wait for a result only when a later
call needs it as an argument.
When you have everything you need, answer without calling a tool.

Related patterns

Composes well with:

Sources

  1. An LLM Compiler for Parallel Function Calling (Kim et al., 2023)
  2. Plan-and-Execute Agents (LangChain, 2024)

From the Agentic Atlas by Joran Kikke · Edit on GitHub · Suggest a correction