Skip to content

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.

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.

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 lists them all.

  • 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 creates them, together with the dashboard sign-in we use in step 5.

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:

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:

Terminal window
kubectl apply -f calculator.yaml

The output lists the objects:

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

Wait for the service to start:

Terminal window
kubectl rollout status deployment/calculator-service

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

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

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:

Terminal window
kubectl apply -f toolregistry.yaml

The output confirms the object:

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

Read the registry’s status:

Terminal window
kubectl get toolregistry agent-tools -o yaml

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

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.

Step 4: connect the registry to your agent

Section titled “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:

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

The output shows the change:

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

Read the field back:

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

The command prints agent-tools.

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:

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
}
}
}
}
Terminal window
kubectl apply -f configmap.yaml

The output shows the change:

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:

Terminal window
kubectl rollout status deployment/my-assistant

Then check the pack condition:

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

The command prints True.

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

Terminal window
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:

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 shows how to read it back.