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" }
        }
      }
    ]
  }'
FieldPurpose
nameTool name the LLM calls. Use snake_case.
kind"http" for REST endpoints.
descriptionWhen the LLM should call it. Write this for the LLM, not for humans — be explicit about triggers.
input_schemaJSON Schema for the arguments the LLM passes.
config.urlAbsolute URL of your endpoint.
config.methodHTTP method, default POST.
config.headersStatic 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();
}
BehaviourWhat the agent does
2xx with JSON bodyReads the JSON; LLM uses it in its next utterance
Non-2xxLLM 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

RiskMitigation
Anyone who knows the URL can call itRequire X-Oshara-Secret (set in config.headers); reject mismatches
Replay / IP spoofingLimit to Oshara’s outbound IPs (contact support for the list)
Sensitive data leakDon’t return fields the agent shouldn’t say out loud — strip from the response
Long-running callsReturn 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:
BadGood
"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);
  }
});