Step hooks
StepHook lifecycle points, prepare_step sugar, and stop_when predicates including stepCountIs and tokensExceed.
Step hooks
Step hooks observe and influence each iteration of the agent runner loop — the cycle of LM call → tool execution → LM call.
Configure via step_hooks on the Agent spec, or use the sugar fields prepare_step and stop_when (folded into hooks by normalizeAgent()).
StepHook protocol
Four lifecycle points:
| Hook | Purpose |
|---|---|
beforeStep | Rewrite messages/request or short-circuit with { stop: true } |
beforeFinalize | Veto termination when the model returns no tool calls; supply a Continuation |
shouldStop | Custom halt predicate evaluated after each step |
afterStep | Observational only — metrics, logging |
import type { StepHook, StepContext, StepDecision } from "@maniac-ai/agents/schemas";BaseStepHook in @maniac-ai/agents provides empty defaults for subclassing.
beforeStep example
Inject a system message at the start of every iteration:
import { BaseStepHook, runAgent } from "@maniac-ai/agents";
class InjectSystem extends BaseStepHook {
async beforeStep(ctx) {
return {
messages: [
{ role: "system", content: "Always cite sources." },
...ctx.messages
]
};
}
}
await runAgent({
id: "researcher",
instructions: "Research topics.",
model,
step_hooks: [new InjectSystem()]
}, "Summarize quantum computing");Return { stop: true, reason?: string } from beforeStep to halt the run early without another LM call.
beforeFinalize
When the model responds with text and no tool calls, the runner normally finalizes. beforeFinalize can veto that and inject a Continuation — for example forcing one more tool round.
stop_when predicates
stop_when is sugar for a shouldStop hook. Pass a StopWhen predicate function or use the shipped helpers from @maniac-ai/agents:
import {
stepCountIs,
tokensExceed,
wallSecondsExceed,
responseMatches,
runAgent
} from "@maniac-ai/agents";
// Halt after 5 runner iterations
await runAgent({ id: "a", instructions: "...", model, stop_when: stepCountIs(5) }, "go");
// Halt when cumulative tokens exceed 8_000
await runAgent({ ..., stop_when: tokensExceed(8_000) }, "go");
// Wall-clock budget
await runAgent({ ..., stop_when: wallSecondsExceed(120) }, "go");
// Regex on final assistant text
await runAgent({ ..., stop_when: responseMatches(/TASK_COMPLETE/) }, "go");| Predicate | Stops when |
|---|---|
stepCountIs(n) | Iteration count ≥ n |
tokensExceed(n) | Cumulative prompt + completion tokens ≥ n |
wallSecondsExceed(s) | Elapsed wall time ≥ s seconds |
responseMatches(pattern) | Latest assistant text matches RegExp or string |
Combine predicates by passing a custom shouldStop hook that ORs multiple conditions.
prepare_step sugar
prepare_step is a functional alternative to a beforeStep subclass:
await runAgent({
id: "a",
instructions: "...",
model,
prepare_step: async (ctx) => ({
messages: ctx.messages.filter(m => m.role !== "system")
})
}, "hello");normalizeAgent() prepends a CallableStepHook built from prepare_step and stop_when ahead of explicit step_hooks.
Hook composition
Multiple hooks run in array order. Each hook receives the StepContext after prior hooks have applied their mutations.
step_hooks: [
new InjectSystem(),
new MetricsHook(),
new AuditHook()
]Nested sub-agent runs inherit the parent agent's hook list unless the child spec defines its own.
Trace events
Each step emits step trace events with iteration index and budget state. Pair with Tracer streaming for live step counters in UIs.