Skip to content
Mayura

Data, safety and extensions

Lifecycle hooks

Run your own checks and observers at fixed points of an agent run: before model and tool calls, before output release, and at the end.

Lifecycle hooks let your code take part in every run of an agent at fixed points: before the run starts, before each model call, before each tool call, before a result is released, and when the run finishes. There are two kinds. Control hooks can stop the run and can ask the runtime to call read-only tools first, for example a policy check. Observer hooks watch without changing anything, for example to write an audit log or count outcomes. Use hooks for cross-cutting policy and telemetry; use guards to check or redact content itself.

ts
import { defineAgent, defineHook, z } from 'mayura';

const releasePolicy = defineHook({
  id: 'policy.release',
  version: '1',
  stage: 'beforeOutputRelease',
  tools: [],
  handler: event => ({ decision: event.candidate === null ? 'block' : 'continue' }),
});

const audit = defineHook({
  id: 'audit.tools',
  version: '1',
  stage: 'afterToolCall',
  mandatory: true,
  handler: async event => { await auditLog.append({ tool: event.toolId, status: event.status }); },
});

const agent = defineAgent({
  id: 'support.assistant', version: '1', instructions: 'Answer questions about orders.',
  model, tools: [], input: z.string(), output: z.string(),
  hooks: [releasePolicy, audit],
});

Hooks belong to the agent definition, so they run on every run of that agent. An agent accepts up to 16 hooks, with unique ids. Hooks of the same stage run in the order you list them.

Stages

Control stages are awaited before the step they protect. Each handler returns { decision: 'continue' } or { decision: 'block' }.

Stage Runs The event contains
beforeExecution After the input schema and input guards, before the first model call input, and media (a summary of each image or PDF: type, size, name) when there is some
beforeStep Before each reasoning step step
beforeModelCall Before each primary model call modelId, purpose, and request with messages, tools, maxOutputTokens, and media (summaries by message index) when there is some; hooks never see media bytes
beforeToolCall Before each tool call the model proposed proposal with callId, toolId, input
beforeDelegate Before a child agent starts (on the parent's hooks) childRunId, childAgentId, input
beforeOutputRelease After output guards, before a tool result enters the conversation or the final answer is released source (agent or tool), callId, toolId, candidate

Observer stages see metadata only, never content, and return nothing.

Stage Runs The event contains
afterStep After each step step, result (tool_calls, final or stopped)
afterModelCall After each primary model call step, modelId, response, toolCalls
afterToolCall After each tool call callId, toolId, status, execution, disclosure
afterDelegate After a child agent finishes (on the parent's hooks) childRunId, childAgentId, status
onViolation When a guard, hook or permission check refuses something source, boundary, code
afterExecution, onError, onCancel, onBlocked Once at the end, depending on the outcome status, error.code
onFinally Once at the very end, always status, error.code

Every handler also receives a context with the run's runId, rootId, parentId, agentId, verified scope, the hook's id and version, the current step (null before the first step) and an abort signal. The event and context are frozen.

Decisions

A control hook that returns { decision: 'block' } ends the run with status blocked and code GUARD_BLOCKED. A handler that throws, times out or returns anything else ends it blocked with GUARD_UNAVAILABLE. Hooks cannot rewrite what they see: the model request, the tool proposal and the candidate output stay exactly as they are. To change content, use a guard with a rewrite verdict.

The beforeToolCall event holds the model's raw proposal, before the tool's input schema transforms it.

Actions: calling a tool from a hook

A control hook can ask the runtime to run tools before it continues. List the tools the hook may call in tools, and return them as actions:

ts
import { defineHook, defineTool, z } from 'mayura';

const assertProjectReadable = defineTool({
  id: 'policy.assert-readable', version: '1', description: 'Fails unless the path is inside the project.',
  input: z.string(), output: z.literal(true), effects: 'none', capabilities: [],
  execute: path => {
    if (!path.startsWith('project/')) throw new Error('Outside the project.');
    return true as const;
  },
});

const proposalPolicy = defineHook({
  id: 'policy.proposal',
  version: '1',
  stage: 'beforeToolCall',
  tools: [assertProjectReadable],
  handler: event => ({
    decision: 'continue',
    actions: [{ toolId: assertProjectReadable.id, input: event.proposal.input }],
  }),
});
  • Hook tools must have effects: 'none' or 'read'. Agent wrappers and child workflows are not allowed.
  • The tools are private to the hook. The model does not see them, and listing them grants nothing: the runtime still needs tool:<id>, the tool's capabilities and effect:read for a read tool.
  • Actions run one after another through the normal tool path: same scope, budget, tool guards and the agent's output guards. If one fails, the run stops with that tool's outcome.
  • The action's result is not passed back to the handler. Write the policy as an assertion: a tool that fails (or whose guard blocks) when the check fails. A tool that returns false successfully does not block anything.
  • Actions are not a transaction. A later failure does not undo an earlier action.

Observers and mandatory

An observer that throws or times out is recorded as a failed hook, and the run continues. Set mandatory: true when the observation must happen, such as an audit log: if a mandatory observer fails, the run ends blocked with GUARD_UNAVAILABLE and a successful answer is withheld. The end-of-run stages onViolation, onError, onCancel, onBlocked and onFinally never change the outcome, even when mandatory. A mandatory afterExecution observer can withhold a successful answer.

Limits

Option Default Maximum Applies to
timeoutMs 5,000 30,000 Each hook call, including its actions
maxActions 4 8 Control hooks, per call
maxResultBytes 65,536 1,048,576 Control hooks: the decision and all action inputs
tools none 32 Control hooks (required, [] if none)

The runtime also counts every hook call against its maxHookCalls limit (default 128, maximum 4,096). Hook calls do not use model steps or cost anything themselves; the tools they call are charged like any other tool call.

Hooks outside agent runs

Some lifecycle points belong to other parts of Mayura and take plain callbacks rather than defineHook:

Where Option Stages
Memory hooks on createNativeMemory, createMemoryStore beforeMemoryWrite (control), afterMemoryWrite (observer)
Context assembly hooks on assembleContext beforeContextBuild, afterContextBuild (both control)
Retries onRetry on retry onRetry (control)
Durable workflows createWorkflowHookRelay from mayura/workflows onWait, onResume, onApprovalRequested, onApprovalResolved, and the end stages
ts
import { createNativeMemory } from 'mayura/memory';

const memory = createNativeMemory({
  store, scope, permissions: { allow: ['memory:read', 'memory:write'] },
  hooks: {
    beforeMemoryWrite: event => ({ decision: event.candidate?.sensitivity === 'restricted' ? 'block' : 'continue' }),
    afterMemoryWrite: event => { console.log('memory', event.operation, event.id, event.version); },
  },
});

A blocked memory write stores nothing. If afterMemoryWrite fails, the call rejects with a message saying the write did commit.

The workflow hook relay reads a run's stored event log and delivers callbacks from a durable cursor. Call relay.deliver(runId) from a worker loop. Delivery is at least once, so deduplicate on event.eventId.

Events

Runs with hooks emit hook.started and hook.completed events with the hook id, stage, step and completion status. They never include content, action inputs or error messages. See observability.

Good to know

  • Handlers are your code in your process. They are trusted: hooks are not a sandbox.
  • A timed-out handler cannot dispatch the actions it returns later, but its promise keeps running until it settles.
  • beforeModelCall sees the messages and tools sent to the model, not the agent instructions or the provider's private continuation state.
  • The final beforeOutputRelease runs before the run waits for required child agents; a child that fails later can still fail the parent.