States, Tools & User Knowledge
Attach state to agents, define custom tools the LLM can call, and prime users with knowledge before their first conversation.
Custom States
Flexible key-value storage injected into the LLM context at chat time. Use it to pass environment state, task progress, resources, or any application data directly into every AI response.
Scoping Model
Global State
Per InstanceShared across all users in an instance. Use for environment state, configuration, agent status, global events.
Per-User State
Per Instance + UserScoped to one user in an instance. Use for assigned tasks, workflow progress, user preferences, active tools.
Instances
All states are scoped to an instanceId — one deployment context of your agent (e.g. a workspace or environment). Omit instanceId to use the default instance. See Instances for managing multiple contexts.
Create a State
import { Sonzai } from "@sonzai-labs/agents";
const client = new Sonzai({ apiKey: "sk-..." });
// Global state (shared across all users)
await client.agents.customStates.create("agent-id", {
key: "current_status",
value: "Processing requests",
scope: "global",
contentType: "text",
instanceId: "workspace-1", // omit to use default instance
});
// Per-user state
await client.agents.customStates.create("agent-id", {
key: "assigned_tasks",
value: { reports: 1, reviews: 3 },
scope: "user",
contentType: "json",
userId: "user-123",
instanceId: "workspace-1",
});Upsert (Create or Update)
upsertcreates a state if the key doesn't exist or updates the value if it does. Idempotent — safe to call on every update cycle.
await client.agents.customStates.upsert("agent-id", {
key: "workflow_stage",
value: "review_started",
scope: "user",
userId: "user-123",
});Get by Key
Retrieve a specific state by its composite key.
const state = await client.agents.customStates.getByKey("agent-id", {
key: "assigned_tasks",
scope: "user",
userId: "user-123",
});
console.log(state.value); // { reports: 1, reviews: 3 }
console.log(state.updatedAt); // ISO timestampList States
// All global states for an instance
const globals = await client.agents.customStates.list("agent-id", {
scope: "global",
instanceId: "workspace-1",
});
// All per-user states for a specific user
const userStates = await client.agents.customStates.list("agent-id", {
scope: "user",
userId: "user-123",
});Delete by Key
await client.agents.customStates.deleteByKey("agent-id", {
key: "assigned_tasks",
scope: "user",
userId: "user-123",
});Tools
Tools let the LLM call functions during inference. Sonzai handles sonzai_-prefixed built-in tools. Custom tools are defined by you and executed by your backend — Sonzai surfaces the call as a side effect.
Built-In Tools (Capabilities)
Toggle platform-managed capabilities per agent. These are enabled at agent creation or updated via the capabilities API.
sonzai_memory_recallAlways OnSearches stored memories during inference. Auto-injected into context.
sonzai_remember_nameToggleablePersists the user's name for future conversations. On by default.
sonzai_web_searchToggleableLive web search via Google. On by default.
sonzai_inventoryToggleableRead 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.
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 ?? "");
}Tool Scoping Summary
| 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) |
User-Scoped Knowledge (Priming)
Seed what an agent knows about a user before their first conversation. Use this for CRM data, user profiles, previous purchase history, or any user-specific knowledge that should be available from day one.
Prime a User
primeUser seeds facts about a user asynchronously. Returns a job ID you can poll to check when extraction is complete.
const job = await client.agents.priming.primeUser(
"agent-id",
"user-123",
{
display_name: "Jane Smith",
metadata: {
company: "Acme Corp",
title: "Product Manager",
email: "jane@acme.com",
custom: { tier: "premium", region: "us-west" },
},
content: [
{
type: "text",
content: "Jane joined in 2024 and prefers concise answers. She focuses on mobile growth.",
},
],
source: "crm_import",
},
);
// Check when priming is complete
const status = await client.agents.priming.getPrimeStatus(
"agent-id", "user-123", job.jobId
);
console.log(status.status); // "pending" | "processing" | "completed" | "failed"Get & Update User Metadata
// Get stored metadata for a user
const meta = await client.agents.priming.getMetadata("agent-id", "user-123");
console.log(meta.displayName, meta.company, meta.customFields);
// Update metadata (partial update — unspecified fields are preserved)
await client.agents.priming.updateMetadata("agent-id", "user-123", {
custom: { tier: "enterprise", lastContact: "2026-03-28" },
});Batch Import (Multiple Users)
Import many users at once. Returns a job ID to track progress.
const job = await client.agents.priming.batchImport("agent-id", {
users: [
{
userId: "user-1",
displayName: "Alice",
metadata: { company: "Acme", custom: { tier: "pro" } },
content: [{ type: "text", content: "Alice is a power user." }],
},
{
userId: "user-2",
displayName: "Bob",
metadata: { company: "Globex" },
},
],
source: "bulk_crm_sync",
});
// Poll import status
const status = await client.agents.priming.getImportStatus(
"agent-id", job.jobId
);
console.log(status.processed, status.total, status.failed);
// List recent import jobs
const jobs = await client.agents.priming.listImportJobs("agent-id", 10);