Harness
A bare Agent is just a model loop. The harness is the set of opt-in primitives you compose onto it to give it richer capabilities — context assembly, persistent knowledge, sub-task spawning, and the supporting middleware they wire in.
This page is the configuration reference for those primitives. For the conversational entry point (agent.ask(), tools, HITL, observing events), see Agent Communication.
Constructor#
The loop-related parameters (config, tools, middleware, observers, prompt, …) are covered in Agent Communication and the parameter-specific guides. The harness hooks are assembly=, knowledge=, and tasks=, each documented below.
assembly= — context policies#
A list of AssemblyPolicy instances. When non-empty, the Agent wires an internal AssemblerMiddleware at the outermost position of the middleware chain so your policies transform (prompts, events) before every LLM call.
Order matters — see the ordering rule in the assembly doc. AssemblerMiddleware.validate_order() will flag known problematic compositions.
knowledge= — KnowledgeConfig#
Groups everything that involves the KnowledgeStore: the store itself, optional bootstrap, and optional compaction + aggregation strategies.
| Field | What it does |
|---|---|
store | Registered in context.dependencies[KnowledgeStore] so policies like WorkingMemoryPolicy / EpisodicMemoryPolicy can read it; also given to EventLogWriter for stream persistence. |
compact / compact_trigger | Wires a compaction middleware that fires compact() between turns when the trigger thresholds are exceeded. |
aggregate / aggregate_trigger | Wires an aggregation middleware that fires aggregate() on the configured cadence. |
bootstrap | Runs once on first use to seed the store (e.g. DefaultBootstrap writes SKILL.md files). |
The compaction and aggregation middleware are opt-in per field: passing compact= without compact_trigger= still works (a default CompactTrigger() with all thresholds disabled is used). Omit a strategy entirely and the corresponding middleware is not wired.
tasks= — TaskConfig#
Every Agent (unless constructed with tasks=False) automatically carries a pair of sub-task tools — run_subtask and run_subtasks — that let the LLM spawn isolated child Agents to handle self-contained work. TaskConfig configures how those children are built.
| Field | What it does |
|---|---|
config | The ModelConfig used for sub-task Agents. Falls back to the parent Agent's config. |
prompt | Default system prompt for sub-task Agents. |
include_tools | Allowlist of parent-tool names to inherit. None means "inherit all". |
exclude_tools | Blocklist of parent-tool names to drop. Applied after include_tools. |
extra_tools | Additional tools given to sub-tasks that the parent does not have. |
By default a sub-task Agent inherits all of the parent's user-supplied tools. Sub-tasks are constructed with tasks=False, so they themselves have no run_subtask / run_subtasks tools — recursive delegation is structurally impossible and no depth limit is needed.
tasks=False — opt out#
Pass tasks=False to suppress the auto-injected sub-task tools entirely. Use this when an Agent should never spawn children — for example, when it's already running as a sub-task itself, or when delegation isn't appropriate for its role.
run_subtask / run_subtasks — auto-injected tools#
Unless you set tasks=False, every Agent exposes two tools to the LLM:
run_subtask(task: str)— spawn one sub-task Agent. Useful when the LLM has a single self-contained piece of work to delegate.run_subtasks(tasks: list[str], parallel: bool = True)— spawn multiple sub-tasks in one tool call. Defaults to running them concurrently withasyncio.gather; passparallel=Falseonly when later tasks depend on earlier results.
The LLM is told (via the tool descriptions) that it can call run_subtask multiple times in parallel within a single response, and that run_subtasks is the deliberate fan-out form. Each child gets a fresh MemoryStream and the parent's tools (filtered by TaskConfig).
For a more explicit, named delegate where the parent LLM sees a tool like task_researcher instead of generic run_subtask, use Agent.as_tool(). The two patterns can coexist: a coordinator can have both auto-injected sub-tasks and a named task_researcher tool.
See Task Delegation for the full sub-task delegation guide — context flow, custom streams, and the depth_limiter middleware for self-delegation via as_tool().
Agent.as_tool()#
Expose any Agent as a FunctionTool so another Agent can invoke it like any other tool:
as_tool() returns a FunctionTool named task_{child.name} that accepts an objective parameter and forwards it into the child's stream. See Task Delegation for sub-task streams, depth limiting, and stream factories.
Turn lifecycle#
Each await agent.ask(...) runs through the middleware chain in this order (outermost → innermost):
1. AssemblerMiddleware (if assembly=[...])
2. _HaltCheckMiddleware (if assembly=[...] — watches for HaltEvent)
3. _CompactionMiddleware (if knowledge.compact configured)
4. _AggregationMiddleware (if knowledge.aggregate configured)
5. User-provided middleware (retry, rate-limit, logging, …)
6. LLM client (innermost)
The internal harness middleware (_AssemblerMiddleware, _HaltCheckMiddleware, _CompactionMiddleware, _AggregationMiddleware) are assembled conditionally — you only pay for what you turn on.
Lifecycle events emitted during a turn include ObserverStarted / ObserverCompleted, CompactionCompleted, AggregationCompleted, and HaltEvent. Subscribe to any of them via an Observer or a stream subscriber.