Skip to Content
Polyant is open source under AGPL-3.0 — star us on GitHub.

Hooks

On this page

The Hooks section (an agent’s Behaviour group) attaches deterministic code to a conversation’s lifecycle. A hook is not a tool: the agent never decides whether it runs. The engine runs every enabled hook at the event it subscribes to, in a fixed order, on every turn.

Reading the hook list requires agent.governance:read. Creating, changing, or deleting a hook requires agent.governance:write.

The four lifecycle events

EventWhen it runs
conversation_startBefore the model is called, on the conversation’s first persisted turn only.
message_receivedBefore the model is called, on every turn.
response_generatedAfter the response exists, before it is delivered.
response_sentAfter delivery, at the end of the turn.

The two pre-model events run after the governance input gate, so a hook never observes content that governance blocked.

What a hook is made of

A hook binds one event to one hook function - a named handler registered by the engine or by an installed plugin. The catalogue of registered functions is served by GET /api/hook-functions and populates the function selector in the dialog. A hook whose function is no longer registered is flagged in the list and is skipped at runtime.

A plugin’s hook functions follow whether the organization holds that plugin, both in the catalogue and at runtime: the functions of a plugin it has not installed or been granted are not offered and are not executed. “No such hook function” and “that plugin is not granted to you” answer with the same message, so the refusal is not an existence oracle for what the installation runs.

Each hook row carries:

  • Event - one of the four above.
  • Function - the registered hook function to execute.
  • Enabled - whether the hook participates in the turn.
  • Position - integer ordering. Hooks for the same event run sequentially in ascending position.
  • Timeout - per-hook budget, between 1,000 ms and 30,000 ms (default 10,000 ms).

A hook function only receives the secrets it declared, and a hook whose required secrets are unconfigured is not executed - the misconfiguration is audited instead.

The Hooks section

The section lists the agent’s hooks grouped by event, in run order. Each row names its function and shows its timeout, its order and an enable switch. Two badges flag a hook that will not do what it says: Not available when its function is no longer registered, and Parameters missing when an enabled hook’s function declares a required key the agent has not set.

Clicking a row opens the hook in a side sheet:

  • The header names the function, with its description and the event it runs on.
  • When it runs holds the Event, Timeout (ms) and Order.
  • Parameters holds the keys the function declares, set right there; “This hook function asks for no parameters.” when it declares none. Several hooks can ask for the same key, because a key belongs to the agent: setting it in one sheet sets it for all of them. Provider credentials are not here; they are set in the agent’s Model section, or with managed services bound as connections in its Providers section.
  • One Save writes the settings and the parameters together. Closing the sheet with unsaved changes asks first.

A function declaring mutatesResponse carries a warning in the sheet and in the dialog that it can replace the response, so the turns it affects are not streamed. See Streaming caveat.

Add hook opens a dialog with the event, the function (its description appears once chosen), the timeout and the order, and, as soon as a function that declares keys is picked, its Parameters. The keys are saved first, so the hook finds them on its first run.

Managed services. Without agent.governance:write the section is read-only: no Add hook, the enable switches and the sheet’s settings are disabled, and the sheet has no Delete. The parameters follow the agent’s secret permissions instead, so a reader allowed to set the agent’s secrets can still fill them in.

Failures never block the turn

The hook runner is observe-only by contract: a load error, a handler exception, or a timeout is logged and audited, and the pipeline continues. One failing hook does not stop the hooks after it, and it does not stop the reply.

The audit record keeps the outcome, duration, and error, but never the rendered arguments - those can carry personal data.

The four control returns

A hook function may return a control signal instead of just observing. The first hook to request a given signal wins; later hooks requesting the same one are ignored.

  • injectContext - pre-model only. Adds one-shot text to this turn’s model input (truncated at 4,000 characters). Use it to hand the model a looked-up fact without giving it a tool call.
  • halt - pre-model only. Skips the model entirely and replies with the hook’s message. The persisted reply is badged as hook-authored.
  • replaceResponse - post-model. Swaps the generated text for the hook’s message.
  • regenerate - post-model. Replays the whole turn (system prompt, tools, model call) up to 5 times before the last output is delivered anyway.

replaceResponse and regenerate require the hook function to declare mutatesResponse. replaceResponse is honoured with a warning when the declaration is missing; regenerate is dropped, because a replay multiplies the turn’s cost.

On Room and webhook turns only halt and replaceResponse are honoured. Those turns call the supervisor directly, with no replay loop, so a regenerate or an injectContext returned there is dropped, and the engine logs a warning naming the hook. Nothing else says so: the execution is still recorded as a success.

Streaming caveat

replaceResponse and regenerate can only act on a response that has not left the engine yet. The engine makes sure it has not: when the agent has an enabled response_generated hook whose function declares mutatesResponse, or an enabled governance output gate, a turn on a streaming endpoint (/v1/chat/completions with stream: true, or POST /api/instances/{slug}/chat/stream) is buffered instead of streamed. The turn runs to its end, hooks included, and the final text is sent in one piece. This is what the warning in the sheet and the dialog means.

Buffering is decided by the declaration, not by what the hook returns on a given turn: a hook that declares mutatesResponse and replaces nothing still costs those turns their streaming. A hook that returns replaceResponse without declaring it does not trigger buffering, so on a streamed turn the replacement cannot change the text the client has already received. Turns that never stream (the HTTP channel’s sync-response mode, Room and webhook turns) are unaffected.

A hook’s writes to the conversation state store ride the same turn: state written by a pre-model hook is visible to the model in that turn, and the buffer is flushed when the turn commits.

Conversation-level visibility

GET /api/conversations/{conversationId}/hooks (permission conversation:read) returns the hook executions recorded for one conversation - event, function, success, duration, and any control signal requested. Use it to answer “did the hook fire on that turn, and what did it do”.

See also

  • Governance - policy gates, which run before hooks on the input path.
  • Conversations - where hook executions and conversation state are inspected.
  • Privacy - opt-out, which is enforced before hooks run.
Last updated on