Skip to content

Lifecycle Hooks

Every agent execution flows through a deterministic 10-phase lifecycle. Hooks let you intercept any phase to add logging, metrics, validation, or custom behavior.

import { Effect } from "effect";
import { ReactiveAgents } from "reactive-agents";
const agent = await ReactiveAgents.create()
.withProvider("anthropic")
.withReasoning()
.withHook({
phase: "think",
timing: "after",
handler: (ctx) => {
console.log(`Iteration ${ctx.metadata.stepsCount}`);
return Effect.succeed(ctx);
},
})
.build();
PhaseWhen It RunsCommon Hook Use Cases
bootstrapBefore anything elseLoad external config, validate preconditions
guardrailInput safety checkLog blocked inputs, custom filtering
cost-routeModel tier selectionOverride routing decisions
strategyStrategy selectionLog which strategy was chosen
thinkEach reasoning iterationProgress logging, custom metrics
actTool executionTool call tracking, audit logging
observeProcess tool resultsResult validation, caching
verifyOutput fact-checkingCustom verification logic
memory-flushPersist memoriesCustom memory operations
completeFinal result assemblyPost-processing, cleanup

Each phase supports three timing points:

  • before — Runs before the phase executes. Can modify the ExecutionContext.
  • after — Runs after the phase completes successfully. Receives the updated context.
  • on-error — Runs when the phase throws an error. Can log or clean up, but cannot prevent the error from propagating.
handler: (ctx: ExecutionContext) => Effect.Effect<ExecutionContext, HookError>

The handler receives the current ExecutionContext and must return it (possibly modified). Useful fields include:

  • metadata — step count, strategy, last response, reasoning results (engine-populated)
  • toolResults — tool execution results accumulated this run
  • messages — conversation messages for the task
  • taskId / agentId / sessionId — correlation identifiers

Agent-visible working memory is the recall meta-tool (Conductor’s Suite), not a field on this context.

Hooks registered for the same phase and timing run sequentially in registration order. If a hook fails:

  • before hook failure: the phase is skipped and the on-error hook runs
  • after hook failure: logged but does not affect the phase result
  • on-error hook failure: logged but does not mask the original error
import { Effect } from "effect";
// …then chain on your builder:
.withHook({
phase: "think",
timing: "before",
handler: (ctx) => {
const step = ctx.metadata.stepsCount + 1;
const max = ctx.metadata.maxIterations ?? 10;
console.log(`Step ${step}/${max}`);
return Effect.succeed(ctx);
},
})
import { Effect } from "effect";
.withHook({
phase: "complete",
timing: "after",
handler: (ctx) => {
if (ctx.metadata.cost > 0.10) {
console.warn(`⚠ Execution cost $${ctx.metadata.cost.toFixed(3)} exceeded $0.10 threshold`);
}
return Effect.succeed(ctx);
},
})
import { Effect } from "effect";
.withHook({
phase: "act",
timing: "after",
handler: (ctx) => {
const last = ctx.toolResults.at(-1) as { toolName?: string } | undefined;
const toolName = last?.toolName ?? "unknown";
auditLog.append({ event: "tool_call", tool: toolName, taskId: ctx.taskId, timestamp: Date.now() });
return Effect.succeed(ctx);
},
})
import { Effect } from "effect";
.withHook({
phase: "think",
timing: "on-error",
handler: (ctx) => {
console.error(`Think phase failed at step ${ctx.metadata.stepsCount}. Check your prompt or model.`);
return Effect.succeed(ctx);
},
})