Valcraven Docs
Building with Valcraven

Add a chat tool

Give the AI agent a new tool it can call mid-conversation — the AgentTool shape, the ToolContext it receives, and how to register it, following the built-in create_item / generate_image pattern.

Add a chat tool

The built-in AI chat is more than a chatbot: the agent can call tools mid-conversation to take real actions on the user's behalf — create a record, generate an image, schedule a follow-up. Adding a tool means writing one object and registering it. The template ships three as the reference (create_item, generate_image, schedule_followup) in apps/realtime/src/tools.ts; copy their shape for your own.

The agent runs in the apps/realtime Worker — see AI chat for the feature and Realtime Worker (and the dev wiki at /admin/docs/agents) for the architecture.

How tools work

Every chat turn, the agent passes its tool definitions (name, description, input_schema) to Anthropic. When the model decides to call one, the chat loop dispatches the tool_use block to that tool's handler, feeds the result back to the model, and lets it continue. A handler that throws is surfaced to the model as an error result, so it can recover gracefully and explain to the user — handlers should throw on failure rather than swallow it.

The agent itself never touches the database. Tools act through the web app's API routes, which keeps all persistence and authorization in one place. Two transports are available on the context:

  • callAsUser(env, cookie, path, init) — forwards the user's session cookie, so the call runs as the user (e.g. create_itemPOST /api/items). Requires an interactive turn (a cookie).
  • callInternal(...) — an internal-secret call for work with no cookie (e.g. a scheduled wake). The web route verifies ownership before writing.

The tool shape

A tool is an AgentTool (apps/realtime/src/types.ts):

export interface AgentTool {
  name: string;
  description: string;
  input_schema: Record<string, unknown>; // Anthropic tool input schema (JSON Schema)
  handler: (input: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
}

The handler receives the model's parsed input and a ToolContext:

export interface ToolContext {
  userId: string;        // == the validated Better Auth user id
  cookie?: string;       // present when the turn came from a live socket
  env: Env;              // Worker bindings + secrets
  conversationId?: string; // the thread this turn belongs to
}

Write the tool

Follow createItemTool in apps/realtime/src/tools.ts. A tool that acts on the user's data requires a cookie and calls a web API route through callAsUser:

import type { AgentTool } from "./types";
import { callAsUser } from "./web-api";

export const logMealTool: AgentTool = {
  name: "log_meal",
  description:
    "Record a meal the user describes. Use when the user says they ate something.",
  input_schema: {
    type: "object",
    properties: {
      name: { type: "string", description: "What the user ate (required)." },
      calories: { type: "number", description: "Optional calorie estimate." },
    },
    required: ["name"],
  },
  handler: async (input, ctx) => {
    // Acting AS the user requires an interactive session cookie.
    if (!ctx.cookie) {
      throw new Error("log_meal requires an interactive session (no cookie on this turn).");
    }
    const res = await callAsUser(ctx.env, ctx.cookie, "/api/meals", {
      method: "POST",
      body: JSON.stringify({ name: input.name, calories: input.calories }),
    });
    if (!res.ok) {
      const detail = await res.text().catch(() => "");
      throw new Error(`Failed to log meal (${res.status}): ${detail}`);
    }
    const { meal } = (await res.json()) as { meal: { id: string } };
    return { logged: true, meal };
  },
};

Notes drawn from the built-in tools:

  • Write a precise description — it's the model's only cue for when to call the tool. Say what it does and when to use it.
  • Keep input_schema tight — mark required fields, describe each property. The model fills it from the conversation.
  • Guard ctx.cookie for user-acting tools, exactly as create_item and generate_image do — a scheduled wake has no cookie and must not act as the user.
  • Return a small JSON result the model can read back to the user. Don't return huge blobs.
  • The corresponding /api/meals route is a normal scoped endpoint — build it per Add an API route. Because callAsUser forwards the cookie, the route's own getSession check enforces ownership; the tool inherits that for free.

Register the tool

AppAgent.getTools() returns the built-ins (create_item, generate_image, schedule_followup) plus anything registered. The template's extension surface is registerTool() — call it from a subclass so you never edit the core agent file:

this.registerTool(logMealTool);

Re-registering the same name replaces the prior definition. (Alternatively, for a tool you consider part of the baseline, you can add it to the built-in list returned by getTools() in apps/realtime/src/index.ts, next to createItemTool — but registerTool is the intended path for downstream tools.)

Tools that close over the agent (to schedule wake-ups, publish proactively, etc.) are built per-instance — see makeScheduleFollowupTool(agent) in tools.ts, which takes the agent so its handler can call scheduleProactive.

Deploy

The agent runs in apps/realtime, which deploys independently of the web app. Ship tool changes with a manual deploy:

npm run deploy:realtime

On this page