Parallel Agent Orchestration
Introduction & Context
Welcome back! In the previous lessons, you built a concurrent Ruby agent system that can:
- Run multiple conversations at the same time.
- Execute multiple tool calls concurrently inside a single agent turn.
Now we will combine those ideas into a higher-level architecture: parallel agent orchestration.
In this pattern, one orchestrator agent receives a complex user request, breaks it into independent subtasks, delegates those subtasks to specialized agents, and then synthesizes the results. The delegated agents are wrapped as normal OpenAI function tools, so the orchestrator can call them just like any other tool.
The key principle for this unit is:
Agent tools should return final, JSON-serializable values. The
Agent#runmethod owns concurrency by running same-turn tool calls in Ruby threads.
That means a wrapped agent tool should call the delegated agent, wait for its final response, and return that response. It should not return raw concurrency primitives like Thread objects.
Understanding Agent Orchestration
Agent orchestration is the pattern of using one coordinator agent to manage work across one or more specialized agents.
Think of it like a project manager. If a user asks for a report comparing the technology and manufacturing industries, the project manager does not need to research everything personally. Instead, it can split the work:
- Ask a researcher agent to investigate technology.
- Ask a researcher agent to investigate manufacturing.
- Compare the two returned summaries.
- Produce a final synthesized report.
Each delegated task is independent, so the model may emit multiple delegated tool calls in one response turn. If that happens, the Agent#run method executes those tool calls concurrently using Ruby threads.
This is the same tool-level parallelism you learned in Unit 2, but now the tools are more powerful: calling one tool can trigger a complete agent run.
Building the Researcher Agent
First, we create a specialized researcher agent. It has one job: use a search tool to gather information, then summarize what it found.
The sleep(1) simulates slow network I/O. This makes concurrency easier to observe when multiple searches happen close together.
Next, we define the search tool schema:
Then we create the researcher:
The researcher does not need to know anything about orchestration. It only needs to be good at its focused role: searching and summarizing.
Wrapping Agents as Synchronous Tools
To let another agent call the researcher, we wrap it as a function tool using create_agent_tool.
This helper returns two things:
tool_function— the Ruby callable that runs the delegated agent.tool_schema— the OpenAI function tool schema the orchestrator can see.
The tool function is intentionally synchronous:
It blocks until the delegated agent finishes, then returns the final response text.
That may sound like it prevents concurrency, but it does not. The outer Agent#run method executes multiple tool calls in separate Ruby threads. Each tool should block until its own final value is ready, while the framework handles running multiple tools at once.
The Tool Contract: Return Final Values, Not Threads
A tool should return a final result that can be serialized into the function_call_output message:
Good tool return values include:
- Strings
- Numbers
- Booleans
- Arrays
- Hashes
- Other JSON-serializable data
A tool should not return raw concurrency primitives such as:
ThreadQueueMutex- Sockets
- File handles
For this course, the tool contract is simple:
A tool may perform slow work internally, but it should return the final value, not the object used to perform the work.
This keeps responsibilities clear:
- The tool does the work and returns the final result.
- The agent framework runs multiple tools concurrently when the model emits multiple tool calls in the same turn.
Creating the Orchestrator Agent
Now we can wrap the researcher and give that wrapper to a manager agent:
The returned schema has a generated name based on the agent:
Now we create the manager:
The manager has exactly one tool: researcher_tool.
From the model's perspective, this looks like a normal function. From Ruby's perspective, that function starts a full researcher.run(...) call and returns the researcher's final answer.
Conditional Parallel Delegation
Parallel delegation depends on the model emitting multiple tool calls in the same turn.
If the model emits one tool call, Agent#run executes one tool call.
If the model emits multiple tool calls in the same response turn, Agent#run executes them concurrently:
This means the orchestrator prompt can encourage same-turn parallel delegation:
But the runtime behavior is:
If multiple
researcher_toolcalls appear in one turn, they run concurrently.
This distinction is important because the model decides which tool calls to emit. The framework controls how same-turn tool calls are executed.
Running the Orchestrator
We send the manager a multi-faceted question:
The initial message uses the same structured text_message helper used throughout the course:
The manager may break the request into independent research tasks. If it emits both delegated calls in the same turn, each call to researcher_tool runs in its own Ruby thread.
Observing Parallel Agent Delegation
When the manager emits multiple delegated calls in the same turn, you may see output like this:
The two delegation messages appear close together because both researcher_tool calls are being executed by the outer agent's tool threads.
Each delegated researcher may then call its own tools:
This demonstrates nested concurrency:
- The manager can run multiple delegated agent tools concurrently.
- Each delegated researcher can run multiple search tools concurrently if the model emits same-turn search calls.
The output order may vary between runs because Ruby thread scheduling and model tool-call choices can vary.
Anti-Pattern: Fire-and-Forget Thread-Returning Tools
A tempting mistake is to make the agent tool itself return a Thread:
This starts background work, but it breaks the tool contract.
The outer Agent#run already runs tool calls in threads. If the tool returns a raw Thread, the outer tool runner treats that Thread object as the tool result. It does not automatically know that the thread's future value is the real result.
So instead of sending the delegated agent's response back to the model, the agent may serialize the thread object itself:
The correct pattern is to keep the tool synchronous:
Let Agent#run handle parallel execution across multiple tool calls. Do not leak raw Thread objects into tool results.
Summary
You have learned how to build a parallel agent orchestration system in Ruby. The core pattern is:
- Create specialized agents for focused work.
- Wrap those agents as synchronous tools using
create_agent_tool. - Give those tools to an orchestrator agent.
- Encourage the model to emit multiple tool calls in the same turn for independent subtasks.
- Let
Agent#runexecute same-turn tool calls concurrently using Ruby threads. - Keep tool return values final and JSON-serializable.
This architecture gives you modularity and concurrency at the same time. Specialized agents stay focused, the orchestrator handles coordination, and the framework handles parallel execution.
