Orchestration in code or by a model
In this lesson one customer message goes through three fake agents in two arrangements. First a program runs the agents in a fixed order and passes each output to the next. Then a fake manager model decides which agent runs when, and it calls the other agents as tools. After that we name the arrangements, and we break the manager version at each boundary to see what reaches the customer.
The customer is from the small shop in
Writing the plan before taking the steps,
and the message is about the kettle from order 1042. The agents and the
manager are fake models written by hand. The runs show how each
arrangement passes work along, and they don’t show how well a real model
does it. The complete program is
site/examples/building-agents/orchestrating-agents/agent.py in the
repository. Copy the orchestrating-agents folder, and each step below is
python3 agent.py <step>.
A pipeline written in code
Section titled “A pipeline written in code”The message is “My kettle from order 1042 stopped working. Please send
the replacement to my office address this time.” The agents are
lookup, writer and checker. lookup reads the message, looks up
the order and writes a brief of one line. writer writes the reply from
the brief. checker reads the reply and approves it or rejects it. Here
are the first two, each a fake model call with text in and text out.
def lookup(message: str) -> str: """Reads the customer's message, looks up the order and writes a one-line brief.""" order_id = order_id_in(message) order = ORDERS[order_id] wish = "refund" if "money back" in message else "replacement" days = order["days_since_delivery"] return f"order {order_id}, {order['item']}, delivered {days} days ago, wants a {wish}"
def writer(brief: str) -> str: """Writes the reply to the customer from the brief.""" parts = brief.split(", ") if len(parts) == 4 and parts[0].startswith("order "): order, item, wish = parts[0], parts[1], parts[3].replace("wants a ", "") return ( f"Dear customer, we are sorry that your {item} broke. " f"A {wish} for {order} is on its way." ) return "Dear customer, we are sorry about the problem. We will get back to you soon."The order data says the kettle was delivered 12 days ago. The program
decides who runs when, and it always runs lookup, then writer, then
checker.
def pipeline(message: str) -> list: messages = [("customer", "lookup", message)] brief = lookup(message) messages.append(("lookup", "writer", brief)) draft = writer(brief) messages.append(("writer", "checker", draft)) verdict = checker(draft) messages.append(("checker", "program", verdict)) if verdict == "approved": messages.append(("program", "customer", draft)) return messagesThis is orchestration in code, which the Agent Engineer Course calls
deterministic. The flow is written down in advance, so it runs the same
way every time and you can step through it like other code. Deciding
what comes next costs no model call [1]. The messages from one
agent to the next are the handoffs. The handoffs step prints those
two, each as sender -> receiver: 'text'.
Predict the handoffs
Section titled “Predict the handoffs”In the lesson, a program runs three fake agents in a fixed order on one customer message: My kettle from order 1042 stopped working. Please send the replacement to my office address this time. The lookup agent returns a brief of the form order ID, item, delivered N days ago, wants a WISH, where the wish is refund if the message says money back and replacement otherwise. The order data says the kettle was delivered 12 days ago. The writer agent splits the brief at each comma and space. With four parts it returns: Dear customer, we are sorry that your ITEM broke. A WISH for ORDER is on its way. Here ORDER is the first part of the brief, such as order 1042. The output is one line per handoff, as sender -> receiver: and the text in single quotes, first from lookup to writer and then from writer to checker.
What are the two handoff messages? Predict the output, then run it.
python3 agent.py handoffslookup -> writer: 'order 1042, kettle, delivered 12 days ago, wants a replacement' writer -> checker: 'Dear customer, we are sorry that your kettle broke. A replacement for order 1042 is on its way.'
Output verified in CI from site/examples/building-agents/orchestrating-agents/handoffs.py.
What does lookup put in its brief, and which part of the customer's message has no place in it?
The brief has no place for the office address, so the address is lost at the first boundary. The checker approves the reply, because it sees only the draft, and the draft reads well. Here is the whole run.
The whole pipeline
Section titled “The whole pipeline”Run this, and compare what you see with the output below.
python3 agent.py pipelinecustomer -> lookup: 'My kettle from order 1042 stopped working. Please send the replacement to my office address this time.' lookup -> writer: 'order 1042, kettle, delivered 12 days ago, wants a replacement' writer -> checker: 'Dear customer, we are sorry that your kettle broke. A replacement for order 1042 is on its way.' checker -> program: 'approved' program -> customer: 'Dear customer, we are sorry that your kettle broke. A replacement for order 1042 is on its way.'
Output verified in CI from site/examples/building-agents/orchestrating-agents/pipeline.py.
A fact can be dropped at each boundary between agents. The Agent Engineer Course lists this among the weaknesses of a pipeline, because a mistake early in the line travels forward to the agents after it [2]. One agent that read the message and wrote the reply would have had the address in its context. The pipeline made three model calls and two handoffs, and it lost a fact on the way. Add a second agent when it can do something the first can’t, such as work with a focused prompt, its own small set of tools or other permissions [2]. The next lesson counts what each arrangement costs in tokens.
A manager that delegates to workers
Section titled “A manager that delegates to workers”Now a model decides who runs when. A manager model gets the customer’s
message, and it can call two workers, lookup and writer, the way an
agent calls tools. The loop around the manager runs each worker call the
manager asks for, and gives the result back to the manager. This
arrangement has no checker, and the manager’s last reply goes to the
customer. Here is the fake manager.
def manager(history: list) -> dict: last = history[-1] if last["from"] == "customer": return {"calls": [{"worker": "lookup", "task": last["text"]}]} if last["from"] == "lookup": # Like a real model, it passes on what it got, an error message included. brief = last["text"] if last["ok"] else last["error"] return {"calls": [{"worker": "writer", "task": brief}]} return {"answer": last["text"]}Each reply either asks for worker calls or gives the answer. The loop
runs a worker the way a harness runs a tool, and turns its result into a
dictionary with ok and either text or error.
for _ in range(max_turns): reply = model(history) problem = check_reply(reply) if problem: stop(problem) return if "answer" in reply or not reply.get("calls"): show("manager", "customer", reply.get("answer", "")) return for call in reply["calls"]: show("manager", call["worker"], call["task"]) result = run_worker(workers, call["worker"], call["task"]) show(call["worker"], "manager", result) history.append({"from": call["worker"], **result}) problem = check_result(call["worker"], result) if problem: stop(problem) returncheck_reply and check_result are the checks at the two boundaries,
and for now neither of them stops a run. The manager step runs the
same message.
The manager and its workers
Section titled “The manager and its workers”Run this, and compare what you see with the output below.
python3 agent.py managermanager -> lookup: 'My kettle from order 1042 stopped working. Please send the replacement to my office address this time.'
lookup -> manager: {'ok': True, 'text': 'order 1042, kettle, delivered 12 days ago, wants a replacement'}
manager -> writer: 'order 1042, kettle, delivered 12 days ago, wants a replacement'
writer -> manager: {'ok': True, 'text': 'Dear customer, we are sorry that your kettle broke. A replacement for order 1042 is on its way.'}
manager -> customer: 'Dear customer, we are sorry that your kettle broke. A replacement for order 1042 is on its way.'Output verified in CI from site/examples/building-agents/orchestrating-agents/manager.py.
The customer gets the same reply, and the address is still lost. This
time a model decided who ran when. Each line that starts with
manager -> comes from a model call that picked the next agent, and a
real manager could pick another order, skip a worker or call one twice.
So orchestration by a model can handle a task whose steps nobody wrote
down in advance. In exchange, the same input can take a different path,
which is harder to debug, and each decision is one more model call
[1].
Claude Code uses this arrangement for its subagents. A subagent that gets a task from the main conversation begins with a new context window of its own. The messages exchanged so far aren’t in it. When it finishes, the main conversation gets back a short summary instead of everything the subagent read [3]. The Academy course on subagents gives this as a reason to delegate: the main conversation holds less text and doesn’t drift from its topic [4]. It also means that a fact from the conversation that the task text doesn’t mention is out of the subagent’s reach. The task text is the handoff, as the brief was in the pipeline.
Who decides the order?
Section titled “Who decides the order?”A team handles expense claims with three agents. Every claim goes through the same three steps in the same order: read the receipt, check the amount against the policy, and write the approval note.
How should the team coordinate the three agents?
Does anything about the order of the steps change from one claim to the next?
Sequential, hierarchical and collaborative
Section titled “Sequential, hierarchical and collaborative”The pipeline and the manager are two of the common arrangements. The Agent Engineer Course describes them with two more, collaborative and competitive [2], and this lesson covers the collaborative one.
- Sequential. The agents form a pipeline, and each one works on the output of the one before it. The pipeline above is sequential.
- Hierarchical. A manager splits the task, hands the parts to worker agents and puts their results together. The manager above is hierarchical, with two workers.
- Collaborative. Peer agents share a workspace or a conversation, with none of them in charge. They build on each other’s work until they agree on a result.
The coordination gets harder in that order. The course rates it low for a pipeline, medium for a manager and its workers, and high for a group of peers, and it lists endless discussion among the risks of the last [2]. The arrangements also combine, and a step of a pipeline can itself be a manager with workers. The course advises starting with one agent, then a pipeline if the task has stages, and a system of many agents only when one agent has shown that it can’t do the task [1].
Name the arrangement
Section titled “Name the arrangement”The lesson names three arrangements of agents. In a sequential one, each agent works on the output of the one before it. In a hierarchical one, a manager hands parts of a task to workers and combines their results. In a collaborative one, peer agents share a workspace or conversation, with none in charge.
Who decides what each agent works on: a fixed order, a manager, or the peers among themselves?
A fault at each boundary
Section titled “A fault at each boundary”The manager version has two boundaries: the manager’s reply to the loop,
and a worker’s result to the manager. We break each one. At the first,
the manager’s reply asks for no worker and doesn’t answer either, which
is an empty plan. At the second, the order service times out, and the
lookup worker returns an error. Here is the empty plan.
An empty plan from the manager
Section titled “An empty plan from the manager”Run this, and compare what you see with the output below.
python3 agent.py empty_planmanager -> customer: ''
Output verified in CI from site/examples/building-agents/orchestrating-agents/empty_plan.py.
The loop takes a reply with no calls as the end of the run, and the customer gets an empty message. Here is the timeout.
A tool error from a worker
Section titled “A tool error from a worker”Run this, and compare what you see with the output below.
python3 agent.py tool_errormanager -> lookup: 'My kettle from order 1042 stopped working. Please send the replacement to my office address this time.'
lookup -> manager: {'ok': False, 'error': 'order service timed out'}
manager -> writer: 'order service timed out'
writer -> manager: {'ok': True, 'text': 'Dear customer, we are sorry about the problem. We will get back to you soon.'}
manager -> customer: 'Dear customer, we are sorry about the problem. We will get back to you soon.'Output verified in CI from site/examples/building-agents/orchestrating-agents/tool_error.py.
The manager passed the error text to the writer as if it were a brief,
and the customer got a polite reply that promises nothing. Nobody in the
shop knows that the lookup failed. Each run ends with a message to the
customer, and neither message is true to what happened. The course asks
for a check of each agent’s output before the next agent gets it, so
that a bad result doesn’t travel on [1]. The guarded run passes
stop_on_error as the check for worker results.
def stop_on_error(worker: str, result: dict) -> Optional[str]: """Stops the run when a worker returns an error.""" if not result["ok"]: return f"{worker} failed: {result['error']}" return NoneWhen a check returns a text, stop prints it after stopped: and then
prints nothing was sent to the customer. The check runs in the loop,
after the worker’s result is shown and before the manager’s next turn.
Predict the report
Section titled “Predict the report”In the lesson, a fake manager model delegates to two workers that it calls as tools: lookup, then writer. The loop prints each message as sender -> receiver: and the content, with a worker result shown as a dictionary with ok and text, or ok and error. In this run the lookup worker returns {'ok': False, 'error': 'order service timed out'}. The loop checks each worker result after it prints it and before the manager's next turn. The check returns the worker name, then failed:, then the error. On a check that returns a text, the loop prints stopped: and the text, then the line nothing was sent to the customer, and ends. The customer message is: My kettle from order 1042 stopped working. Please send the replacement to my office address this time.
The lookup worker times out again, and this time the loop checks each
worker result. What does the run print? Predict the output, then run it.
python3 agent.py guardedmanager -> lookup: 'My kettle from order 1042 stopped working. Please send the replacement to my office address this time.'
lookup -> manager: {'ok': False, 'error': 'order service timed out'}
stopped: lookup failed: order service timed out
nothing was sent to the customerOutput verified in CI from site/examples/building-agents/orchestrating-agents/guarded.py.
Which line does the loop print before the check runs, and does the manager get another turn after it?
The run stops at the boundary where the fault happened, and the report names the worker and the error. A person or a retry policy can act on that. The writer never ran, and the customer got nothing false.
The guarded loop has a check for worker results, and its check for the
manager’s reply, stop_on_empty_plan, lets every reply through. The
exercise step runs both faults with the guarded loop.
Both faults before the exercise
Section titled “Both faults before the exercise”Run this, and compare what you see with the output below.
python3 agent.py exercisefault: empty plan from the manager
manager -> customer: ''
fault: tool error from the lookup worker
manager -> lookup: 'My kettle from order 1042 stopped working. Please send the replacement to my office address this time.'
lookup -> manager: {'ok': False, 'error': 'order service timed out'}
stopped: lookup failed: order service timed out
nothing was sent to the customerOutput verified in CI from site/examples/building-agents/orchestrating-agents/exercise.py.
Exercise
In your copy of the orchestrating-agents folder, write
stop_on_empty_plan so that it returns a text when the manager’s reply
has no worker calls and no answer, and None otherwise. Run
python3 agent.py exercise. The output is the two traces, one per
fault. Writing the check yourself shows that the boundary checks belong
to the code around the model, whoever decides the order.
A good result: the empty plan trace ends with a stopped: line that
says the manager returned an empty plan, then
nothing was sent to the customer, and the tool error trace is as it
was. Compare with python3 model_answer.py in the same folder. Which other
boundary in this arrangement could pass a bad result on, and what would
its check look for? The opening comment of model_answer.py gives one
answer.
Stretch: Add a check for the writer's result as well: stop when the draft doesn't name the order the customer wrote about. Pass it as check_result in step_tool_error, run tool_error and see which boundary now catches the fault.
Recap
- In orchestration by code, a program decides which agent runs when. It runs the same way every time and costs no model call to decide [1].
- In orchestration by a model, a manager decides and calls the other agents as tools. It can handle steps nobody planned, and the same input can take another path [1].
- A fact can be lost at each handoff. Add a second agent when it can do something the first can’t [2].
- Sequential, hierarchical and collaborative arrangements get harder to coordinate in that order [2]. Check the result at each boundary in code, and stop with a report when it is wrong.
You can now
- Justifies the coordination cost of more than one agent
- Composes patterns and names the failure modes of the composition
Where was the fact lost?
Section titled “Where was the fact lost?”A program runs three agents in a fixed order on support tickets. A reader agent writes a one-line summary of the ticket, a writer agent writes the reply from the summary, and a checker reviews the reply for grammar before it goes to the customer. The checker sees only the reply. One ticket says the customer writes on behalf of their father, who owns the account. The reader's summary reads: Customer asks to change the delivery address on their account.
The reply treats the customer as the account owner. Where was the fact lost, and why didn’t the agents after it notice?
Which agent was the last one to see the whole ticket?
The nightly report
Section titled “The nightly report”A team runs a report every night with three agents: one fetches the sales numbers, one computes the totals, and one writes the summary. The steps and their order are the same every night. A developer proposes a manager model that calls the three agents as tools.
The team asks you which design to build. What do you recommend?
What would the manager decide each night that the team doesn't know already?
Which arrangement is it?
Section titled “Which arrangement is it?”Four agents write a market report. Each can read and write one shared document. Any agent may add a section or ask another agent a question in the document, and none of them assigns work to the others.
Which of the three arrangements from the lesson is this?
Is there a fixed order, a manager, or neither?
After the worker error
Section titled “After the worker error”A manager model delegates to two workers that it calls as tools, a search worker and a summary worker. The loop around the manager checks each worker result before the manager's next turn, and on an error it prints a stop line with the worker and the error and sends nothing. The search worker returns an error that the search index is unavailable.
What happens after the search worker returns the error?
Where does the check run, and who gets the next turn?
References
Section titled “References”- Addy Osmani, Ivar Soares Urdalen, Leo Simons. Orchestrators: code- versus model-driven, patterns, anti-patterns. Agent Engineer Course. Course.
AEC-18 - Addy Osmani, Ivar Soares Urdalen, Leo Simons. Multi-agent systems: architectures, roles, the orchestration tax. Agent Engineer Course. Course.
AEC-07 - Anthropic. Create custom subagents. Claude Code documentation. Reference.
Claude Code subagents - Anthropic. Introduction to subagents. Claude Academy. Course.
Academy introduction-to-subagents