Skip to content

Adding Tools

Deep dive into tool patterns — inline tools, createTool, composition, and advanced usage.

Inline vs createTool

Inline — for simple, one-off tools:

tsx
<Tool
  name="greet"
  description="Greet someone"
  input={z.object({ name: z.string() })}
  handler={({ name }) => `Hello, ${name}!`}
/>

createTool — for reusable tools with static access:

tsx
const GreetTool = createTool({
  name: "greet",
  description: "Greet someone",
  input: z.object({ name: z.string() }),
  handler: ({ name }) => `Hello, ${name}!`,
});

// Use as component
<GreetTool />;

// Call programmatically
await GreetTool.run({ name: "Ryan" });

Use createTool when you need to reuse the tool across agents or call it programmatically. Use inline <Tool> for agent-specific tools that won't be shared.

Tool Composition

Tools are components — they compose like any React component:

tsx
function DataTools({ database }: { database: Database }) {
  return (
    <>
      <Tool
        name="query"
        description="Run a SQL query"
        input={z.object({ sql: z.string() })}
        handler={({ sql }) => database.query(sql)}
      />
      <Tool
        name="schema"
        description="Get table schema"
        input={z.object({ table: z.string() })}
        handler={({ table }) => database.schema(table)}
      />
    </>
  );
}

// Use in any agent
<DataTools database={productionDb} />;

Async Handlers

Handlers can be async. The execution waits for the promise to resolve before proceeding to the next tick.

tsx
const FetchTool = createTool({
  name: "fetch_url",
  description: "Fetch content from a URL",
  input: z.object({ url: z.string().url() }),
  handler: async ({ url }) => {
    const res = await fetch(url);
    if (!res.ok) return `Error: ${res.status}`;
    return await res.text();
  },
});

Context Object Model (ctx)

The second argument to handlers is the ctx — the Context Object Model:

tsx
handler: async ({ query }, ctx) => {
  // Set session state
  ctx?.setState("lastQuery", query);

  // Emit an event
  ctx?.emit("search:started", { query });

  // Read state
  const count = ctx?.getState("searchCount") ?? 0;
  ctx?.setState("searchCount", count + 1);

  return results;
},

Context Injection with use()

Tools defined at module scope can't access React Context directly in their handlers. The use() hook solves this — it runs at render time, captures values from the tree, and passes them to the handler as deps.

The problem: You have a <SandboxProvider> that provides a sandbox via React Context. A tool needs that sandbox, but its handler runs outside the component tree.

The solution: use() bridges the gap.

tsx
const ShellTool = createTool({
  name: "shell",
  description: "Run a shell command",
  input: z.object({ command: z.string() }),
  use: () => ({ sandbox: useSandbox() }),
  handler: async ({ command }, deps) => {
    const result = await deps!.sandbox.exec(command);
    return [{ type: "text", text: result.stdout }];
  },
});

The deps parameter is { ctx, sandbox } when rendered in JSX, undefined when called via .run(). This makes it safe — direct calls still work, they just don't get tree context.

Multiple providers: Two tool instances under different providers capture different values:

tsx
<SandboxProvider sandbox={localSandbox}>
  <ShellTool />  {/* gets localSandbox */}
</SandboxProvider>
<SandboxProvider sandbox={dockerSandbox}>
  <ShellTool />  {/* gets dockerSandbox */}
</SandboxProvider>

When to use use(): When your tool handler needs something from the component tree — a provider value, a custom hook result, a context-scoped service. If the tool only needs COM state, plain ctx is sufficient.

User-Audience Tools

audience: "user" is a visibility flag — the tool is hidden from the model but still registered for programmatic invocation.

tsx
const ResetTool = createTool({
  name: "reset-state",
  description: "Reset the agent's working state",
  input: z.object({}),
  audience: "user",
  aliases: ["reset"],
  handler: async (_, ctx) => {
    ctx?.setState("buffer", []);
    return [{ type: "text", text: "State reset" }];
  },
});

session.dispatch(name, input) invokes any tool by name — it works on both regular and user-audience tools. User-audience tools can only be reached this way since the model can't see them. Use aliases for alternative dispatch names.

Error Handling

Throw from handlers to return tool errors to the model:

tsx
handler: async ({ id }) => {
  const item = await db.find(id);
  if (!item) throw new Error(`Item ${id} not found`);
  return item;
},

The error message is sent back to the model as the tool result. The model can then decide how to proceed.

Released under the ISC License.