Introduction
When building agents, you will often find yourself repeating the same configuration: setting up an LLM provider, registering tools, attaching guard hooks, and wiring a message compiler. TheAgentBuilder class provides a clean composition layer that lets you assemble fully configured AgentLoop instances from reusable, self-contained capabilities.
Instead of manually constructing each dependency and passing them to AgentLoop, you describe what your agent should be able to do by stacking capabilities. Each capability encapsulates a single concern — configuring the LLM provider, adding a tool, attaching a lifecycle hook, or enabling subagent delegation. The builder composes them all into a working agent in a single build() call.
This approach makes agent configuration declarative, testable, and easy to share across your application.
Quick Start
The following example creates an agent that can execute bash commands, uses the Anthropic provider, and enforces step and token limits:AgentBuilder::base() factory creates a builder pre-configured with sensible defaults: a ToolCallingDriver backed by the default LLM provider, the ConversationWithCurrentToolTrace message compiler, and an empty hook stack. Every withCapability() call returns a new builder instance — the builder is immutable, so you can safely branch configurations from a shared base.
Immutability
AgentBuilder is a final readonly class. Every withCapability() call returns a new builder instance with the capability appended, leaving the original unchanged. This means you can safely branch from a shared base without worrying about mutation side effects:
The Build Pipeline
When you callbuild(), the builder delegates to an internal AgentConfigurator that resolves all components in a specific order:
- Message compiler — determines how
AgentStatemessages are compiled into the LLM prompt. The default isConversationWithCurrentToolTrace, which includes all non-trace messages plus the current execution’s tool traces. - Tool-use driver — the driver responsible for calling the LLM and parsing tool calls from the response. The default is
ToolCallingDriver. The resolved message compiler is injected into the driver at this stage if the driver implementsCanAcceptMessageCompiler. - Concrete tools — all tools registered via capabilities, including deferred tools that need access to the finalized driver or event system. Deferred tools are resolved last because they may depend on the final driver and tool set.
- Interceptor — the hook stack is compiled into an interceptor that wraps every lifecycle phase of the agent loop. If no hooks have been registered, a lightweight
PassThroughInterceptoris used instead. - Resolved profile and binding — the profile is snapshotted from the resolved, unbound tools, driver, capabilities, and hooks. The completed profile is then injected into profile-aware tools, drivers, and hooks.
UseToolFactory, UseSubagents, or UsePlanningSubagent) receive the finalized driver, tool set, and event handler at resolution time. If you need a tool that references the driver, use UseToolFactory rather than UseTools.
Resolved profiles and derived prompts
AgentProfile is the deterministic description of the loop that was actually
built. UseSystemPrompt installs a lifecycle hook that derives its tool list and
guidelines from this resolved profile, so adding or removing a tool changes the
next composed prompt without maintaining a second hardcoded list:
CanAcceptAgentProfile implementation may use the injected profile
for runtime behavior, but its descriptor and profile contribution methods must
not depend on that injected value: those methods were already read to create the
snapshot. Loop with*() operations recompute the profile and rebind consumers.
Tools produced by deferred providers are marked with deferred: true in the
profile and in describe() output, including a deferred tool that replaces an
eager tool with the same name.
Core Capabilities
Core capabilities modify fundamental aspects of the agent’s behavior: which LLM it talks to, how it handles tool calls, what guards protect execution, and how the conversation context is compiled.UseLLMConfig
Configures the LLM provider and creates aToolCallingDriver for the agent. If omitted, the builder uses the default provider from LLMProvider::new().
When
maxRetries is greater than 1, an InferenceRetryPolicy is created and passed to the driver, enabling automatic retries on transient LLM errors.
UseGuards
Installs safety guards that stop execution when resource limits are reached. All parameters are optional; set a parameter tonull to disable that specific guard entirely.
Guards are implemented as hooks. Step, token, and time guards run at
BeforeStep with priority 200 (early in the lifecycle). The finish-reason guard runs at AfterStep with priority -200 (late in the lifecycle, after the LLM response has been processed).
UseTools
Adds one or more tool instances to the agent. Tools are merged with any previously registered tools, never replaced.UseTools multiple times across different capabilities. Each invocation merges additional tools into the existing set.
UseHook
Attaches a single hook to the agent’s lifecycle. Hooks intercept execution at defined trigger points and can modify the agent state or halt execution entirely.UseDriver
Replaces the default tool-use driver entirely. Use this when you need a completely custom driver implementation rather than the defaultToolCallingDriver.
UseDriverDecorator
Wraps the current driver with a decorator function. The decorator receives the existing driver and must return a newCanUseTools implementation. This is useful for adding cross-cutting concerns like logging, caching, or rate limiting around the driver without replacing it.
UseContextCompiler
Replaces the message compiler that prepares the conversation history for the LLM. The default compiler isConversationWithCurrentToolTrace.
UseContextCompilerDecorator
Wraps the current message compiler with a decorator. This is the recommended approach for adding token-limit trimming, context windowing, or injecting additional context without replacing the entire compilation pipeline.UseContextConfig
Sets a system prompt and optional response format that are injected before each step via aBeforeStep hook at priority 100. The system prompt is always present regardless of how messages are compiled.
string system prompt and a ResponseFormat object are accepted. If both are empty, the capability is a no-op.
UseReActConfig
Replaces the driver with aReActDriver that implements the Reasoning and Acting (ReAct) pattern. The agent alternates between explicit thinking steps and tool-use actions, producing structured reasoning traces that make the decision process transparent.
UseToolFactory
Registers a deferred tool factory. The factory callback is not invoked immediately — it runs duringbuild() after the driver and tool set have been finalized. This gives the factory access to the complete agent context.
Tools collection, the finalized CanUseTools driver, and the CanHandleEvents event dispatcher. It must return a single ToolInterface instance.
Domain Capabilities
Domain capabilities bundle tools, hooks, and configuration for specific workflows. They are built on top of the core capabilities and provide higher-level abstractions.Writing Custom Capabilities
Every capability implements theCanProvideAgentCapability interface, which defines two methods: capabilityName() for registry lookups, and configure() for applying the capability’s configuration to the agent.
CanConfigureAgent interface provides read and write access to all configurable components:
The
capabilityName() method returns a string identifier used by AgentCapabilityRegistry to look up capabilities by name. This is how agent templates reference capabilities in their definition files (see Agent Templates).
Discovering capabilities from Composer manifests
Trusted packages can publish zero-argument capability and tool classes underextra.cognesy-agents in composer.json. Discovery is explicit and populates the
same registries used by definitions:
DefinitionLoopFactory
resolves definition references. Root-package declarations override vendor
declarations; malformed entries are reported by $result->errors(). See
Agent Templates for
the manifest shape and trust boundary.
Hook contract diagnostics
AgentBuilder::withHookContractDiagnostics() emits contract-violation events
while preserving hook output. withStrictHookContracts() turns the same checks
into exceptions and is intended for tests and controlled deployments. Both modes
are opt-in; default hook execution remains compatible and permissive.
AgentBuilder vs AgentLoop
UseAgentLoop::default() or construct AgentLoop directly when you have a small, one-off setup where the wiring is straightforward. Use AgentBuilder when:
- The configuration is complex enough to benefit from decomposition into capabilities.
- You want to share the same setup across multiple agents via branching.
- You need deferred tool resolution (tools that depend on the final driver).
- You want testable, reusable configuration units that can be independently verified.
Event Propagation
TheAgentBuilder::base() method accepts an optional CanHandleEvents parent. Events dispatched by the built agent propagate upward to this parent handler, allowing you to collect events from multiple agents in a single place: