What a tool is
A tool is an endpoint the LLM can call during a call. You register it on the character; the agent worker proxies calls to your URL when the LLM decides it’s needed. Examples:
- Look up an order status by ID
- Check inventory before quoting a price
- Create a support ticket
- Fetch the user’s last invoice
The agent reads the response and speaks the answer naturally — all in real time.
Registering an HTTP tool
curl -X PATCH https://api.oshara.ai/api/ai-characters/support-bot/ \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"tools": [
{
"name": "ticket_lookup",
"kind": "http",
"description": "Look up a support ticket by ID. Use when the user mentions a ticket number like TICKET-123.",
"input_schema": {
"type": "object",
"properties": {
"ticket_id": { "type": "string", "description": "Ticket ID, e.g. TICKET-123" }
},
"required": ["ticket_id"]
},
"config": {
"url": "https://yoursite.com/api/oshara/ticket-lookup",
"method": "POST",
"headers": { "X-Oshara-Secret": "your-shared-secret" }
}
}
]
}'| Field | Purpose |
|---|---|
name | Tool name the LLM calls. Use snake_case. |
kind | "http" for REST endpoints. |
description | When the LLM should call it. Write this for the LLM, not for humans — be explicit about triggers. |
input_schema | JSON Schema for the arguments the LLM passes. |
config.url | Absolute URL of your endpoint. |
config.method | HTTP method, default POST. |
config.headers | Static headers (auth tokens, secrets) sent on every call. |
Your endpoint contract
The worker sends the LLM’s arguments as the JSON request body. Your endpoint returns JSON; the body is fed back to the LLM as the tool result.
// Express
app.post("/api/oshara/ticket-lookup", verifySecret, async (req, res) => {
const { ticket_id } = req.body;
const ticket = await db.tickets.findById(ticket_id);
if (!ticket) return res.status(404).json({ error: "Ticket not found" });
res.json({
id: ticket.id,
status: ticket.status,
subject: ticket.subject,
priority: ticket.priority,
updated: ticket.updatedAt,
});
});
function verifySecret(req, res, next) {
if (req.header("X-Oshara-Secret") !== process.env.OSHARA_SECRET)
return res.status(401).end();
next();
}| Behaviour | What the agent does |
|---|---|
2xx with JSON body | Reads the JSON; LLM uses it in its next utterance |
Non-2xx | LLM is told the call failed; can retry or apologise |
| Timeout (>10s) | LLM is told the call timed out |
Multiple tools on one character
"tools": [
{
"name": "ticket_lookup",
"kind": "http",
"description": "Look up an existing ticket by ID.",
"input_schema": { "type": "object", "properties": { "ticket_id": { "type": "string" } }, "required": ["ticket_id"] },
"config": { "url": "https://yoursite.com/api/oshara/ticket-lookup", "method": "POST" }
},
{
"name": "create_ticket",
"kind": "http",
"description": "Create a new support ticket when the user reports an issue.",
"input_schema": {
"type": "object",
"properties": {
"subject": { "type": "string" },
"priority": { "type": "string", "enum": ["low", "normal", "high"] },
"details": { "type": "string" }
},
"required": ["subject", "details"]
},
"config": { "url": "https://yoursite.com/api/oshara/tickets", "method": "POST" }
}
]The LLM picks the right tool based on the description fields.
Passing session context to your endpoint
The worker adds the session’s metadata and session_id into the request body by default, so your endpoint can act on behalf of the right user:
{
"ticket_id": "TICKET-456",
"_session": {
"session_id": "sess_a1b2c3",
"metadata": {
"user_id": "u_42",
"user_email": "alice@acme.com"
}
}
}Use _session.metadata.user_id server-side to enforce that the looked-up ticket belongs to that user.
Securing your endpoint
| Risk | Mitigation |
|---|---|
| Anyone who knows the URL can call it | Require X-Oshara-Secret (set in config.headers); reject mismatches |
| Replay / IP spoofing | Limit to Oshara’s outbound IPs (contact support for the list) |
| Sensitive data leak | Don’t return fields the agent shouldn’t say out loud — strip from the response |
| Long-running calls | Return fast (<3s ideal) — if the work is slow, return a job ID and have the agent follow up |
Writing a good description
The LLM picks tools based on description. Write triggers, not features:
| Bad | Good |
|---|---|
"Returns ticket info" | "Look up a support ticket by its ID. Use when the user mentions a ticket number like TICKET-XXX or asks 'what's happening with my ticket'." |
"Creates a lead" | "Create a new sales lead. Use when the user expresses interest in pricing, demos, or scheduling a call — but only after collecting at least their name and email." |
Testing a tool
# Simulate the worker calling your endpoint
curl -X POST https://yoursite.com/api/oshara/ticket-lookup \
-H "Content-Type: application/json" \
-H "X-Oshara-Secret: your-shared-secret" \
-d '{"ticket_id": "TICKET-456"}'Then start a session and ask: “what’s the status of ticket TICKET-456?” — the agent should call the tool and read the response.
Client-event tools (no HTTP, just data channel)
Some tools don’t need to call your backend — they should fire a message to the browser instead (open a modal, navigate, scroll to an element). Use kind: "client_event":
{
"name": "show_pricing_modal",
"kind": "client_event",
"description": "Open the pricing comparison modal on the page.",
"input_schema": {
"type": "object",
"properties": { "plan": { "type": "string", "enum": ["pro", "enterprise"] } }
},
"config": { "topic": "ui.show_pricing" }
}In the browser:
room.on(RoomEvent.DataReceived, (payload, _p, _k, topic) => {
if (topic === "ui.show_pricing") {
const { plan } = JSON.parse(new TextDecoder().decode(payload));
openPricingModal(plan);
}
});