Agents and sub-agents¶
config/fabric.md holds root, budget, llm (frontmatter; body = human docs) plus skill_dirs
(directories of SKILL.md specs resolved relative to the config dir — this is how code-free domains like
examples/notes/ are loaded; python domain loaders still apply via --domains/build_registry).
Each agent is config/agents/<name>/AGENT.md:
---
name: stats_team # == folder name
kind: supervisor
strategy: sequential
sub_agents: [statistician, methods_reviewer]
role: Statistics team
description: One line - what parent routers see when deciding to delegate here.
---
Working instructions (this body is the agent's system prompt, after an optional role/goal header).
Mention sub-agents and components in `backticks` so the lint can verify them.
Frontmatter fields (AgentSpec): kind, backend, fallback, role, goal, backstory, description,
model, temperature, max_retries, sub_agents, strategy, synthesize, max_delegations, domains,
pipeline, function, output_schema, interpret, inputs, options. instructions and source are
derived from the file and must not appear in frontmatter. An agent needs a goal, a description or a body.
Loading reports all file problems at once, located by relative path.
| kind | backends | notes |
|---|---|---|
supervisor |
fabric (router/sequential), rules (sequential, no LLM), pydantic_ai (tool delegation) |
owns sub_agents |
planner |
fabric, pydantic_ai, dspy, template, stats_rules |
plans over domains, executes, interprets |
pipeline |
fabric |
runs pipeline with options.params; inputs mapped by name, then by type |
llm |
fabric |
free text or output_schema; options.check_grounding |
function |
fabric |
registered callable (task, inputs) -> dict |
Validation before run (AgentFabric.validate): unknown kind/backend/sub-agent/pipeline/function/
schema/domain (with suggestions), self-reference, duplicates, sub-agents on leaf kinds, supervisors without
sub-agents, cycles, depth > budget.max_depth.
Router delegation plan (validated, self-corrected): {"delegations":[{"id","agent","instruction","inputs","depends_on"}]}
— agents must be sub-agents, inputs must be blackboard keys or @<delegation_id>; dependencies acyclic;
at most max_delegations. Dependents of failed delegations are skipped.
Blackboard keys: user inputs by name (data, notes); agent output at <path>; artifacts at
<path>.<name> (e.g. research_lead/profile_runner.labels). Sequential supervisors pass each child
child.inputs or parent inputs + main outputs of earlier siblings.
Budget: max_depth, max_agent_runs, max_delegations, max_llm_calls (all LLM calls go through
MeteredBackend; PydanticAI calls are charged from its usage). Once exhausted, supervisors stop delegating.
Parallel delegations: budget.max_parallel (default 1 = sequential, as before) lets a router run independent
delegations at the same time, in threads. Delegations run in waves (all dependencies done); budget is charged and
delegate events are emitted in plan order before a wave starts, results are returned in plan order, and every trace
event carries a seq giving the run a total order. Counters, trace and blackboard are lock-protected. Keep your own
function agents and backends thread-safe when you raise it.
Streaming: fabric.run(..., on_delta=fn) (and the CLI live view) receives fn(path, text) for the answer text of every
LLM call as it is generated, when the backend can stream() (LiteLLMProxyBackend, ScriptedBackend); other backends just
don't stream. The returned text, the budget and the trace are identical with or without a listener. Async callers can use
acomplete/astream on the backends (AsyncLLMBackend, StreamingLLMBackend; SyncToAsyncBackend adapts any sync one).
Trace export: the on_event stream can be persisted two ways. agent_fabric.trace.JsonlTraceSink (or
write_jsonl(report.trace, path) after the run) writes every event as one JSON line with stable field names —
seq (total order), at (seconds since run start), path (agent path), event (start/end/llm_call/
delegate/fallback/skip/error), detail — flushed per event so a crashed run keeps its trace:
{
"seq": 3,
"at": 0.41,
"path": "note_taker",
"event": "llm_call",
"detail": "local-fast"
}
The CLI does it with agent-fabric run <config> ... --trace-out run.jsonl. OtelTraceSink (CLI flag --otel,
needs pip install 'spec-agents-core[otel]') maps a run to OpenTelemetry: an agent run is a span (start opens,
end closes with an ERROR status on failure), llm_call opens a child span closed by the next event at the same
path, and delegate/fallback/skip/error become span events on the nearest ancestor span. With no
app-configured tracer provider, a console provider printing finished spans is installed so --otel alone shows
output; under opentelemetry-instrument or an app provider, spans go to its exporters.
Fallbacks: build-time and run-time; recorded in result.notes and trace (fallback event).
Memory: per agent at <session>/<path>; routers/planners receive their last runs as context.
Adding a kind/backend: @AgentFabric.builder(kind, backend, accepts_sub_agents=..., requires=(...))
returning a BaseAgent subclass implementing _run(task, ctx, path, depth). Planner backends:
planner_builder(name, make) with make(fabric, spec, metered_backend) -> Planner.