Agents
The harness exposes four agent roles, each constructed with a dedicated define_* helper that returns a frozen AgentSpec.
The harness exposes four agent roles, each constructed with a dedicated define_* helper that
returns a frozen AgentSpec.
from flowai_harness import (
define_coordinator,
define_executor,
define_planner,
define_specialist,
)
coordinator = define_coordinator(
name="scenario_coordinator",
model="claude-sonnet-4-6",
routes=["scenario_planner", "scenario_executor"],
approval={"plans": "always", "tools": "never"},
prompt="Route plan-building to the planner and execution to the executor.",
)
planner = define_planner(
name="scenario_planner",
model="claude-sonnet-4-6",
plan=scenario_plan,
prompt="You produce typed scenario plans.",
)
executor = define_executor(
name="scenario_executor",
model="claude-sonnet-4-6",
plan=scenario_plan,
tools=[search_products],
prompt="You execute approved scenario plans action by action.",
)
specialist = define_specialist(
name="product_insights",
model="claude-haiku-4-5",
tools=[search_products],
prompt="You answer focused product questions.",
)The four roles
- Coordinator — the top-level orchestrator. Receives user prompts, decides which subordinate
agent should handle each step, and surfaces approval gates. A runtime can contain at most one
coordinator and it must declare at least one
routestarget. Stateful by default. - Planner — produces structured plan instances constrained by a
PlanSpec. Stateful by default, but can be opted out withstateful=False. - Executor — consumes an approved plan and runs tools to enact each action. Carries the same
planschema as the planner so its inputs are typed. Stateless by default, but can opt in withstateful=True. - Specialist — a focused, directly-invokable helper that the runtime invokes through
Runtime.run_specialist(...). Specialists are stateless by default, can opt in withstateful=True, and do not appear in coordinator routing unless you add them explicitly.
Required and optional fields
| Helper | Required | Optional |
|---|---|---|
define_coordinator | name, model, prompt, routes | approval, tool_approvals, stateful, max_turns, tools, toolkits |
define_planner | name, model, prompt, plan | approval, tool_approvals, stateful, max_turns, tools, toolkits |
define_executor | name, model, prompt, plan | approval, tool_approvals, stateful, max_turns, tools, toolkits |
define_specialist | name, model, prompt | approval, tool_approvals, stateful, max_turns, tools, toolkits |
prompt accepts either a plain string or a LayeredPrompt returned by
layered_prompt(...). When you pass a LayeredPrompt, its deterministic cache key is attached
to the agent for the runtime's prompt cache.
Use max_turns to raise or lower one agent's LLM/tool-loop budget without changing the rest of
the runtime. Omit it to keep the interpreter default.
Role-provided tools
Role constructors provide the built-in tools required for that role to work. Passing
toolkits=[...] adds extra built-in toolkits; it does not remove the role-required tools.
| Role | Built-in tools provided by default |
|---|---|
Coordinator with routes=[...] | call_agent |
Planner with plan=... | storePlan, getPlan |
Executor with plan=... | getPlan, executePlan, resolveRef, glimpseRef |
| Specialist | none |
For example, adding catalog access to a planner keeps plan authoring available:
planner = define_planner(
name="scenario_planner",
model="claude-sonnet-4-6",
plan=scenario_plan,
toolkits=["catalog"],
prompt="Use the catalog, then store a typed scenario plan.",
)The effective prompt-visible tools are the planner tools (storePlan, getPlan) plus the
catalog toolkit tools. Planner agents do not receive executePlan by default, and executor
agents do not receive storePlan by default. Executors also receive the reference helpers by
default so they can inspect typed handles before or around plan execution when needed.
Model selection
model accepts a string, a mapping, or an explicit
ModelSpec value:
define_planner(name="scenario_planner", model="claude-sonnet-4-6", ...)
define_planner(name="scenario_planner", model={"id": "claude-sonnet-4-6", "provider": "anthropic"}, ...)Per-agent model selects the model identifier; provider transport configuration (API key
environment variables, base URLs, retry policy) is set once on the runtime via
define_runtime(..., providers=...), not per agent.
Approval policies
Approval policy is hierarchical:
- The runtime has a default floor: plans require approval, tools do not.
- A coordinator
approval={...}becomes the runtime floor unlessdefine_runtime(..., approval_policies=...)is supplied explicitly. - Any agent can pass
approval={...}to override the plan/tool defaults for that agent. - Any agent can pass
tool_approvals={tool_name: rule}to override one tool under that agent. - A Python
define_tool(..., approval=...)also becomes a tool override, scoped only to agents that bind that tool.
define_coordinator(
name="scenario_coordinator",
model="claude-sonnet-4-6",
routes=["scenario_planner", "scenario_executor"],
approval={"plans": "always", "tools": "never"},
prompt="...",
)
executor = define_executor(
name="scenario_executor",
model="claude-sonnet-4-6",
plan=scenario_plan,
approval={"plans": "never", "tools": "never"},
tool_approvals={"execute_query": "always"},
prompt="...",
)Allowed values for each channel:
"always"— every action of this kind requires explicit approval."never"— no approval gate."default"— leave the runtime default for this channel unchanged.{"kind": "dynamic", "value": "<predicate-id>"}— call a registered Python predicate.
Approval is enforced in Rust
The Python approval field is metadata only. The Rust runtime (flowai-runtime) owns the
gate: it pauses the loop, emits an approval_required event, and waits for
runtime.respond_to_approval(...) before continuing.
See also
- Plans — the schema attached to planners and executors.
- Tools — handlers wired into executors and specialists.
- Prompts — the recommended way to build the
promptargument. AgentSpecreference
Tenant
Every runtime is scoped to exactly one tenant. define_tenant(...) creates the identity serialized into RuntimeSpec; the Rust runtime (flowai-runtime) uses it as the isolation key...
Plans
A plan in the harness is a typed Pydantic schema that the planner emits and the executor consumes; when the action list is polymorphic, TaggedUnion builds a Pydantic...
