---
title: When agents build agents
description: >-
  Part two of the series: a loop is a system of agents that picks its own
  structure, builds its own tools, and outlives a single run. What that looks
  like with sub-agents, dynamic workflows, and runtime authoring.
date: '2026-07-14'
authors:
  - David Sanchez
categories:
  - Pydantic AI
  - Pydantic Logfire
canonical: 'https://pydantic.dev/articles/when-agents-build-agents'
---

> Markdown version of [When agents build agents](https://pydantic.dev/articles/when-agents-build-agents) — the canonical HTML page.
>
> By David Sanchez · 2026-07-14 · Pydantic AI, Pydantic Logfire
>
> Related: [You perfected the wrong agent](https://pydantic.dev/articles/agents-week.md) · [Observability tools agents want](https://pydantic.dev/articles/observability-tools-agents-want.md)
>
> All articles: [/articles.md](https://pydantic.dev/articles.md) · Site index: [/llms.txt](https://pydantic.dev/llms.txt)

---

The [first post of this series](https://pydantic.dev/articles/what-makes-a-good-harness) argued that a good harness is about timing: it discloses the right context before the model acts, and it steers the run once the run starts to drift. A harness arms a single run and makes it reliable. This post is about what you build when one run is not enough.

We already called the agent a loop: a model working tools toward a goal. We'll now use loop to refer to _a loop of agents_ (loopception?). A loop of agents does two things a single harnessed run cannot: it chooses its own structure, and it outlives the run.

The pieces below are experimental in the [Pydantic AI Harness](https://pydantic.dev/docs/ai/harness/overview/) today, but already clear enough to build on.

## The loop chooses its own structure

The way you point an agent at hard work today is to settle a plan with it first, then hand that plan to a fresh agent to carry out. You own the plan; the agent runs it, stops to ask when it is stuck, and goes idle at the end.

A loop is handed the goal instead of the plan: the target, the constraints it has to hold to, and enough of a north star to make its own calls. It works out the structure as it goes, what to split off, what to delegate, what to check.

The first move is delegation. [Sub-agents](https://github.com/pydantic/pydantic-ai-harness/tree/main/pydantic_ai_harness/experimental/subagents) hand a parent one tool, `delegate_task`, and a roster of specialists it can call by name. Each call is a fresh, isolated run: the child sees only the task it was handed, not the parent's conversation, and hands back its result. The parent keeps a small context and a roster of narrow experts, each carrying only the tools its job needs, so every agent in the tree sees the little it needs and nothing else.

Isolation also contains failure. A child that crashes or loops forever does not take the parent down with it. The delegate tool bounds how many times a failing sub-agent is retried (`tool_retries`), and can catch a child's crash and hand it back as a correction to work around instead of an error that aborts the run (`contain_errors`). That is part one's steering, moved down a level.

In the second move, the agent choreographs the sub-agents in code. One delegation per turn is a slow way to run ten independent checks, because the parent has to read each result before it asks for the next. So you give it a single `run_workflow` tool and let it write the coordination as a program.

[Dynamic workflows](https://github.com/pydantic/pydantic-ai-harness/tree/main/pydantic_ai_harness/experimental/dynamic_workflow) expose each specialist as an async function, composed in [Pydantic Monty](https://pydantic.dev/articles/pydantic-monty), our sandboxed Python subset. The model fans work out with `gather`, votes across the results, retries the ones that fail, and only the final value returns to its context. It is [code mode](https://pydantic.dev/docs/ai/harness/code-mode/) for agents: the same trick [the first article](https://pydantic.dev/articles/what-makes-a-good-harness#a-good-harness-discloses) used to collapse a wall of tool schemas, now collapsing a wall of delegations.

<figure class="ld" role="img" aria-label="Two ways to coordinate sub-agents. On the left, delegation: the coordinator calls one sub-agent per turn (reviewer, then reviewer, then fixer) and reads each result before making the next call. On the right, a run_workflow script: the coordinator writes one program that fans three reviewers out in parallel with gather, feeds their findings into fixer, and returns one value.">
<style>
.ld{margin:2rem 0}
.ld svg{max-width:100%;height:auto}
.ld text{fill:#092224}
.ld .hd{font-size:13px;font-weight:600}
.ld .node{fill:#ffffff;stroke:#092224;stroke-width:1.5}
.ld .coord{fill:#FBFFEA;stroke:#092224;stroke-width:2}
.ld .script{fill:#9CFFE9;stroke:#092224;stroke-width:1.5}
.ld .lbl{font-size:10.5px}
.ld .mono{font-size:9px}
.ld .sub{font-size:10px;opacity:.75}
.ld .arr{stroke:#092224;stroke-width:1.4;fill:none}
.ld .arrb{stroke:#092224;stroke-width:1.4;fill:none;stroke-dasharray:3 3}
</style>
<svg viewBox="0 0 720 300" xmlns="http://www.w3.org/2000/svg" font-family="ui-monospace, SFMono-Regular, Menlo, monospace">
<line x1="360" y1="20" x2="360" y2="280" stroke="#092224" stroke-width="1" stroke-dasharray="2 4" opacity="0.4"/>
<text class="hd" x="20" y="24">delegate: one call per turn</text>
<rect class="coord" x="120" y="36" width="120" height="26" rx="4"/>
<text class="lbl" x="180" y="53" text-anchor="middle">coordinator</text>
<path class="arr" d="M150 62 V92" marker-end="url(#a)"/>
<text class="mono" x="112" y="82" text-anchor="end">turn 1</text>
<rect class="node" x="120" y="92" width="120" height="24" rx="4"/>
<text class="lbl" x="180" y="108" text-anchor="middle">reviewer</text>
<path class="arrb" d="M210 92 V62" marker-end="url(#a)"/>
<path class="arr" d="M150 116 V146" marker-end="url(#a)"/>
<text class="mono" x="112" y="136" text-anchor="end">turn 2</text>
<rect class="node" x="120" y="146" width="120" height="24" rx="4"/>
<text class="lbl" x="180" y="162" text-anchor="middle">reviewer</text>
<path class="arrb" d="M210 146 V116" marker-end="url(#a)"/>
<path class="arr" d="M150 170 V200" marker-end="url(#a)"/>
<text class="mono" x="112" y="190" text-anchor="end">turn 3</text>
<rect class="node" x="120" y="200" width="120" height="24" rx="4"/>
<text class="lbl" x="180" y="216" text-anchor="middle">fixer</text>
<path class="arrb" d="M210 200 V170" marker-end="url(#a)"/>
<text class="sub" x="180" y="256" text-anchor="middle">the parent reads each result</text>
<text class="sub" x="180" y="270" text-anchor="middle">before it makes the next call</text>
<text class="hd" x="392" y="24">run_workflow: one script</text>
<rect class="coord" x="480" y="36" width="120" height="26" rx="4"/>
<text class="lbl" x="540" y="53" text-anchor="middle">coordinator</text>
<path class="arr" d="M540 62 V74" marker-end="url(#a)"/>
<rect class="script" x="408" y="74" width="264" height="40" rx="4"/>
<text class="mono" x="420" y="90">await gather(</text>
<text class="mono" x="428" y="102">reviewer(t), reviewer(t), reviewer(t))</text>
<path class="arr" d="M452 114 V150" marker-end="url(#a)"/>
<path class="arr" d="M540 114 V150" marker-end="url(#a)"/>
<path class="arr" d="M628 114 V150" marker-end="url(#a)"/>
<rect class="node" x="410" y="150" width="84" height="24" rx="4"/>
<text class="lbl" x="452" y="166" text-anchor="middle">reviewer</text>
<rect class="node" x="498" y="150" width="84" height="24" rx="4"/>
<text class="lbl" x="540" y="166" text-anchor="middle">reviewer</text>
<rect class="node" x="586" y="150" width="84" height="24" rx="4"/>
<text class="lbl" x="628" y="166" text-anchor="middle">reviewer</text>
<path class="arr" d="M452 174 V196" marker-end="url(#a)"/>
<path class="arr" d="M540 174 V196" marker-end="url(#a)"/>
<path class="arr" d="M628 174 V196" marker-end="url(#a)"/>
<rect class="script" x="480" y="196" width="120" height="24" rx="4"/>
<text class="lbl" x="540" y="212" text-anchor="middle">fixer(findings)</text>
<path class="arr" d="M540 220 V246" marker-end="url(#a)"/>
<text class="sub" x="540" y="270" text-anchor="middle">one value returns to the parent</text>
<defs><marker id="a" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0 0 L7 3.5 L0 7 z" fill="#092224"/></marker></defs>
</svg>
<figcaption>Delegation calls one sub-agent per turn and reads each result before the next. A workflow hands the model the coordination itself, as a script.</figcaption>
</figure>

You compose the roster; the model writes the program that runs it:

```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.dynamic_workflow import DynamicWorkflow, WorkflowAgent

reviewer = Agent('anthropic:claude-sonnet-4-5', instructions='Review the diff for one class of bug.')
fixer = Agent('anthropic:claude-opus-4-7', instructions='Apply the smallest fix for a finding.')

coordinator = Agent(
    'anthropic:claude-opus-4-7',
    instructions='Find and fix the bugs in this diff, then verify.',
    capabilities=[
        DynamicWorkflow(agents=[
            WorkflowAgent(reviewer, name='reviewer', description='Reviews a diff for one class of bug.'),
            WorkflowAgent(fixer, name='fixer', description='Applies the smallest fix for a finding.'),
        ]),
    ],
)
```

The coordinator never sees `reviewer` and `fixer` as buttons to press one at a time. It sees two functions and writes the program that coordinates them. Who writes that program falls on a spectrum: you can hand it a fixed plan, let an orchestrator agent design the roster, or let the model decompose the task entirely, which is what recursive language models do when they treat their own context as a variable to grep, partition, and recurse over.[^1]

## The loop builds its own tools

Splitting work is one half of choosing your own structure; the other is extending yourself. If you give an agent a task it has no tool for, the question worth asking is whether it can make one.

[Runtime authoring](https://github.com/pydantic/pydantic-ai-harness/tree/main/pydantic_ai_harness/experimental/authoring) gives an agent a tool that writes a new capability to disk. The agent generates the code; the harness imports it and runs its static checks before it counts as real.

To author against the actual API instead of a guessed one, a companion capability serves the current [Pydantic AI](https://pydantic.dev/docs/ai/) docs on demand, and another walks the repo the agent was pointed at, loading its `CLAUDE.md` and `AGENTS.md` files and cataloging the skills and sub-agents already defined there. The loop discovers the context a team already wrote, then adds to it.

The honest boundary, and it is the load-bearing part, is that none of this happens mid-run. Pydantic AI resolves an agent's full set of [capabilities](https://pydantic.dev/docs/ai/core-concepts/capabilities/) once, at the start of a run, and holds it fixed. A capability the agent authors on turn nine is not live on turn ten, it is live on the next run. The loop does not rewrite itself in flight: it runs, learns something, re-arms, and runs again. The unit that improves is the loop, not the turn.

That constraint buys something. A run whose tools and instructions are fixed at the start presents a stable prefix, and a stable prefix is one the provider can serve from cache. A run that rewrote its own toolset every few turns would invalidate that cache on every edit and pay full price for the whole history each time.

This is why the one in-flight change that does exist, revealing a new sub-agent to a running workflow, appends it at the tail and leaves the tool definitions untouched: new capacity, same cached prefix. Working with the provider's cache instead of against it is most of the difference between a loop that is cheap to run and one that is not.

## The loop outlives the run

The second axis is time. A harnessed run lives inside one request: it starts, works, and returns. A loop is decoupled from any single request. It can go idle and wake on an outside event, a closed pull request, a failed CI job, a teammate's reply, and pick up where it left off. It survives a restart, and carries state across a horizon far longer than any one model's context window.

Two mechanisms make that concrete, both forms of step persistence:

- **Checkpoint and continue:** a loop that is interrupted resumes instead of restarting.
- **Fork:** a loop branches at a decision, tries two approaches in parallel, and keeps the one that verifies.

Forking is the one that points forward: a loop that can split itself and keep the winner is the raw material for a loop that gets better at its own job.

Before it can do that, two things are still missing. The loop needs to see every one of its runs on one timeline, and it needs a memory of what its sub-agents did that outlives any single context.

The first exists today: every model call, tool call, delegation, and context compaction lands as a span in [Pydantic Logfire](https://pydantic.dev/logfire), so the whole agent tree reads as one trace. The second is the frontier we are working toward, making each sub-agent's history a substrate the rest of the loop can query on demand, scoped by rank, reachable and known but not loaded into every context by default. No harness ships that yet.

## Where this goes

A loop that picks its own structure, writes its own tools, and can see its own runs is one step from a loop that improves them. Sub-agents, dynamic workflows, and runtime authoring are the moving parts; the interesting question is what happens when the loop starts turning that machinery on itself, proposing a better prompt, writing an evaluator for its own output, keeping the change only if it measurably helps.

That is the last part of this series: loops that improve themselves, and where Pydantic Logfire fits when they do.

---

This is part two of three on where harnesses are heading. Next: loops that improve themselves.

To try the moving parts, add a capability or two from the [Harness capability matrix](https://github.com/pydantic/pydantic-ai-harness#capability-matrix) to an agent, give it a task big enough to split, and watch the whole tree resolve on one Logfire trace.

## Citations

[^1]: Alex Zhang and colleagues (MIT), ["Recursive Language Models"](https://alexzhang13.github.io/blog/2025/rlm/). The model, not a person, decides how to break the context down; the authors are explicit that "RLMs are not agents."
