Skip to main content
Sonzai Docs

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 Instance

Shared across all users in an instance. Use for environment state, configuration, agent status, global events.

Per-User State

Per Instance + User

Scoped 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 timestamp

List 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 On

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

sonzai_remember_nameToggleable

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

sonzai_web_searchToggleable

Live web search via Google. On by default.

sonzai_inventoryToggleable

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.

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

TypeScopePersistenceManaged Via
Built-in (sonzai_)All instancesPlatform-managedSDK capabilities, Dashboard
Agent-level customAll instancesPersistentSDK, Dashboard
Session-levelPer sessionTemporarySDK (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:   "[email protected]",
      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);