Skip to content

What's automatic

Every framework hides something. The problem is not hiding it — it is you not knowing what, and finding out at the wrong moment.

This page is the complete list. If something happens without you asking, it is here.

Done for you

Happens on its ownWhere you get in
assembling the messages (system, user, assistant, tool)the beforePrompt hook changes the system prompt
calling the model with the declared toolssampling on the provider or on @Agent
detecting that the model asked for a tool(closed — see below)
validating the arguments against the Zod schemayou write the schema
running the tool and feeding the result backthe beforeTool / afterTool hooks
appending each turn to the historyctx.state is public and editable
repeating while a loop's until is not trueyou write the until
creating the workflow state, one per runyou declare the class in @Workflow({ state })
retrying a transient network failureretry on the provider
masking known secrets in captured contentredact in the config
recording the execution treereport and log in the config

The deliberately closed part

Three rows are in bold. They are the core of the framework and have no customisation hook, on purpose.

When the model answers, something has to decide: is this a final answer, or a request for a tool? Hand-rolled, that code becomes a pile of conditionals that changes with every model:

ts
// what you do NOT write
if (response.tool_calls?.length) { … }
else if (response.content?.startsWith("{")) { …try to parse… }
else if (response.content?.includes("<tool_call>")) { …another format… }

That is where the nastiest agent bugs live: a small model emits the call as text instead of using the structured field, and the agent finishes thinking it answered. The framework handles it — including rescuing calls written as text, in several formats — and you never see it.

Why this is good

Swapping qwen2.5-coder for gpt-4o-mini should not require changing your code. Because that layer belongs to the framework, it does not.

If you genuinely need to intervene there, the right extension point is writing a provider — the answer to "my backend speaks differently".

What is not automatic

Equally important: the framework does not decide for you.

  • When to stop. A loop's until is yours. There is no hidden "I think we're done" heuristic — see Loops.
  • When to search memory. Nothing is injected into the prompt on its own. If you want vector context, you call recall where it makes sense.
  • How much to spend. Without a budget, nothing is measured or limited.
  • How to format recalled context. You build the string.
  • When a tool failure should be fatal. Everything is recoverable unless you throw FatalToolError.

The rule we follow: the framework provides the mechanism, you choose the policy.

What changes behaviour by default

One honest exception to the rule above. Retry is on: a 429 or a momentary 503 is retried up to 3 times, with growing backoff.

That was deliberate — a transient network failure taking down a whole run is almost never wanted. To turn it off:

ts
super({ host, model, retry: false });

Timeout, by contrast, has no default: an arbitrary ceiling would abort a slow local model that works today. Turn it on when you want to (Providers).

The second is redaction, also on: known secret patterns are masked in everything captured, before it reaches the report, the log or a plugin. A file on disk with no retention policy is the worst place for a secret to surface by accident. See Redaction.

What is conditionally automatic

Observation. A run builds its execution tree, emits events and asks the provider to stream only when someone is watchingreport, log, a plugin with onEvent, or an explicit observe: true.

With none of those, the run takes the zero-cost path: no tree, no events, no streaming. It is worth roughly 2× in CPU time per run, and it is the reason onEvent can appear to do nothing.

Where you can get in

From lightest to most invasive:

PrecisionTool
adjust the final promptbeforePrompt hook
inspect or block a toolbeforeTool hook (a throw cancels)
transform a tool's resultafterTool hook
transform the agent's responseafterResponse hook
handle an error without dyingonError hook
receive the context or state in a tool@context(), @state() on parameters
choose which vector memory@memory(Store) in the constructor
touch the history and contextctx.state, public
control the loopa loop's until
wrap every tool run or model callapp.use({ tool, chat })
talk to another backendwrite a provider
take over the whole turna run(input, ctx) method on the agent class

That last one is the total escape hatch: if the agent class defines run, it owns the step and no hook fires.