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 integrations —
create_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
| Type | Scope | Persistence | Managed Via |
|---|---|---|---|
Built-in (sonzai_) | All instances | Platform-managed | SDK capabilities, Dashboard |
| Agent-level custom | All instances | Persistent | SDK, Dashboard |
| Session-level | Per session | Temporary | SDK (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 messageSessions — 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
- Custom States walkthrough — end-to-end example that includes a
spend_energytool writing back to Custom State
Next steps
Instances
Deploy the same agent into multiple isolated contexts — workspaces, departments, regions, or tenants — each with their own independent state.
Memory-Aware Chat
Build a conversational agent that remembers everything — user preferences, past events, commitments, and emotional context — across sessions. By the end you'll know how to seed context programmatically, search memories, and inspect what the agent has learned about a user.