Skip to content

Interventions

Interventions are a composable control layer for agents. They provide a typed action model for common control concerns — authorization, guardrails, steering, and content transformation — with ordered evaluation and short-circuiting. Unlike raw hooks and plugins which mutate event objects directly, intervention handlers return typed decisions (proceed, deny, guide, confirm, transform) that the framework applies with well-defined semantics — enabling automatic short-circuiting, feedback accumulation, and conflict resolution.

Create an intervention handler by extending InterventionHandler and overriding the lifecycle methods you need. Register handlers via the interventions option in agent configuration:

class ToolGuard extends InterventionHandler {
readonly name = 'tool-guard'
private blockedTools: string[]
constructor(blockedTools: string[]) {
super()
this.blockedTools = blockedTools
}
override beforeToolCall(event: BeforeToolCallEvent) {
if (this.blockedTools.includes(event.toolUse.name)) {
return InterventionActions.deny(
`Tool '${event.toolUse.name}' is not allowed in this environment`
)
}
return InterventionActions.proceed()
}
}
const agent = new Agent({
tools: [searchTool, deleteTool],
interventions: [new ToolGuard(['delete_file'])],
})
// The agent can search freely, but any attempt to call delete_file
// is blocked before execution — the model sees the denial reason
// and adjusts its approach
await agent.invoke('Clean up the temp directory')

Handlers only need to override the lifecycle methods relevant to their concern — all methods default to proceed().

Each lifecycle method returns one of five typed actions:

ActionFactoryDescription
ProceedInterventionActions.proceed()Allow the operation to continue unchanged
DenyInterventionActions.deny(reason)Block the operation. Short-circuits remaining handlers
GuideInterventionActions.guide(feedback)Cancel and provide feedback for the model to retry with
ConfirmInterventionActions.confirm(prompt)Pause for human approval
TransformInterventionActions.transform(apply)Modify event content in-place before execution continues

The following examples show each action type in a realistic handler:

// deny — block tool calls that access production resources
class EnvironmentGuard extends InterventionHandler {
readonly name = 'environment-guard'
override beforeToolCall(event: BeforeToolCallEvent) {
const input = event.toolUse.input as Record<string, string>
if (input.database?.includes('prod')) {
return InterventionActions.deny('Production database access is not allowed')
}
return InterventionActions.proceed()
}
}
// guide — steer the model when it tries to send emails without a subject
class EmailValidator extends InterventionHandler {
readonly name = 'email-validator'
override beforeToolCall(event: BeforeToolCallEvent) {
if (event.toolUse.name === 'send_email') {
const input = event.toolUse.input as Record<string, string>
if (!input.subject) {
return InterventionActions.guide('All emails must include a subject line.')
}
}
return InterventionActions.proceed()
}
}
// confirm — require human approval before deleting files
class DeleteApproval extends InterventionHandler {
readonly name = 'delete-approval'
override beforeToolCall(event: BeforeToolCallEvent) {
if (event.toolUse.name === 'delete_file') {
const input = event.toolUse.input as Record<string, string>
return InterventionActions.confirm(
`Approve deleting "${input.path}"?`
)
}
return InterventionActions.proceed()
}
}
// transform — redact PII from outgoing email bodies
class PiiRedactor extends InterventionHandler {
readonly name = 'pii-redactor'
override beforeToolCall(event: BeforeToolCallEvent) {
if (event.toolUse.name === 'send_email') {
return InterventionActions.transform((e) => {
const toolEvent = e as BeforeToolCallEvent
const input = toolEvent.toolUse.input as Record<string, string>
input.body = input.body.replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[REDACTED]')
})
}
return InterventionActions.proceed()
}
}

Intervention handlers can override five lifecycle methods. Each method supports a specific subset of actions:

MethodValid ActionsWhen it Runs
beforeInvocationProceed, Deny, Guide, TransformBefore the agent loop starts
beforeToolCallProceed, Deny, Guide, Confirm, TransformBefore each tool execution
afterToolCallProceed, TransformAfter each tool execution
beforeModelCallProceed, Deny, Guide, TransformBefore each model API call
afterModelCallProceed, Guide, TransformAfter each model response

How actions behave depends on the lifecycle method:

ActionBefore eventsAfter events
DenySets event.cancel, short-circuits remaining handlersNo effect (warns at runtime)
GuideOn beforeToolCall/beforeInvocation: cancels with accumulated feedback. On beforeModelCall: injects feedback as user messageInjects feedback and retries
ConfirmPauses agent via interrupt/resume for human approval; denied responses set event.cancelNot supported
TransformCalls action.apply(event) — later handlers see modified contentCalls action.apply(event)

Handlers evaluate in registration order. If any handler returns deny(), remaining handlers are skipped — the operation is blocked immediately. This enables efficient pipelines where fast checks (like authorization) run first and prevent expensive evaluations (like LLM-based steering) from running unnecessarily.

class RateLimiter extends InterventionHandler {
readonly name = 'rate-limiter'
private callCount = 0
override beforeToolCall(event: BeforeToolCallEvent) {
this.callCount++
if (this.callCount > 10) {
// deny() short-circuits: handlers registered after this one are skipped
return InterventionActions.deny('Rate limit exceeded')
}
return InterventionActions.proceed()
}
}
class ToneSteeringHandler extends InterventionHandler {
readonly name = 'tone-steering'
override afterModelCall(event: AfterModelCallEvent) {
// This handler never runs for denied tool calls
return InterventionActions.guide('Use a more professional tone.')
}
}
// Handlers evaluate in registration order
const agent = new Agent({
tools: [searchTool],
interventions: [
new RateLimiter(), // Evaluates first
new ToneSteeringHandler(), // Skipped if RateLimiter denies
],
})

For guide() actions, all handlers continue to run and their feedback is accumulated — the model receives combined guidance from all guiding handlers.

The onError property controls what happens when a handler throws an exception:

ValueBehavior
'throw'Rethrow the error (default). The invocation fails.
'proceed'Log the error and continue as if proceed() was returned.
'deny'Log the error and treat it as a deny() (fail-closed).
// 'proceed' — if this handler throws, continue as if proceed() was returned
class BestEffortLogger extends InterventionHandler {
readonly name = 'best-effort-logger'
readonly onError: OnError = 'proceed'
override beforeToolCall(event: BeforeToolCallEvent) {
// If the logging service is unreachable, the agent continues normally
console.log(`Tool called: ${event.toolUse.name}`)
return InterventionActions.proceed()
}
}
// 'deny' — if this handler throws, treat it as a deny (fail-closed)
class StrictAuth extends InterventionHandler {
readonly name = 'strict-auth'
readonly onError: OnError = 'deny'
override beforeToolCall(event: BeforeToolCallEvent) {
// If the auth service is down (throws), the operation is denied
if (!this.checkPermission(event.toolUse.name)) {
return InterventionActions.deny('Unauthorized')
}
return InterventionActions.proceed()
}
private checkPermission(toolName: string): boolean {
// ... call external auth service
return true
}
}
// 'throw' (default) — errors propagate and fail the invocation
class CriticalValidator extends InterventionHandler {
readonly name = 'critical-validator'
// onError defaults to 'throw'
override beforeToolCall(event: BeforeToolCallEvent) {
// If this throws, the entire invocation fails
return InterventionActions.proceed()
}
}

Use 'deny' for security-critical handlers where a failure should block execution. Use 'proceed' for non-critical handlers like logging where availability is more important than enforcement.

Interventions are built on top of the hooks system — under the hood, each lifecycle method registers a hook callback. The difference is in how they communicate with the framework.

Hooks and plugins mutate event properties directly (e.g., setting event.cancel = "reason"). The framework doesn’t know why something was cancelled — was it a hard authorization denial or soft guidance to retry differently? Multiple plugins modifying the same event can conflict silently with last-write-wins semantics.

Interventions return typed actions that the framework interprets. This enables:

  • Short-circuiting — a deny() from an authorization handler skips all remaining handlers automatically. With hooks, each plugin must independently check event.cancel before doing work.
  • Feedback accumulation — multiple handlers can return guide() and their feedback is combined into a single message to the model, rather than overwriting each other.
  • Human-in-the-loopconfirm() integrates with the SDK’s interrupt/resume system to pause for approval without the handler needing to manage interrupt lifecycle.
  • Ordered evaluation — handlers always run in registration order with well-defined precedence (deny > confirm > guide > transform > proceed).
  • Error policies — each handler declares its own failure mode via onError. A logging handler can use 'proceed' (skip on failure), while an auth handler can use 'deny' (fail closed). Hooks have no equivalent — a thrown error always propagates.
  • Hooks — Low-level event callbacks for observing and modifying agent behavior
  • Plugins — Bundle related hooks and tools into reusable modules
  • Interrupts — The interrupt/resume system that confirm() builds on