Steering a running subagent¶
A background subagent can be redirected mid-flight. The parent sends a message, the subagent folds it into its next model request, and everything it has done so far survives.
from pydantic_ai import Agent
from subagents_pydantic_ai import SubAgentCapability, SubAgentConfig
agent = Agent(
"openai:gpt-4.1",
capabilities=[
SubAgentCapability(
subagents=[
SubAgentConfig(
name="searcher",
description="Searches a codebase",
instructions="You search codebases and report matches.",
),
],
)
],
)
result = await agent.run(
"Search the repo for the retry logic in the background. "
"If you learn it lives in packages/sparta, tell the searcher to narrow down."
)
The model does this with two tools:
task(description="Find the retry logic", subagent_type="searcher", mode="async")
-> Task ID: a1b2c3d4
send_message_to_subagent(task_id="a1b2c3d4", message="Narrow to packages/sparta/, it isn't in core/")
-> Message delivered to task 'a1b2c3d4'; it will be applied on the subagent's next step.
Why not cancel and re-delegate¶
Cancelling throws away every tool call the subagent already made and every conclusion it already reached, then pays for all of it again. Steering keeps the run alive: the message arrives as an extra user instruction on the next model turn, alongside the history the subagent has built up.
Reach for steering when you learn something the subagent needs:
- narrowing a search that is going too wide
- an early stop ("the first five matches are enough")
- a correction ("that module was renamed last month")
- a change of priority ("skip the tests, I only need the public API")
Steering versus answering¶
Two tools deliver text to a running subagent, and they are not interchangeable.
send_message_to_subagent |
answer_subagent |
|
|---|---|---|
| Who starts it | The parent, unprompted | The subagent, by calling ask_parent |
| Task status | running |
waiting_for_answer |
| Effect | Extra instruction on the next model request | Resolves the call the subagent is blocked on |
| If it never arrives | The subagent carries on as before | The subagent blocks until ask_timeout_seconds |
See Questions for the ask_parent side.
How delivery works¶
Steering rides pydantic-ai's own AgentRun.enqueue, so the library is not
splicing message parts by hand:
send_message_to_subagentputs the message on the internal bus, keyed by the task id.- The subagent's driving loop drains the bus between graph nodes.
- Anything pending goes to
enqueue, and core delivers it before the next model request.
Because core owns placement, a steering message can never be spliced between a tool call and its return -- a shape most providers reject outright.
Background tasks only
Steering needs a live run to deliver into. In sync mode the parent's own run
loop is blocked inside the delegation, so there is no turn in which it could
send anything; use ask_user and Questions instead. A task
that has already finished returns an error naming its status.
Driving it from Python¶
An application that orchestrates delegation itself — a UI, a supervisor loop, a
team abstraction — does not have a model calling tools. SubAgentToolset exposes
the same two operations directly:
from subagents_pydantic_ai import create_subagent_toolset
toolset = create_subagent_toolset()
# While the task is running: fold a message into its next model request.
delivered = await toolset.steer_task(task_id, "Narrow to packages/sparta/")
# While the task is blocked in `ask_parent`: resolve the question.
answered = toolset.answer_task(task_id, "Use PostgreSQL")
Both return a bool rather than raising, so a supervisor can pick whichever
applies:
if toolset.answer_task(task_id, message):
... # it was waiting for an answer
elif await toolset.steer_task(task_id, message):
... # it is running, and will see the message next step
else:
... # the task is over; read the outcome from its handle
Unlike the tools, neither performs run scoping — a caller holding a task_id
already knows which task it owns.
Isolation¶
A steering message reaches a task only if the calling run started it:
One toolset instance typically serves every run of its agent, so tasks record the
run_id that started them and the tools refuse ids belonging to another run. In a
server sharing one agent across users, run B cannot steer -- or read, answer, or
cancel -- run A's task by guessing its id.
Ordering¶
Messages are delivered in the order they were sent, and several sent before the subagent's next step arrive together on that step. Nothing is dropped while the task is running, and nothing is queued for a task that has finished.
Next steps¶
- Questions -- the subagent asking the parent
- Cancellation -- when steering is not enough
- Execution modes -- why steering needs background mode