Skip to main content
SONZAI

Custom Tools

Define functions the LLM can call during chat — built-in platform capabilities, persistent agent-level tools, and ephemeral session tools injected at runtime.

Custom Tools let the LLM invoke functions during inference. Sonzai handles sonzai_-prefixed built-in tools automatically. Custom tools are defined by you and executed by your backend — Sonzai surfaces the call as a side effect in the SSE stream.

Using your own LLM?

If you use standalone memory mode (BYO-LLM), Sonzai exposes tool schemas you can wire into your agent framework (LangChain, Vercel AI SDK, Gemini function calling, etc.). See the Tool Integration guide for details.

What you can build with it

  • Expressive companion actions — emote, change outfit, move scene, give a gift
  • Backend integrationscreate_ticket, lookup_order, schedule_meeting
  • State mutations — tools that read or write Custom State on behalf of the agent
  • Approval-gated workflows — propose an action, your backend validates before executing
  • Context-sensitive tools — inject different tool sets per session depending on user role or screen

Quickstart

Register a session-level tool and handle its call in the next chat turn.

import { Sonzai } from "@sonzai-labs/agents";

const client = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });

// 1. Register a tool for this session
await client.agents.sessions.setTools("agent-id", "session-id", [
{
  name: "check_status",
  description: "Return the current operational status. Call when the user asks about system health.",
  parameters: { type: "object", properties: {} },
},
]);

// 2. Chat — tool calls appear in sideEffects
const toolCalls: { name: string; arguments: Record<string, unknown> }[] = [];
for await (const event of client.agents.chatStream({
agent:  "agent-id",
userId: "user-123",
messages: [{ role: "user", content: "What's the current status?" }],
})) {
process.stdout.write(event.choices?.[0]?.delta?.content ?? "");
toolCalls.push(...(event.sideEffects?.externalToolCalls ?? []));
}

// 3. Execute and return results
const results = await Promise.all(toolCalls.map(c => myBackend.run(c.name, c.arguments)));
for await (const event of client.agents.chatStream({
agent:  "agent-id",
userId: "user-123",
messages: [
  { role: "user", content: "What's the current status?" },
  { role: "tool", content: results.join("\n") },
],
})) {
process.stdout.write(event.choices?.[0]?.delta?.content ?? "");
}

Core concepts

Built-In Tools (Capabilities)

Toggle platform-managed capabilities per agent. These are enabled at agent creation or updated via the capabilities API.

sonzai_memory_recall (Always On)

Searches stored memories during inference. Auto-injected into context.

sonzai_remember_name (Toggleable)

Persists the user's name for future conversations. On by default.

sonzai_web_search (Toggleable)

Live web search via Google. On by default.

sonzai_inventory (Toggleable)

Read user resource items and join with Knowledge Base data.

// Set capabilities at agent creation
const agent = await client.agents.create({
agentId: "your-stable-uuid",  // recommended — makes creation idempotent
name: "Luna",
big5: { openness: 0.75, conscientiousness: 0.6, extraversion: 0.8,
        agreeableness: 0.7, neuroticism: 0.3 },
toolCapabilities: {
  webSearch:       true,
  rememberName:    true,
  imageGeneration: false,
  inventory:       true,
},
});

// Or update capabilities on an existing agent
await client.agents.update("agent-id", {
toolCapabilities: {
  webSearch: false,
  inventory: true,
},
});

Reserved Prefix

The sonzai_ prefix is reserved. Your custom tools must not use it — the API will reject them.

customTools in agent capabilities

AgentCapabilities includes a customTools field — a snapshot of the agent-level custom tools currently registered. Use get_capabilities() to read them, or use the dedicated list_custom_tools() / createCustomTool() methods (shown in the Full API section below) to manage them.

// Read agent capabilities — includes current custom tools
const caps = await client.agents.getCapabilities("agent-id");
console.log(caps.customTools);  // CustomToolDefinition[] | null

// Register a new agent-level custom tool
await client.agents.createCustomTool("agent-id", {
name: "lookup_order",
description: "Look up an order by ID and return its status.",
parameters: {
  type: "object",
  properties: {
    order_id: { type: "string" },
  },
  required: ["order_id"],
},
});

Tool scoping

TypeScopePersistenceManaged Via
Built-in (sonzai_)All instancesPlatform-managedSDK capabilities, Dashboard
Agent-level customAll instancesPersistentSDK, Dashboard
Session-levelPer sessionTemporarySDK (inline or setTools)

Full API

Custom Tools (Agent-Level)

Persistent tools stored with the agent and available in every chat, regardless of session or instance.

// Create a custom tool
await client.agents.createCustomTool("agent-id", {
name: "check_inventory",
description: "Check the user's current tasks and their statuses",
parameters: {
  type: "object",
  properties: {
    item_type: {
      type: "string",
      description: "Filter by category: active, pending, completed",
    },
  },
},
});

// List all custom tools
const tools = await client.agents.listCustomTools("agent-id");

// Update a tool's description or parameters
await client.agents.updateCustomTool("agent-id", "check_inventory", {
description: "Check and summarize the user's tasks by category",
});

// Delete a tool
await client.agents.deleteCustomTool("agent-id", "check_inventory");

Session-Level Tools (temporary)

Inject tools dynamically for a specific session. Session tools merge with agent-level tools — same-name session tools take precedence. Discarded when the session ends.

Option 1 — Set for an existing session

await client.agents.sessions.setTools("agent-id", "session-id", [
{
  name: "execute_action",
  description: "Execute an action from the agent's capabilities",
  parameters: {
    type: "object",
    properties: {
      action_name: { type: "string" },
      target:      { type: "string" },
    },
    required: ["action_name"],
  },
},
]);

Option 2 — Pass inline with the chat call

for await (const event of client.agents.chatStream({
agent:    "agent-id",
messages: [{ role: "user", content: "Check my tools" }],
userId:   "user-123",
toolDefinitions: [
  {
    name:        "check_inventory",
    description: "List the agent's active tools",
    parameters:  { type: "object", properties: {} },
  },
],
})) {
// handle events...
}

Handling Tool Calls

When the LLM decides to call a custom tool, it appears as a side effect in the SSE stream. Your backend executes the tool and returns the result in the next message.

1. Receive the tool call

const toolCalls: { name: string; arguments: Record<string, unknown> }[] = [];

for await (const event of client.agents.chatStream({
agent:    "agent-id",
messages: [{ role: "user", content: "What tasks do I have?" }],
userId:   "user-123",
})) {
// Stream content to the user
const content = event.choices?.[0]?.delta?.content;
if (content) process.stdout.write(content);

// Collect tool calls from side effects
const calls = event.sideEffects?.externalToolCalls ?? [];
toolCalls.push(...calls);
}

2. Execute and return results

// Execute your tool calls on your backend
const toolResults: string[] = [];
for (const call of toolCalls) {
const result = await myBackend.executeTool(call.name, call.arguments);
toolResults.push(result);
}

// Return results in the next chat message
for await (const event of client.agents.chatStream({
agent:    "agent-id",
userId:   "user-123",
messages: [
  { role: "user",  content: "What tasks do I have?" },
  { role: "tool",  content: toolResults.join("\n") },
],
})) {
process.stdout.write(event.choices?.[0]?.delta?.content ?? "");
}

In Practice

What you expose as tools differs sharply by use case — keep descriptions vivid and tightly scoped so the LLM invokes them naturally.

Tools are expressive actions. Things the character can DO in your app — emote, change outfit, move to a different scene, give a gift. Keep descriptions vivid so the LLM invokes them naturally.

await client.agents.sessions.setTools("agent-id", "session-id", [
  {
    name: "change_scene",
    description: "Move to a new location in the story. Use when the scene has run its course or a new chapter begins.",
    parameters: { type: "object", properties: { location: { type: "string" } }, required: ["location"] },
  },
]);

Don't include a handoff tool. Companions should never punt to a human — the relationship IS the product.

Combines with

Custom State — what tools often act on

Define a tool that lets the agent trigger a state change from inside a conversation. Your backend executes the tool call and calls upsert to apply the new value.

await client.agents.sessions.setTools("agent-id", "session-id", [
  {
    name: "spend_energy",
    description: "Deduct energy from the user. Call when the user takes an action that costs energy.",
    parameters: {
      type: "object",
      properties: {
        amount: { type: "number", description: "Energy to deduct (1–50)" },
      },
      required: ["amount"],
    },
  },
]);

// In your tool handler:
// 1. Receive externalToolCall { name: "spend_energy", arguments: { amount: 10 } }
// 2. Read current energy with getByKey
// 3. Upsert the new value
// 4. Return the result in the next chat message

Sessions — session-scoped vs persistent tools

Agent-level tools persist across all sessions. Session-level tools are injected at runtime and discarded when the session ends — use them when the available tool set depends on the current screen, user role, or conversation context.

Conversations — tool calls in the message stream

Tool calls appear as side effects in the SSE stream. See the Conversations page for the full event shape and streaming patterns.

Tutorials

Next steps

On this page