# Adding tools to agents

In this tutorial we give an agent a calculator tool. We deploy a small HTTP service, register it in a ToolRegistry, and attach the registry to an AgentRuntime.

## What you will build

The agent calls the calculator service through a ToolRegistry handler. The runtime reads the handler from the ToolRegistry and makes the call, not the browser.

```mermaid
flowchart LR
  client[Dashboard console] --> facade[AgentRuntime facade]
  facade --> runtime[AgentRuntime runtime]
  runtime -->|reads handlers| registry[ToolRegistry agent-tools]
  runtime -->|POST /calculate| svc[Service calculator]
```

The ToolRegistry supports six handler types: `http`, `grpc`, `mcp`, `openapi`, `client` and `platform`. We use `http`. The [ToolRegistry reference](/reference/core/toolregistry/) lists them all.

## Prerequisites

- A running Omnia install with an AgentRuntime named `my-assistant`, a PromptPack named `assistant-pack` and a Provider named `my-provider`. The [getting started tutorial](/tutorials/getting-started/) creates them, together with the dashboard sign-in we use in step 5.

## Step 1: create a tool service

The calculator is a short Python service that reads `{"expression": "..."}` from a `POST /calculate` body and replies with `{"result": <number>}`. We keep its code in a ConfigMap and run it on the public `python:3.12-alpine` image, together with a Service in front of it:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: calculator-code
  namespace: default
data:
  server.py: |
    import ast
    import json
    import operator
    from http.server import BaseHTTPRequestHandler, HTTPServer

    OPS = {ast.Add: operator.add, ast.Sub: operator.sub,
           ast.Mult: operator.mul, ast.Div: operator.truediv}

    def evaluate(node):
        if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
            return node.value
        if isinstance(node, ast.BinOp) and type(node.op) in OPS:
            return OPS[type(node.op)](evaluate(node.left), evaluate(node.right))
        raise ValueError("unsupported expression")

    class Handler(BaseHTTPRequestHandler):
        def do_POST(self):
            if self.path != "/calculate":
                self.send_error(404)
                return
            body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
            try:
                tree = ast.parse(body["expression"], mode="eval")
                out, code = {"result": evaluate(tree.body)}, 200
            except Exception as err:
                out, code = {"error": str(err)}, 400
            data = json.dumps(out).encode()
            self.send_response(code)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(data)))
            self.end_headers()
            self.wfile.write(data)

    HTTPServer(("", 8080), Handler).serve_forever()
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: calculator-service
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: calculator
  template:
    metadata:
      labels:
        app: calculator
    spec:
      containers:
        - name: calculator
          image: python:3.12-alpine
          command: ["python", "/app/server.py"]
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: code
              mountPath: /app
      volumes:
        - name: code
          configMap:
            name: calculator-code
---
apiVersion: v1
kind: Service
metadata:
  name: calculator
  namespace: default
spec:
  selector:
    app: calculator
  ports:
    - port: 80
      targetPort: 8080
```

Save the manifests as `calculator.yaml` and apply them:

```bash
kubectl apply -f calculator.yaml
```

The output lists the objects:

```text
configmap/calculator-code created
deployment.apps/calculator-service created
service/calculator created
```

Wait for the service to start:

```bash
kubectl rollout status deployment/calculator-service
```

The command ends with `deployment "calculator-service" successfully rolled out`.

## Step 2: create a ToolRegistry

Create a ToolRegistry with an HTTP handler that points at the service:

```yaml
apiVersion: omnia.altairalabs.ai/v1alpha1
kind: ToolRegistry
metadata:
  name: agent-tools
  namespace: default
spec:
  handlers:
    - name: calculator
      type: http
      httpConfig:
        endpoint: "http://calculator.default.svc.cluster.local:80/calculate"
        method: POST
        contentType: application/json
      tool:
        name: calculate
        description: "Perform mathematical calculations"
        inputSchema:
          type: object
          properties:
            expression:
              type: string
              description: "Mathematical expression to evaluate"
          required: [expression]
      timeout: "10s"
```

Apply it:

```bash
kubectl apply -f toolregistry.yaml
```

The output confirms the object:

```text
toolregistry.omnia.altairalabs.ai/agent-tools created
```

## Step 3: check tool discovery

Read the registry's status:

```bash
kubectl get toolregistry agent-tools -o yaml
```

The status lists the tool and reports the registry as `Ready`:

```yaml
status:
  phase: Ready
  discoveredToolsCount: 1
  discoveredTools:
    - handlerName: calculator
      name: calculate
      status: Available
      endpoint: http://calculator.default.svc.cluster.local:80/calculate
  conditions:
    - type: HandlersValid
      status: "True"
    - type: ToolsDiscovered
      status: "True"
```

The `conditions` list also holds `Ready`, `Degraded` and `PolicyBound`. We show only the two that matter here.

:::note[`Ready` means the config is valid, not that the backend is reachable]
By default the controller does not probe the tool endpoint. It marks the tool `Available` when the handler config validates and the endpoint resolves. A `Ready` ToolRegistry can still point at a service that is down. Set `spec.probe.enabled` to make the controller TCP-dial each endpoint.
:::

## Step 4: connect the registry to your agent

We add `toolRegistryRef` to the AgentRuntime. The rest of the manifest stays as in the getting started tutorial, so a patch changes only that field:

```bash
kubectl patch agentruntime my-assistant --type merge \
  -p '{"spec":{"toolRegistryRef":{"name":"agent-tools"}}}'
```

The output shows the change:

```text
agentruntime.omnia.altairalabs.ai/my-assistant patched
```

Read the field back:

```bash
kubectl get agentruntime my-assistant -o jsonpath='{.spec.toolRegistryRef.name}'
```

The command prints `agent-tools`.

## Step 5: declare the tool in the pack

The runtime offers the model only the tools that a prompt in the PromptPack lists in `tools`. The registry executes the tool, and the pack decides whether the model may call it. Until the pack lists `calculate`, the AgentRuntime reports `PackContentValid` as `False` and the model never sees the calculator.

We add `"tools": ["calculate"]` to the `main` prompt in the `assistant-prompts` ConfigMap from the getting started tutorial. Replace `configmap.yaml` with the following and apply it:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: assistant-prompts
  namespace: default
data:
  pack.json: |
    {
      "$schema": "https://promptpack.org/schema/latest/promptpack.schema.json",
      "id": "assistant",
      "name": "Assistant",
      "version": "1.0.0",
      "template_engine": {
        "version": "v1",
        "syntax": "{{variable}}"
      },
      "prompts": {
        "main": {
          "id": "main",
          "name": "Main Assistant",
          "version": "1.0.0",
          "system_template": "You are a helpful AI assistant. Be concise and accurate in your responses. Always be polite and professional.",
          "tools": ["calculate"],
          "parameters": {
            "temperature": 0.7,
            "max_tokens": 4096
          }
        }
      }
    }
```

```bash
kubectl apply -f configmap.yaml
```

The output shows the change:

```text
configmap/assistant-prompts configured
```

The PromptPack controller re-reads the ConfigMap and records a new content digest. The operator puts that digest in the agent pod's environment, so the pod rolls. Wait for the new pod:

```bash
kubectl rollout status deployment/my-assistant
```

Then check the pack condition:

```bash
kubectl get agentruntime my-assistant \
  -o jsonpath='{.status.conditions[?(@.type=="PackContentValid")].status}'
```

The command prints `True`.

## Step 6: test tool invocation

We chat through the dashboard console, as in the getting started tutorial. Port-forward the dashboard:

```bash
kubectl port-forward -n omnia-system svc/omnia-dashboard 3000:3000
```

Browse to `http://localhost:3000` and sign in as `admin` with the password `tutorial-admin-pw`. Open **Agents**, open `my-assistant` and select **Console**. Type a message that needs the calculator and press Enter:

```text
What is 25 * 4?
```

The agent replies with a sentence that contains the result, `100`. The wording varies from run to run.

The console shows the reply and no `tool_call` or `tool_result` message, because the runtime executes `http` tools server-side and never forwards them over the WebSocket. Only `client` tools appear there. The runtime records the call in session-api, and the [tool execution model](/explanation/agents/tool-execution-model/) shows how to read it back.

## Next steps

- [ToolRegistry reference](/reference/core/toolregistry/): every handler type and field.
- [Connect MCP servers](/how-to/tools/connect-mcp-servers/) and [connect OpenAPI services](/how-to/tools/connect-openapi-services/): self-describing handlers, and how to combine handler types in one registry.
- [Build a tool backend](/how-to/tools/build-a-tool-backend/): the HTTP and gRPC contracts your own backend must implement.
- [Test a tool before wiring it to an agent](/how-to/tools/test-tools/): exercise a handler from the dashboard.
- [Authenticate tools](/how-to/tools/authenticate-tools/): bearer and basic secrets, projected ServiceAccount tokens, and the secret-handling model.
- [Advanced HTTP tools](/how-to/tools/advanced-http-tools/): URL templates, static injection, request and response mapping, redaction, and retry policies.
- [Client-side tools](/how-to/tools/client-tools/): browser tools with user consent.
- [Observability](/how-to/observability/setup-observability/): monitor tool calls.