# Tool execution model

Tools in Omnia don't all execute in the same place. Where a tool runs
determines whether you can see its calls in the WebSocket stream. Two
questions follow when a tool misbehaves: where did the call run, and was the
model offered the tool at all?

## Where each tool runs

Client tools run in the browser. Every other tool runs in the runtime
container. The facade sits between the two and forwards only the calls the
browser has to execute.

```mermaid
flowchart LR
    Browser <-->|"WebSocket: tool_call, tool_result"| Facade["Facade (cmd/agent)"]
    Facade <-->|"gRPC Converse"| Runtime["Runtime (cmd/runtime)"]
    Runtime -->|"http, grpc, mcp, openapi handlers"| Backends["Tool backends"]
    Runtime -->|"platform handlers"| Operator["Operator REST API"]
    Runtime -->|"tool_calls records"| SessionAPI["Session API"]
```

### Client tools run in the browser

A handler with `type: client` (endpoint `client://browser`) is forwarded to
the connected browser over the WebSocket facade as a `tool_call` message. The
browser executes the tool locally and sends back a `tool_result` message.

Client tools are the only tools visible in the WebSocket stream. If you
inspect WebSocket traffic to debug a tool call and don't see it, the tool most
likely isn't a client tool.

The facade forwards only calls the runtime marks as client-executed, so the
arguments and results of server-side tools never reach the browser. A
`tool_call` message has no field that says where the tool runs, because every
tool call on the WebSocket is client-side. See the
[WebSocket protocol](/reference/platform/websocket-protocol/) for the message
shapes and [WebSocket message flows](/explanation/platform/websocket-flows/)
for the order of messages in a tool call.

### Server and platform tools run in the runtime

Everything else executes in the runtime container, server-side, and is never
forwarded over the WebSocket:

- Server tools: `http`, `grpc`, `mcp` and `openapi` handlers.
- Platform tools: capabilities registered through the PromptKit SDK, such as
  memory and skills.

## Declaring a tool and executing it

A ToolRegistry binds a tool name to an executor. It does not decide what the
model is offered. A PromptPack prompt does that, through its `tools` list.

The runtime reads the union of the `tools` lists across the pack's prompts and
compares it with the tools the ToolRegistry serves. A tool the registry serves
but no prompt declares is never offered to the model. A tool a prompt declares
but the registry doesn't serve is never offered either. In both cases the
model is offered fewer tools than you expect, and nothing fails: the only
symptom is an answer that doesn't use the tool.

The runtime logs each mismatch at startup, naming the tools and the fix: add
the name to the prompt's `tools` list and republish the pack, or expose the
tool from the attached ToolRegistry. Tools in the `memory__`, `skill__`, `a2a__`, `workflow__` and
`mcp__` namespaces are the exception: PromptKit offers them without a `tools`
entry, so the runtime doesn't flag their absence from the pack.

The split keeps the pack author in charge of what a prompt may do. If the
registry's contents were merged into every prompt's allow-list, attaching a
registry would silently widen every prompt, and the pack the runtime executes
would differ from the pack you published.

## Debugging: use the right signal

The runtime records every tool call to session-api, and the session's
`/tool-calls` endpoint returns them. That covers client and server tools
alike, so it is the signal that always exists.

- A client tool also appears on the WebSocket stream.
- A server or platform tool never appears on the WebSocket. Read the runtime
  logs or the session's `/tool-calls` endpoint.
- A tool the model never calls may not have been offered. Check the runtime's
  startup log for tools the pack doesn't declare.

Because the `tool_calls` row is the only record of a server-side call, a
policy denial of one is visible there and not on the WebSocket.

## Trade-offs

Forwarding only client calls keeps tool arguments and results that
the browser doesn't need off the client data-plane channel. The cost is that a live view of the
conversation can't render those calls from the stream. It learns that a call
was recorded from the session activity stream, then reads the row through the
authorised session-api.

Making the pack the source of truth for offered tools costs one extra step
when you add a tool, because you edit the registry and the pack. The
alternative, offering whatever the registry serves, is shorter to author and
gives up the pack as the record of what a prompt can do.

## See also

- [Build a tool backend](/how-to/tools/build-a-tool-backend/)
- [Test a tool before wiring it to an agent](/how-to/tools/test-tools/)
- [Client-side tools](/how-to/tools/client-tools/)
- [WebSocket protocol](/reference/platform/websocket-protocol/)