hooks are plain async event listeners, not middleware. The runtime proceeds with sensible
defaults when a hook is omitted, and a hook that wants to reject a run just throws — the throw is
caught and routed through the same sanitized error response as any other failure (see
Errors). All eight are wired in the current release.
beforeRun / afterRun
ctx: { userId, kind: 'chat' | 'architect', agentId?, threadId?, messages }—agentIdis only set forkind: 'chat'; the Architect has noagentIdof its own.beforeRunfires beforePOST /chat’s orPOST /architect’s stream starts. Throwing here aborts the run before any upstream call is made.result: { text, eventCount, interrupted, erroredInBand }.erroredInBandistruewhen the stream’s last event was aRUN_ERROR— that’s a normal completed-run outcome, not a thrown exception, soafterRunstill fires for it. (onErrordoes not fire for in-band errors — see below.)
onError
ctx.phase: 'auth' | 'chat' | 'architect'.- Fires on a thrown exception only: the initial request failing (auth/validation/network) or
the stream dying mid-read. It does not fire on an in-band
RUN_ERRORevent — that goes throughafterRunwithresult.erroredInBand: trueinstead, since the run technically completed rather than crashed.
beforeToolCall / afterToolCall
ctx: { userId, agentId, threadId?, toolName, toolCallId }.beforeToolCallfires on eachTOOL_CALL_STARTevent inside a chat stream.afterToolCallfires on the matchingTOOL_CALL_RESULTevent;resultis the raw tool output (string, or already-parsed JSON if the upstream event carried it that way).
onFileUpload
ctx: { userId, fileName, mimeType? } — fires after POST /files succeeds.
onThreadCreate
ctx: { userId, agentId, threadId } — fires on an explicit POST /threads, and when a
POST /chat run’s RUN_STARTED event reports a threadId that wasn’t supplied on the way in
(Persona created one implicitly for that turn). Use this if you need to know about every thread
that comes into existence, not just ones your own code explicitly created.
onMemoryWrite
ctx: { userId, agentId?, path } — fires after PUT /memory/file succeeds.
Design notes
- Hooks are side-effect listeners, not a request/response pipeline — none of them can modify the request or response, only observe it (or abort by throwing).
- A thrown hook error is treated exactly like any other unexpected error: sanitized in
mode: 'production', full detail inmode: 'development'. See Errors. - For per-user or per-action authorization finer than a capability
flag (e.g. “this user can call
search_webbut notsend_email”),beforeToolCallthrowing is the intended place to enforce it — the runtime has no built-in tool-level permission model.