Classes

AgentBuilder

Class: AgentBuilder

Defined in: src/core/agent/AgentBuilder.ts:164

Fluent builder. tool() accepts any Tool<TArgs, TResult> and registers it by its schema.name. Duplicate names throw at build time.

Constructors

Constructor

new AgentBuilder(opts): AgentBuilder

Defined in: src/core/agent/AgentBuilder.ts:351

Parameters

opts

AgentOptions

Returns

AgentBuilder

Methods

act()

act(options): this

Defined in: src/core/agent/AgentBuilder.ts:570

Everything this agent DOES about its own loop, in one block.

Tools do the work. .act() decides about the work. watch remembers both — and nothing can act without being watched.

Five keys, one per moment of a turn, each optional and each the exact argument the individual door takes:

const agent = Agent.create({ provider, model })
  .act({
    input:      [scrubSSNs],        // the message, before the run commits it
    beforeTool: [refundCeiling],    // every call, before it is dispatched
    afterTool:  [hideRawPII],       // every result, before the model reads it
    window:     slidingWindow({ keepRecentTurns: 12 }),
    output:     [noInternalCodenames],
  })
  .build();

It is sugar, and provably so. Each key is forwarded to the door that already owned it — .messageMiddleware(), .toolMiddleware(), .window() — so the agent it builds sends the same request bytes and files the same records as the same rules spelled out one call at a time. That equivalence is pinned per key by tests, the way .compaction()'s is.

The keys cannot fall behind the loop. They are locked at compile time against LoopMoment, so a sixth moment cannot ship without a key here.

A rule speaks where its hooks say, not where you filed it. beforeTool and afterTool are one chain; an entry with both onToolCall and onToolResult runs at both moments whichever key you wrote it under — the KEYS are named for the moments, the HOOKS for what they receive — and an entry named under both keys is the same object attached once. A governance rule that silently did not run because it was written in the wrong bucket is exactly the failure this library exists to make impossible — so the bucket is checked for the hook it names, and the hooks decide the rest.

Call it once. A second .act() throws: two posture blocks means the answer to "what does this agent do at each moment?" is in two places, and the second one silently wins. Adding one piece to an agent somebody else built — a plugin, a policy pack — is what the individual doors are for, and they stay open for exactly that.

Parameters

options

ActOptions

One key per moment. input / output take message middleware, beforeTool / afterTool take tool middleware, window takes a WindowStrategy. Unknown keys throw.

Returns

this


appName()

appName(name): this

Defined in: src/core/agent/AgentBuilder.ts:893

Set the agent's display name — substituted as {{appName}} in commentary + thinking templates. Same place to brand a tenant ("Acme Bot"), distinguish multi-agent roles ("Triage" vs "Reviewer"), or localize ("Asistente"). Default: 'Chatbot'.

Parameters

name

string

Returns

this


build()

build(): Agent

Defined in: src/core/agent/AgentBuilder.ts:1956

Returns

Agent


checkIn()

checkIn(opts?): this

Defined in: src/core/agent/AgentBuilder.ts:1857

Parameters

opts?

CheckInBuilderOptions = {}

Returns

this


commentaryTemplates()

commentaryTemplates(templates): this

Defined in: src/core/agent/AgentBuilder.ts:908

Override agentfootprint's bundled commentary templates. Spread on top of defaultCommentaryTemplates; missing keys fall back. Same Record<string, string> shape with {{vars}} substitution as the bundled defaults — see defaultCommentaryTemplates for the full key list.

Use cases: i18n ('agent.turn_start': 'El usuario...'), brand voice ("You: {{userPrompt}}"), per-tenant customization.

Parameters

templates

Readonly<Record<string, string>>

Returns

this


compaction()

compaction(options): this

Defined in: src/core/agent/AgentBuilder.ts:753

Keep the live context window inside a token budget — without ever losing the record.

Sugar for .window(summarizeOldest(options)), and byte-for-byte the same agent. It keeps its own name because compaction is what the market calls this and it is the strategy most people want first.

At each ReAct iteration boundary, compaction compares the LAST call's adapter-reported input tokens against thresholdTokens. Over budget, it folds the oldest foldable span of the conversation into one summary message and sends that instead. Counted, never guessed: a provider that reports no usage gets a named refusal (CompactionUnmeasurableError) rather than an invented number.

The fold edits the window, not the record. The turns it folds stay in the run's commit log byte-identical — footprintjs's log is append-only, so a fold cannot erase them even in principle. The summary enters as its own recorded step naming every runtimeStageId it folded, plus what was measured and what refused to fold. A compacted run is still a provable run: the lens draws a fold seam, not a hole.

Never folded: the system envelope, the last keepRecentTurns turns, and any turn holding something unresolved — an unanswered tool call, a paused tool, a pending check-in. Folding an unanswered question would destroy the referent of the answer that has not arrived yet, so those refuse by name and the fold takes the next oldest instead.

Omit .compaction() and nothing changes: no stage, no extra keys, the same request bytes as before.

Parameters

options

CompactionOptions

Returns

this

Example

const agent = Agent.create({ provider: anthropic(), model: 'claude-sonnet-4-5' })
  .compaction({
    thresholdTokens: 120_000,
    summarizer: anthropic(),
    model: 'claude-haiku-4-5',   // the cheap one writes the summary
  })
  .build();

configure()

configure(fn): this

Defined in: src/core/agent/AgentBuilder.ts:503

Decide this run's model and/or system prompt when the run starts.

An agent is built once and run many times, but not every run wants the same model or the same instructions: a long message may deserve the bigger model, a tenant may have its own house rules, a canary may want last week's prompt. Rebuilding the whole agent per request works and is wasteful; reaching in and mutating one is worse, because the trace then describes an agent that no longer exists.

The resolver runs ONCE per run(), at the start of the run, and what it returns is committed to the traceresolvedModel and resolvedInstructions land in the run's commit log before the first LLM call, and the LLM call reads them from there. So the recording says which model actually answered instead of which model the agent was built with.

Return {} (or nothing) to keep the defaults; ctx.defaults carries them, so a resolver can decide relative to what was built rather than restating it. Omit .configure() entirely and every run behaves — and records — exactly as it did before.

This is the RUN axis only. Tools are the iteration axis and already have an owner: .toolProvider(), consulted every iteration.

Throws if called more than once (same rule as .toolProvider() — a silently-overridden resolver is a config that lies).

Parameters

fn

RunConfigFn

Returns

this

Examples

Bigger model for a bigger question
  const agent = Agent.create({ provider, model: 'small-model' })
    .system('You answer support questions.')
    .configure(({ message, defaults }) =>
      message.length > 500 ? { model: 'big-model' } : {},
    )
    .build();
Per-tenant house rules
  const agent = Agent.create({ provider, model })
    .system('You answer support questions.')
    .configure(({ identity, defaults }) => ({
      instructions: `${defaults.instructions}\n\n${rulesFor(identity?.tenant)}`,
    }))
    .build();

fact()

fact(injection): this

Defined in: src/core/agent/AgentBuilder.ts:1243

Register a Fact — developer-supplied data the LLM should see. User profile, env info, computed summary, current time, … Distinct from Skills (LLM-activated guidance) and Steering (always-on rules) in INTENT — the engine treats them all alike.

Parameters

injection

Injection

Returns

this


injection()

injection(injection): this

Defined in: src/core/agent/AgentBuilder.ts:946

Register any Injection. Use this for power-user / custom flavors; for built-in flavors use the typed sugar (.skill, .steering, .instruction, .fact).

An Injection carrying inject.messages is ROUTED here, not refused (7.19.1 refused it; 7.21.0 delivers it). What still gets refused is the pair the wire cannot take: a role: 'tool' message has no tool call to answer, so it is rejected here, at the declaration, on every provider. A role the ATTACHED provider cannot carry is a different question — it depends on the provider, which this builder does not have — so it is refused at run start instead, by name. This is the one funnel every flavor passes through, so a hand-built Injection cannot go around the checks the named factories make.

Parameters

injection

Injection

Returns

this


instruction()

instruction(injection): this

Defined in: src/core/agent/AgentBuilder.ts:1221

Register an Instruction — rule-based system-prompt guidance. Predicate runs each iteration. Use for context-dependent rules including the "Dynamic ReAct" on-tool-return pattern.

Parameters

injection

Injection

Returns

this


instructions()

instructions(injections): this

Defined in: src/core/agent/AgentBuilder.ts:1232

Bulk-register many instructions at once. Convenience for consumer code that organizes its instruction set in a flat array (const instructions = [outputFormat, dataRouting, ...]). Each element is registered via .instruction() so duplicate-id checks still fire per-entry.

Parameters

injections

readonly Injection[]

Returns

this


maxIterations()

maxIterations(n): this

Defined in: src/core/agent/AgentBuilder.ts:823

Override the ReAct iteration cap set via Agent.create({ maxIterations }). Convenience for builder-style code that prefers fluent setters over constructor opts. Last call wins.

Throws if n is not a positive integer or exceeds the hard cap (clampIterations's upper bound).

Parameters

n

number

Returns

this


memory()

memory(definition): this

Defined in: src/core/agent/AgentBuilder.ts:1270

Register a Memory subsystem — load/persist conversation context, facts, narrative beats, or causal snapshots across runs.

The MemoryDefinition is produced by defineMemory({ type, strategy, store }). Multiple memories layer cleanly via per-id scope keys (memoryInjection_${id}):

Agent.create({ provider })
  .memory(defineMemory({ id: 'short', type: MEMORY_TYPES.EPISODIC,
                         strategy: { kind: MEMORY_STRATEGIES.WINDOW, size: 10 },
                         store }))
  .memory(defineMemory({ id: 'facts', type: MEMORY_TYPES.SEMANTIC,
                         strategy: { kind: MEMORY_STRATEGIES.EXTRACT,
                                     extractor: 'pattern' }, store }))
  .build();

The READ subflow runs at the configured timing (default MEMORY_TIMING.TURN_START) and writes its formatted output to the memoryInjection_${id} scope key for the slot subflows to consume.

Parameters

definition

MemoryDefinition

Returns

this


messageMiddleware()

messageMiddleware(...middleware): this

Defined in: src/core/agent/AgentBuilder.ts:1833

Wrap the message boundary in a governance chain — the input before the model sees it, the output before the caller receives it.

Same verbs as toolMiddleware minus one: there is no ask here, and the type says so. Tool dispatch runs inside a pausable stage, so it has a checkpoint to suspend on; the message boundary is a plain stage, and inventing a second pause to give it one would be a worse answer than not offering it.

The 'input' half runs at the very top of the run, BEFORE the message is committed. That placement is the point: everything downstream reads scope.history — the window strategies, the injections, all three slots, the bytes on the wire and every slice taken afterwards — so the transformed text is what the whole run agrees was said. The 'output' half runs where the final answer is captured, so the record and the caller receive the same string.

deny(reason) at either phase raises a MessageDeniedError rather than returning. At 'input' there is no model to tell; at 'output' the middleware has just refused to release an answer, and handing the caller a string in its place is the one substitution they must never make without noticing. The error carries the reason, the phase and the middleware's name — never the refused content.

Parameters

middleware

...readonly MessageMiddleware[]

Returns

this

Example

import { Agent, allow } from 'agentfootprint';

const agent = Agent.create({ provider, model })
  .messageMiddleware({
    name: 'mask-card-numbers',
    onMessage: (msg) => {
      const clean = msg.content.replace(/\b(?:\d[ -]?){13,16}\b/g, '[card]');
      return clean === msg.content ? allow() : allow(clean, 'masked a card number');
    },
  })
  .build();

outputFallback()

outputFallback<T>(options): this

Defined in: src/core/agent/AgentBuilder.ts:1440

3-tier degradation for output-schema validation failures. Pairs with .outputSchema() — an agent that has one and not the other is refused at .build(), in either call order.

Three tiers:

  1. Primary — LLM emitted schema-valid JSON. Caller gets it.
  2. FallbackOutputSchemaError thrown. The async fallback(error, raw) runs; its return is re-validated.
  3. Canned — static safety-net value. NEVER throws when set.

canned is validated against the schema at .build() — fail-fast on misconfig (a canned that doesn't validate would defeat the fail-open guarantee at the exact moment it is needed).

The tiers run at the TYPED boundary — run() does not reach them

runTyped() and parseOutputAsync() engage the chain. run() does not, and cannot: these tiers produce a typed value T, and run() resolves to the raw answer string — substituting a fallback there would hand a caller a different answer than the model gave, invisibly. So an agent consumed through run() (a server route, a queue worker, standingAgent) gets NO fallback, and until 8.18.0 nothing said so. Now the unmet-contract warning and agentfootprint.agent.output_contract_unmet both carry fallbackConfigured: true — the signal that a safety net exists and this caller is not standing under it.

Two typed events fire on tier transitions for observability:

  • agentfootprint.resilience.output_fallback_triggered
  • agentfootprint.resilience.output_canned_used — carries retriesSpent, and warns when the canned value lands after re-asks that were billed. With canned set, runTyped() is structurally unable to throw, so nothing else would report that spend.

Type Parameters

T

T

Parameters

options

OutputFallbackOptions<T>

Returns

this

Example

import { z } from 'zod';
const Refund = z.object({ amount: z.number(), reason: z.string() });

const agent = Agent.create({...})
  .outputSchema(Refund)
  .outputFallback({
    fallback: async (err, raw) => ({ amount: 0, reason: 'manual review' }),
    canned:   { amount: 0, reason: 'unable to process' },
  })
  .build();

outputSchema()

outputSchema<T>(parser, opts?): this

Defined in: src/core/agent/AgentBuilder.ts:1366

Declarative terminal contract. The agent's final answer must be JSON matching parser. Auto-injects a system-prompt instruction telling the LLM the shape, and exposes agent.runTyped() / agent.parseOutput() for parse + validate at the call site.

The parser is duck-typed: any object with a parse(unknown): T method works (Zod, Valibot, ArkType, hand-written). The optional description field on the parser drives the auto-generated instruction; consumers can also override via opts.instruction.

Throws if called more than once on the same builder (avoids silent override surprises).

What the DEFAULT buys you, and what it does not

.outputSchema(parser) on its own means judge, do not re-ask: the prompt gets the instruction, the answer is validated in the loop, and a failure is recorded (outputAttempts), announced (agentfootprint.agent.output_contract_unmet), warned about once, and readable afterwards through agent.outputContractUnmet(). What it does NOT do is spend a turn fixing the answer — pass { retries: 1 } for the first real correction. Before 8.18.0 the default judged nothing at all inside the run, and a run() caller could not tell a contract had been missed.

The two ways a run with a contract can END

runTyped() throws OutputSchemaError when the answer fails the schema — and MessageDeniedError when an act({ output }) rule refused to release the answer at all. The second one is not a schema failure and is never re-asked: the answer was withheld on purpose, and asking the model for a better-shaped version of a string nobody is allowed to see would route around the rule. A catch block that only knows about OutputSchemaError will miss it.

run() throws neither for a schema failure: it returns the raw answer, as it always has, and says so through the channels above.

Type Parameters

T

T

Parameters

parser

OutputSchemaParser<T>

Validation strategy that throws on shape failure.

opts?

OutputSchemaOptions

Optional { name, instruction, retries, strategy, jsonSchema }.

Returns

this

Example

import { z } from 'zod';
  const Output = z.object({
    status: z.enum(['ok', 'err']),
    items: z.array(z.string()),
  }).describe('A status enum + an array of strings.');

  const agent = Agent.create({...})
    .outputSchema(Output, { retries: 1 })
    .build();

  const typed = await agent.runTyped({ message: '...' });
  typed.status; // narrowed to 'ok' | 'err'

rag()

rag(definition): this

Defined in: src/core/agent/AgentBuilder.ts:1306

Register a RAG retriever — semantic search over a vector-indexed corpus. Identical plumbing to .memory() (RAG resolves to a MemoryDefinition produced by defineRAG()); this alias exists so the consumer's intent reads clearly:

agent
  .memory(shortTermConversation)   // remembers what the USER said
  .rag(productDocs)                // retrieves what the CORPUS says
  .build();

Both end up as memory subflows, but the alias separates "user conversation memory" from "document corpus retrieval" in code intent, ids, and Lens chips.

Parameters

definition

MemoryDefinition

Returns

this


recorder()

recorder(_rec): this

Defined in: src/core/agent/AgentBuilder.ts:877

REMOVED in 9.0.0 — use AgentBuilder.watch instead.

This is a one-release grace error, not a method. Deprecated in 8.0.0 in favour of .watch(...) — same list, same order, same attachment, and .watch() takes more than one observer. The body was deleted in 9.0.0; the NAME is kept for one major so a call site that missed the deprecation gets a sentence instead of builder.recorder is not a function.

It throws at BUILD time, before any run, so the failure is deterministic and lands in development rather than in a trace nobody is watching.

Parameters

_rec

CombinedRecorder

Returns

this

Deprecated

Removed in 9.0.0 — call .watch(rec). This throwing stub is deleted in 10.0.0.


reliability()

reliability(config): this

Defined in: src/core/agent/AgentBuilder.ts:1547

Wire rules-based reliability around every CallLLM execution. The framework wraps the LLM call in a retry/fallback/fail-fast loop driven by preCheck and postDecide rules.

Decision verbs the rules can emit (see ReliabilityDecision for the full list):

continue — pre-check OK, proceed to the call • ok — post-call OK, commit and return • retry — re-call same provider (bumps attempt) • retry-other — advance to next provider in providers[]fallback — invoke config.fallback(req, lastError)fail-fast — throw ReliabilityFailFastError at agent.run()

Streaming + reliability semantics — first-chunk arbitration: Pre-first-chunk failures (connection/headers/breaker-open) honor the full rule set (retry, retry-other, fallback, fail-fast). Post-first-chunk failures (mid-stream) honor only ok and fail-fast; rules wanting retry/retry-other/fallback are escalated to fail-fast with kind 'mid-stream-not-retryable'. This matches LangChain's RunnableWithFallbacks pattern and the prevailing industry default — see the streaming + reliability design memo for the full discussion.

Throws if called more than once on the same builder.

Parameters

config

ReliabilityConfig

Returns

this

Example

import { Agent } from 'agentfootprint';
  import { ReliabilityFailFastError } from 'agentfootprint/reliability';

  const agent = Agent.create({ provider, model: 'mock' })
    .system('Triage support tickets.')
    .reliability({
      postDecide: [
        { when: (s) => s.errorKind === '5xx-transient' && s.attempt < 3,
          then: 'retry', kind: 'transient-retry' },
        { when: (s) => s.error !== undefined,
          then: 'fail-fast', kind: 'unrecoverable' },
      ],
      circuitBreaker: { failureThreshold: 3 },
    })
    .build();

  try {
    await agent.run({ message: 'help' });
  } catch (e) {
    if (e instanceof ReliabilityFailFastError) {
      console.log(e.kind, e.reason);
    }
  }

selfExplain()

selfExplain(opts?): this

Defined in: src/core/agent/AgentBuilder.ts:1865

Parameters

opts?

SelfExplainOptions = {}

Returns

this


skill()

skill(injection): this

Defined in: src/core/agent/AgentBuilder.ts:965

Register a Skill — LLM-activated, system-prompt + tools. Auto-attaches the read_skill activation tool to the agent. Skill stays active for the rest of the turn once activated.

Parameters

injection

Injection

Returns

this


skillGraph()

skillGraph(graph, options?): this

Defined in: src/core/agent/AgentBuilder.ts:1010

Mount a declarative skill graph (proposal 002) — each skill carries a graph-derived trigger (entry → always/rule, deterministic route → rule / on-tool-return), so dynamic token-efficient loading becomes declared and drawable. Pure sugar over .injection()graph.toMermaid() renders the topology.

The optional second argument (SG-C, 9.17.0) sets the MOUNT's routing posture and cursor span — see SkillGraphOptions. Omitted, the agent behaves byte-for-byte as it always has.

Parameters

graph
deferredBodyContract?

{ mode: "warn" | "throw"; }

The graph's note that it deferred its body-contract checks to agent build (built without knownTools — see SkillGraph.deferredBodyContract). Optional for forward-compat; absent → the checks already ran at graph build (or were off), so this agent never re-runs them. Library-built graphs also stamp the note on each compiled skill's metadata, which build() prefers — this field is the fallback for a structurally-typed graph without the per-skill stamps (skills found by both are deduped by id).

deferredBodyContract.mode

"warn" | "throw"

edges?

readonly object[]

The declared edges. Read for ONE thing: which skills the graph wires, so the read_skill gate can tell a skill the graph routes from one it never mentions (see openSkillIds in build()). Optional for forward-compat with graphs built before edges existed; absent → the graph wires nothing.

entrySelection?

"scorer" | "model-read" | "classify"

How the graph picks a turn's starting entry (SG-C). Read for one refusal: strictness: 'rails' cannot honor 'model-read'.

explainNextSkill?

(ctx) => CursorMove

The same cursor resolver, reporting the clause that won (8.5.0). Optional for forward-compat; absent → no cursorMove on context.evaluated.

nextSkill

(ctx) => string | undefined

nodes?

readonly object[]

The drawn nodes. Read for TWO things: a predicate node means this graph is a decision tree() (the gate's refusal says so out loud), and the node-id set is what a continuity cursor is validated against (droppedResume). Derived here rather than added to SkillGraph as a mode field — the shape is already public, and one fact should not be declared twice.

reachableSkills?

(currentSkillId?) => readonly string[]

scoreEntries?

(ctx, signal?) => Promise<EntryScoring>

skills

readonly Injection[]

supersededEntries?

(ctx) => readonly string[]

The entries the cursor law superseded this iteration (8.15.0). Optional for forward-compat; absent → no supersededIds on context.evaluated.

turnRouting?

TurnRoutingPlan

The graph's turn-routing plan (SG-C) — tier-1 rules, intent candidates, the classifier and the resolved tie policy. Optional for forward-compat with graphs built before it existed; absent → the cascade cannot run (classify needs it; continuity degrades to nothing rather than guess).

options?

SkillGraphOptions

Returns

this

Examples

const graph = skillGraph()
    .entry(triage)
    .route(triage, sfp, { when: (r) => r.toolName === 'get_counters' && JSON.parse(r.result).crc > 0 })
    .build();
  Agent.create({ provider }).skillGraph(graph).build();
// The conversation keeps its place across turns, and the model may
  // route only when the router declared ambiguity:
  Agent.create({ provider })
    .skillGraph(graph, { continuity: 'conversation', strictness: 'guard' })
    .build();

skills()

skills(registry): this

Defined in: src/core/agent/AgentBuilder.ts:980

Bulk-register every Skill in a SkillRegistry. Use for shared skill catalogs across multiple Agents — register skills once on the registry; attach the same registry to every consumer Agent.

Parameters

registry
list

Returns

this

Example

const registry = new SkillRegistry();
  registry.register(billingSkill).register(refundSkill);
  const supportAgent = Agent.create({ provider }).skills(registry).build();
  const escalationAgent = Agent.create({ provider }).skills(registry).build();

steering()

steering(injection): this

Defined in: src/core/agent/AgentBuilder.ts:1212

Register a Steering doc — always-on system-prompt rule. Use for invariant guidance: output format, persona, safety policies.

Parameters

injection

Injection

Returns

this


system()

system(prompt, options?): this

Defined in: src/core/agent/AgentBuilder.ts:375

Set the base system prompt.

Parameters

prompt

string

The system prompt text. Stable per-turn.

options?

Optional config. cache controls how the CacheDecision subflow treats this prompt block:

  • 'always' (default) — cache the base prompt as a stable prefix anchor. Highest cache-hit rate; recommended for production agents whose system prompt rarely changes.
  • 'never' — skip caching. Use if the prompt contains volatile content (timestamps, per-request user IDs).
  • 'while-active' — semantically equivalent to 'always' for the base prompt (it's always active by definition).
  • { until } — conditional invalidation (e.g., flush after iter 5).
cache?

CachePolicy

Returns

this


thinking()

thinking(opts): this

Defined in: src/core/agent/AgentBuilder.ts:1640

v2.14+ — REQUEST-side thinking activation. Tells the provider to emit reasoning blocks alongside its response.

What this does: every LLM call carries LLMRequest.thinking = { budget }. The AnthropicProvider translates to thinking: { type: 'enabled', budget_tokens: N } on the wire. The model spends up to budget reasoning tokens before producing the visible response.

Distinct from .thinkingHandler():

  • .thinking({ budget }) = ASK the model to think (request side)
  • .thinkingHandler(h) = NORMALIZE the response (response side)

Most consumers want both; auto-wired handler covers the response side automatically when .thinking() is set on a thinking-capable provider. Setting .thinking() without .thinkingHandler(null) is the typical happy path.

Provider compatibility:

  • Anthropic: requires claude-sonnet-4-5 / opus-4-5 (or newer). Older models reject with HTTP 400.
  • OpenAI: ignores. o1/o3 reasoning is selected at the model id level; this field is a no-op for OpenAIProvider.

Budget guidance: Anthropic recommends 1024-32000 reasoning tokens. budget MUST be less than the request's max_tokens (defaults to 4096 in AnthropicProvider — bump via the request maxTokens if budget > ~3000).

Calling twice throws — same shape as .reliability() / .outputSchema().

Parameters

opts
budget

number

Returns

this

Example

Agent.create({ provider: anthropic({...}), model: 'claude-sonnet-4-5' })
    .system('You are a careful reasoning agent.')
    .thinking({ budget: 5000 })   // ask Anthropic to think
    .build();

thinkingHandler()

thinkingHandler(handler): this

Defined in: src/core/agent/AgentBuilder.ts:1591

Wire a thinking handler (v2.14+). Three usage patterns:

• OMITTED (default) — framework auto-wires by provider.name via findThinkingHandler from the registry. Most consumers using a shipped provider get thinking support for free.

• EXPLICIT handler — override the auto-wire. For custom providers or for swapping in a custom Anthropic/OpenAI handler with different normalization (e.g. redacting blocks before they land).

• EXPLICIT null — opt out entirely. The thinking subflow is NOT mounted even if the provider would auto-match. Use when you want to skip thinking parsing for this agent (cost / latency / UX reasons).

Calling twice throws — same shape as .reliability() / .outputSchema() to enforce single-source intent.

Parameters

handler

ThinkingHandler | null

Returns

this

Examples

// Default — auto-wire AnthropicThinkingHandler for anthropic provider
  Agent.create({ provider: anthropic({...}), model: '...' }).build();
// Custom handler that redacts thinking content
  Agent.create({...}).thinkingHandler(myRedactingHandler).build();
// Opt out of thinking parsing entirely
  Agent.create({ provider: anthropic({...}), model: '...' })
    .thinkingHandler(null)
    .build();

thinkingTemplates()

thinkingTemplates(templates): this

Defined in: src/core/agent/AgentBuilder.ts:920

Override agentfootprint's bundled thinking templates. Same contract shape as commentary; different vocabulary — first-person status the chat bubble shows mid-call. Per-tool overrides go via tool.<toolName> keys (e.g., 'tool.weather': 'Looking up the weather…'). See defaultStatusTemplates for the full key list.

Parameters

templates

Readonly<Record<string, string>>

Returns

this


tool()

tool<TArgs, TResult>(tool): this

Defined in: src/core/agent/AgentBuilder.ts:396

Type Parameters

TArgs

TArgs

TResult

TResult

Parameters

tool

Tool<TArgs, TResult>

Returns

this


toolMiddleware()

toolMiddleware(...middleware): this

Defined in: src/core/agent/AgentBuilder.ts:1760

Wrap every tool dispatch in a governance chain.

Each middleware answers with one of three verbs — allow(), deny(reason) or ask({ question }) — and there is deliberately no fourth. In particular there is no way to return a result: whatever the chain decides, the answer the model finally reads is the real tool's output or a refusal. A rule cannot quietly become the tool.

  • allow() passes the call through. allow(args, why) replaces the args and the run commits BOTH versions with your why beside them, so a slice taken later can find the moment they changed and who changed them.
  • deny(reason) refuses. The reason reaches the model verbatim, as the tool result, and the loop continues — the agent adapts in-flight. A denial is data, not a crash.
  • ask({ question }) suspends the run for a person, on the same checkpoint machinery checkIn and askHuman use. The answer is a DECISION, not a result: approve and the chain resumes and the real tool runs; decline and it becomes a denial the model reads.

Order is call order, and each middleware sees the previous one's output. The first non-allow answer wins and the rest of the chain does not run. A middleware that throws is a denial carrying the error as its reason — never a silent pass.

A link may also carry an onToolResult hook, which decides about the RESULT once the tool has run and before the model reads it — allow(), allow(value, why) or deny(reason), and no ask, because the tool has already run and there is nothing left for a person to prevent. That half of the chain is walked BACKWARDS, so the first-declared rule has the first word about the call and the last word about the answer. A link with only onToolResult takes no part in dispatch at all.

.act({ beforeTool, afterTool }) is the same chain, named by moment.

An existing PermissionChecker still decides FIRST: it is not part of this chain, it runs ahead of it, so a call it denies never reaches a middleware. gatedTools is a different layer again — it decides which tools the model can SEE; this decides what happens when one is called.

Omit this and nothing changes: no chain walk, no committed ledger key, the same request bytes.

Parameters

middleware

...readonly ToolMiddleware[]

Returns

this

Example

import { Agent, allow, deny } from 'agentfootprint';

const agent = Agent.create({ provider, model })
  .toolMiddleware({
    name: 'no-prod-writes',
    onToolCall: (call) =>
      call.args.env === 'prod' ? deny('writes to prod need a change ticket') : allow(),
  })
  .build();

toolProvider()

toolProvider(provider): this

Defined in: src/core/agent/AgentBuilder.ts:450

Wire a chainable ToolProvider (from agentfootprint/providers) as the agent's per-iteration tool source.

The provider is consulted EVERY iteration via provider.list(ctx) with ctx = { iteration, activeSkillId, identity }. Tools the provider emits flow into the Tools slot alongside any static tools registered via .tool() / .tools(). The tool-call dispatcher also consults the provider so dynamic chains (gatedTools, skillScopedTools) dispatch correctly when their visible-set changes mid-turn.

Throws if called more than once on the same builder (avoids silent override surprises).

Parameters

provider

ToolProvider

Returns

this

Example

Permission-gated baseline
  import { gatedTools, staticTools } from 'agentfootprint/providers';
  import { PermissionPolicy } from 'agentfootprint/security';

  const policy = PermissionPolicy.fromRoles({
    readonly: ['lookup', 'list_skills', 'read_skill'],
    admin:    ['lookup', 'list_skills', 'read_skill', 'delete'],
  }, 'readonly');

  const provider = gatedTools(
    staticTools(allTools),
    (toolName) => policy.isAllowed(toolName),
  );

  const agent = Agent.create({ provider: llm, model })
    .system('You answer.')
    .toolProvider(provider)
    .build();

tools()

tools(tools): this

Defined in: src/core/agent/AgentBuilder.ts:411

Register many tools at once. Convenience for tool sources that return a list (e.g., await mcpClient(...).tools()). Each tool is registered via .tool() so duplicate-name validation still fires per-entry.

Parameters

tools

readonly Tool<Record<string, unknown>, unknown>[]

Returns

this


watch()

watch(...observers): this

Defined in: src/core/agent/AgentBuilder.ts:857

Watch this agent. .act() says what the agent may do; .watch() says who is looking while it does it.

Every observer handed here is attached before build() returns, so it sees every event from the very first run — there is no window where the agent has run and nobody was watching.

Variadic, because observers come in sets:

const agent = Agent.create({ provider, model })
  .watch(toolChoiceRecorder(), routeRecorder())
  .act({ beforeTool: [budgetGuard] })
  .build();

Build time, not run time. This returns the builder; agent.attach(o) attaches to a live agent and returns an Unsubscribe you own. Same mechanism underneath — .watch() replays through agent.attach() at the end of build() — so mixing the two is fine and order is preserved.

Called more than once, the sets concatenate in call order. Nothing is de-duplicated here; footprintjs's executor dedupes by recorder id at run time, so the same observer handed in twice still fires once.

Parameters

observers

...readonly CombinedRecorder[]

Returns

this


window()

window(strategy): this

Defined in: src/core/agent/AgentBuilder.ts:643

Choose how the live context window is kept inside its budget.

This is the general door; the strategy decides everything about WHEN it acts and WHAT leaves. Three ship, and they share one turn segmentation and one refusal engine, so a refusal reason means the same thing under all of them:

summarizeOldest({ thresholdTokens, summarizer, ... }) fold the oldest span into one summary message. .compaction() is this, spelled shorter. slidingWindow({ keepRecentTurns }) keep the last N turns and drop older ones. No summarizer, no LLM call, no usage requirement — it runs on any provider. tokenBudget({ thresholdTokens }) the counted-token trigger, dropping instead of summarizing.

Never removed by any of them: the system envelope, the recent turns, and any turn holding something unresolved — an unanswered tool call, a paused tool, a pending check-in. Those refuse BY NAME in the record and the strategy takes the next oldest instead. Removing an unanswered question would destroy the referent of the answer that has not arrived yet, and splitting a tool_use from its tool_result produces a request the vendor rejects.

Whatever leaves the window stays in the ledger. footprintjs's commit log is append-only, so the turns were committed before the strategy ran and remain byte-identical; every strategy files its own recorded step naming the runtimeStageIds whose messages left, and emits one context.evicted per message. Removing is not forgetting.

Exactly one strategy per agent. Omit this (and .compaction()) and nothing changes: no stage, no extra committed key, the same request bytes.

Parameters

strategy

WindowStrategy

Returns

this

Example

import { Agent, slidingWindow } from 'agentfootprint';

const agent = Agent.create({ provider, model })
  .window(slidingWindow({ keepRecentTurns: 12 }))
  .build();

On this page

Class: AgentBuilderConstructorsConstructorParametersoptsReturnsMethodsact()ParametersoptionsReturnsappName()ParametersnameReturnsbuild()ReturnscheckIn()Parametersopts?ReturnscommentaryTemplates()ParameterstemplatesReturnscompaction()ParametersoptionsReturnsExampleconfigure()ParametersfnReturnsExamplesfact()ParametersinjectionReturnsinjection()ParametersinjectionReturnsinstruction()ParametersinjectionReturnsinstructions()ParametersinjectionsReturnsmaxIterations()ParametersnReturnsmemory()ParametersdefinitionReturnsmessageMiddleware()ParametersmiddlewareReturnsExampleoutputFallback()The tiers run at the TYPED boundary — run() does not reach themType ParametersTParametersoptionsReturnsExampleoutputSchema()What the DEFAULT buys you, and what it does notThe two ways a run with a contract can ENDType ParametersTParametersparseropts?ReturnsExamplerag()ParametersdefinitionReturnsrecorder()Parameters_recReturnsDeprecatedreliability()ParametersconfigReturnsExampleselfExplain()Parametersopts?Returnsskill()ParametersinjectionReturnsskillGraph()ParametersgraphdeferredBodyContract?deferredBodyContract.modeedges?entrySelection?explainNextSkill?nextSkillnodes?reachableSkills?scoreEntries?skillssupersededEntries?turnRouting?options?ReturnsExamplesskills()ParametersregistrylistReturnsExamplesteering()ParametersinjectionReturnssystem()Parameterspromptoptions?cache?Returnsthinking()ParametersoptsbudgetReturnsExamplethinkingHandler()ParametershandlerReturnsExamplesthinkingTemplates()ParameterstemplatesReturnstool()Type ParametersTArgsTResultParameterstoolReturnstoolMiddleware()ParametersmiddlewareReturnsExampletoolProvider()ParametersproviderReturnsExampletools()ParameterstoolsReturnswatch()ParametersobserversReturnswindow()ParametersstrategyReturnsExample