> ## Documentation Index
> Fetch the complete documentation index at: https://unmute.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Unmute compiles to exactly four targets. Pipecat and LiveKit are code targets: compile writes a Python project you run. SLNG is a hosted target: compile writes a deployment body and SLNG runs the agent, so it has no `unmute dev`. Twilio compiles to a small Python app that Twilio ConversationRelay calls; you host it, and it has no `unmute dev` either. Those four are the only values `provider` accepts in `targets.yaml`. Deepgram and ElevenLabs appear in these docs as model vendors, which is not the same thing as a target, and `slng` is both.
> The Go structs in `internal/spec` and `internal/ir` are the schema truth. Check a field against them, or run `unmute validate`, rather than against what you remember.

# Hooks

> Run your code before, around and after each prompt, model request and tool call, and on call events.

Hooks run your code before, around and after each prompt, model request and tool call, and on the call's events. You write them once, and they run the same way on LiveKit and Pipecat.

## Add hooks to a call

Build one `Hooks` at import. Build one `CallHooks` per call, and pass it to the agent as `call=`.

<CodeGroup>
  ```python LiveKit theme={null}
  from unmute_core.call import CallHooks
  from unmute_core.hooks import Hooks
  from unmute_core.livekit import UnmuteAgent

  hooks = Hooks()


  async def entrypoint(ctx: JobContext) -> None:
      call = CallHooks(state, hooks=hooks)
      agent = UnmuteAgent(spec, call=call)
  ```

  ```python Pipecat theme={null}
  from unmute_core.call import CallHooks
  from unmute_core.hooks import Hooks
  from unmute_core.pipecat import UnmuteFlows

  hooks = Hooks()


  async def bot(runner_args: RunnerArguments) -> None:
      call = CallHooks(state, hooks=hooks)
      flows = UnmuteFlows(spec, llm=llm, context=context, aggregators=aggregators, call=call)
  ```
</CodeGroup>

1. Many calls may share one `Hooks`. It holds only your functions.
2. A `CallHooks` binds to one call. Build a new one for each call.
3. A hook may be sync or async, except a `prompt` hook, which must be a plain function, and a `wrap_*` hook, which must be async.

## Add a line after every prompt

```python theme={null}
from unmute_core.hooks import PromptSuffix

hooks.add(PromptSuffix("/no_think"))
hooks.add(PromptSuffix("/no_think", model="qwen/qwen3-32b"))
```

1. The suffix goes after the agent's prompt, every task step's, the finish checks' and the summaries', after a blank line.
2. With `model=`, only prompts that model reads get it.
3. It refuses blank text, more than 512 characters, and a `{{...}}` placeholder.

## Change a model request

```python theme={null}
from dataclasses import replace

from unmute_core.model import ModelRequest


@hooks.before_model
def calm(request: ModelRequest) -> ModelRequest | None:
    if "nemotron" not in request.model:
        return None
    return replace(request, params=request.params | {"temperature": 0})
```

1. `request.model` names the model that answers, like `gpt-5.6-luna`. Return None to keep the request as it is.
2. `request.params` are extra body fields for this request only.
3. `wrap_model` hooks run around the request, and `after_model` hooks hear the answer.

## Refuse or change a tool call

```python theme={null}
from unmute_core.hooks import Refuse, ToolRun


@hooks.before_tool
def no_refunds_at_night(run: ToolRun) -> Refuse | None:
    if run.name == "refund" and closed_now():
        return Refuse("Refunds open at 9.")
    return None
```

1. `Refuse(message)` stops the call. The tool never runs, and the model reads the message.
2. Return a changed `ToolRun` to change the arguments. They are checked against the schema again.
3. `wrap_tool` hooks run around the tool, as `async (run, handler)`. Call `await handler(run)` to run it.

## Run code when a tool ends

```python theme={null}
@hooks.after_tool
def audit_refund(run: ToolRun, result: object) -> None:
    if run.name == "refund":
        audit.write(run.arguments, result)
```

1. Return None to keep the result. Return something else to replace what the model reads.
2. An `after_tool` hook that raises is logged, and the model reads the tool's own result.

## Watch call events

```python theme={null}
from unmute_core.call import CallEnd


@hooks.on("tool.*")
def log_tool(event: str, data: dict) -> None:
    logger.info("%s %s", event, data["name"])


@hooks.on("call.ended")
async def report(event: str, end: CallEnd) -> None:
    await crm.save(end.reason, end.values)
```

1. A pattern may hold `*`, like `task.*`. A pattern that matches no event raises `ValueError`.
2. Observers only watch. One that raises is only logged.
3. The end of the call waits for `call.ended` observers, up to `end_timeout_s`.

## Where the model hooks run

The `prompt`, `before_model`, `wrap_model` and `after_model` hooks run only where the library sends the model request. The tool hooks and the event hooks do not depend on this.

| Framework | The hooks run | They do not run |
| - | - | - |
| LiveKit | In `UnmuteAgent`. In a plain `Agent` whose `llm_node` calls `filled_llm` or `traced_llm`. With a `ModelCallLLM`. | In a plain `Agent` that keeps LiveKit's default `llm_node` with a plugin. |
| Pipecat | In `UnmuteFlows(call=call)`. In a plain pipeline with `llm=` passed to `attach_call` or `open_call`. | In a plain pipeline with no `llm=` and no `UnmuteFlows`. |

When a call has model hooks and no request goes through the library, the adapter logs one warning that says what to change. `wrap_model` reaches only a request that a `ModelCall` answers.

## Run tool hooks on a plain agent

`UnmuteAgent` and `UnmuteFlows` pass the call's hooks to every tool. On a plain agent, pass them yourself.

<CodeGroup>
  ```python LiveKit theme={null}
  from unmute_core.livekit import function_tools

  agent = Agent(instructions=PROMPT, tools=function_tools(registry, state, hooks=call))
  ```

  ```python Pipecat theme={null}
  from unmute_core.pipecat import tools_schema

  tools = tools_schema(registry, state, hooks=call)
  ```
</CodeGroup>

1. `hooks=call` runs the tool hooks, and fires the `tool.*` events for your tools.
2. Framework tools skip the tool hooks: end call, handoffs and `native_tools`. So do prefetch lookups.

## Where to go next

<CardGroup cols={2}>
  <Card title="Hooks reference" icon="book" href="/core/reference/hooks">
    Every point, event, rule, argument and error.
  </Card>

  <Card title="The call's life" icon="phone-call" href="/core/build/call-lifecycle">
    `CallHooks`, tools on events, the failure line and the end.
  </Card>

  <Card title="Tools" icon="wrench" href="/core/build/tools">
    The tools your hooks run around.
  </Card>

  <Card title="Model call" icon="brain" href="/core/models/model-call">
    The `ModelCall` that `wrap_model` reaches.
  </Card>

  <Card title="Hooks in agent.yaml" icon="file-code" href="/reference/agent-yaml">
    Where Unmute declares `prompt_suffix`.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.