Tools & Agents

Tools let an LLM call your own functions mid-generation, so it can fetch data, run code, or take actions instead of just answering. You define each tool with a Zod schema and Gerbil handles the calling convention.

Quick Start

Tools are discovered automatically from .gerbil/tools/ in the REPL and CLI, no imports or registration required. Drop in a file:

.gerbil/tools/weather.tool.ts
01// .gerbil/tools/weather.tool.ts
02export default {
03 name: "get_weather",
04 description: "Get current weather for a city",
05 execute: async (params, ctx) => {
06 const { city } = params;
07 // Your implementation
08 return `Weather in ${city}: 72°F, sunny`;
09 },
10};

No imports needed! Tools are automatically discovered from .gerbil/tools/.

Built-in Tools

gerbil_docs

Searches Gerbil documentation for any topic. Built-in tools are referenced by their string id; in Agent mode the model calls them automatically.

gerbil_docs.json
{ "tool": "gerbil_docs", "params": { "query": "streaming" } }
// Returns documentation about streaming responses

Available topics:

  • quickstart, generate, stream, json, thinking, embed
  • models, load, ai-sdk, next, express, react, hono
  • langchain, mcp, skills, define-skill, cli, tools

To call a skill directly from your own code, import it from @tryhamster/gerbil/skills, see the Skills docs.

Defining Custom Tools

Simple Format (Recommended)

Tools use a simple export format with no imports required:

.gerbil/tools/calculator.tool.ts
01// .gerbil/tools/calculator.tool.ts
02export default {
03 name: "calculator",
04 description: "Perform basic math calculations",
05 execute: async (params, ctx) => {
06 const { expression } = params;
07 try {
08 const result = eval(expression);
09 return `${expression} = ${result}`;
10 } catch {
11 return `Error: Invalid expression`;
12 }
13 },
14};

Tool Context

Tools receive a context object with access to the LLM:

.gerbil/tools/generate_joke.tool.ts
01// .gerbil/tools/generate_joke.tool.ts
02export default {
03 name: "generate_joke",
04 description: "Generate a joke about a topic",
05 execute: async (params, ctx) => {
06 const { topic } = params;
07
08 // Use ctx.generate() to call the LLM!
09 if (ctx?.generate) {
10 const joke = await ctx.generate(`Tell me a short joke about ${topic}`);
11 return joke;
12 }
13
14 return "Could not generate joke";
15 },
16};

Create Tool Wizard

The REPL includes a 4-step guided wizard to create tools:

  1. Name, Tool name (snake_case)
  2. Description, What the tool does
  3. Parameters, Inputs (optional, press Enter to skip)
  4. Logic, What to return

The wizard uses AI to generate the execute function body, then saves to .gerbil/tools/.

Tool Registry

Every file in .gerbil/tools/ is registered at startup alongside the built-ins. In the REPL, the Tools view (press 3) lists everything that is registered:

Terminal
gerbil repl
# Press 3 → Tools view lists all registered tools:
# gerbil_docs [builtin]
# get_weather [project]
# calculator [project]

Using Tools

REPL Tools View

Manage tools in the REPL (press 3 or navigate to Tools):

Terminal
gerbil repl
# Press 3 for Tools view
  • x, Execute tool directly
  • c, Create a new tool (guided wizard)
  • o, Open tool file in VS Code
  • Enter, Expand tool details

Tools are shown with badges: [builtin], [project], or [error].

REPL Agent Mode

Use tools automatically in Chat with Agent mode:

Terminal
gerbil repl
# In Chat view: press ⌘A to toggle Agent mode
# Or Tab to cycle modes → select "Agent"

In Agent mode, the model can see available tools, call them by outputting JSON, and receive tool results to synthesize a response.

Programmatic Usage

To drive the full generate → tool → feedback loop from code, use engine.generateWithTools (Node / imperative) or the useAgent hook (React/browser). Both handle prompt formatting, parsing the model's tool calls, and executing them for you, see engine.generateWithTools and agentic tool calling below.

Tool Call Format

Gerbil uses a simple JSON format for tool calls:

format.json
{
"tool": "tool_name",
"params": {
"param1": "value1",
"param2": "value2"
}
}

The model is prompted to output this format when it wants to use a tool.

Agentic tool calling in the browser

The useAgent hook runs a complete agent loop entirely on-device with WebGPU: the model generates, requests a tool, your tool executes, the result is fed back, and the loop repeats until the model produces a final answer. Nothing leaves the browser. Import it from @tryhamster/gerbil/hooks.

import.ts
import { useAgent } from "@tryhamster/gerbil/hooks";

Define one or more tools, pass them to useAgent, then call run() with a prompt. The returned promise resolves to the final answer string, and the steps array gives you the full trace to render in your UI:

WeatherAgent.tsx
01import { useAgent } from "@tryhamster/gerbil/hooks";
02import type { AgentTool } from "@tryhamster/gerbil/hooks";
03
04const weatherTool: AgentTool = {
05 name: "get_weather",
06 description: "Get the current weather for a city",
07 parameters: {
08 type: "object",
09 properties: {
10 city: { type: "string", description: "City name" },
11 },
12 required: ["city"],
13 },
14 execute: async ({ city }) => {
15 const res = await fetch(`https://api.example.com/weather?city=${city}`);
16 const data = await res.json();
17 return `${data.temp}°F and ${data.conditions} in ${city}`;
18 },
19};
20
21function WeatherAgent() {
22 const { run, steps, answer, isRunning, isReady } = useAgent({
23 model: "mlx-community/Qwen3.5-0.8B-4bit",
24 tools: [weatherTool],
25 maxSteps: 5,
26 autoLoad: true,
27 });
28
29 return (
30 <div>
31 <button
32 type="button"
33 disabled={!isReady || isRunning}
34 onClick={() => run("What's the weather in Paris?")}
35 >
36 {isRunning ? "Thinking…" : "Ask"}
37 </button>
38
39 <ol>
40 {steps.map((step, i) => {
41 if (step.kind === "tool_call") {
42 return (
43 <li key={i}>
44 → calling <strong>{step.tool}</strong>(
45 {JSON.stringify(step.args)})
46 </li>
47 );
48 }
49 if (step.kind === "tool_result") {
50 return (
51 <li key={i}>
52{step.tool} returned: {step.result}
53 </li>
54 );
55 }
56 return <li key={i}>{step.text}</li>;
57 })}
58 </ol>
59
60 {answer && <p>{answer}</p>}
61 </div>
62 );
63}

The hook loads the model lazily. With autoLoad: true it begins loading on mount; otherwise call load() yourself and wait for isReady. Use reset() to clear the trace and answer between runs.

useAgent options & return value

useAgent.ts
useAgent(options: {
/** Model id to load (e.g. "mlx-community/Qwen3.5-0.8B-4bit") */
model: string;
/** Tools the agent may call */
tools: AgentTool[];
/** Max generate → tool → feedback cycles (default: 5) */
maxSteps?: number;
/** Start loading the model on mount (default: false) */
autoLoad?: boolean;
}): {
/** Run the loop; resolves to the final answer string */
run: (prompt: string) => Promise<string>;
/** Ordered trace of tool calls, tool results, and the answer */
steps: AgentStep[];
/** The final answer text */
answer: string;
/** True while a run is in flight */
isRunning: boolean;
/** True once the model is loaded and ready */
isReady: boolean;
/** Manually trigger model loading */
load: () => Promise<void>;
/** Clear steps, answer, and error */
reset: () => void;
/** Last error message, or null */
error: string | null;
}

AgentTool & AgentStep

Both types are exported from @tryhamster/gerbil/hooks. An AgentTool is framework-agnostic, its execute function receives the parsed arguments and returns a string (or a promise of one) that is fed back to the model. Tool objects you define in .gerbil/tools/ share this shape, so they can be passed straight to useAgent.

agent-types.ts
interface AgentTool {
/** Tool name the model calls */
name: string;
/** Description shown to the model */
description: string;
/** JSON-schema-ish parameter shape, shown to the model */
parameters?: unknown;
/** Runs the tool; its return value is fed back into the loop */
execute: (args: any) => Promise<string> | string;
}
type AgentStep =
| { kind: "tool_call"; tool: string; args: unknown }
| { kind: "tool_result"; tool: string; result: string }
| { kind: "answer"; text: string };

Lower-level: engine.generateWithTools

For Node or imperative use, the core WebGPUEngine from @tryhamster/gerbil/gpu exposes generateWithTools, which runs the same loop without React. It generates, parses a <tool_call>{"name","arguments"}</tool_call> block, runs the matching tool's execute, feeds the result back, and repeats until the model answers with no tool call:

generate-with-tools.ts
01import { WebGPUEngine } from "@tryhamster/gerbil/gpu";
02
03const engine = await WebGPUEngine.create({
04 repo: "mlx-community/Qwen3.5-0.8B-4bit",
05});
06
07const { text, steps } = await engine.generateWithTools(
08 "What's the weather in Paris?",
09 {
10 tools: [weatherTool],
11 maxSteps: 5,
12 onStep: (step) => console.log(step),
13 maxTokens: 512,
14 }
15);
16
17console.log(text); // final answer
18console.log(steps); // AgentStep[] trace

generateWithTools accepts the same AgentTool[] and an optional onStep callback, plus standard maxTokens and sampling options. It returns { text, steps }, the final answer and the full trace.

Connecting Skills as Tools

Skills and tools share a similar shape, so you can wrap any skill as an AgentTool and pass it to useAgent or generateWithTools:

skills-as-tools.ts
01import { summarize } from "@tryhamster/gerbil/skills";
02import type { AgentTool } from "@tryhamster/gerbil/hooks";
03
04const summarizeTool: AgentTool = {
05 name: "summarize",
06 description: "Summarize text content",
07 parameters: {
08 type: "object",
09 properties: {
10 content: { type: "string" },
11 length: { type: "string", enum: ["short", "medium", "long"] },
12 },
13 required: ["content"],
14 },
15 execute: async ({ content, length }) =>
16 summarize({ content, length }),
17};

API Reference

Tool file shape

A .gerbil/tools/*.tool.ts file default-exports an object with this shape (no imports required):

ToolDefinition.ts
interface ToolDefinition {
/** Tool name (function name) */
name: string;
/** Description for the LLM */
description: string;
/** Optional JSON-schema parameter shape shown to the model */
parameters?: unknown;
/** The function to execute; returns a string fed back to the model */
execute: (params: any, ctx?: { generate: (p: string) => Promise<string> }) => Promise<string> | string;
}

Agent API

The public, importable surface for running tools is the agent loop:

agent-api.ts
// React/browser — @tryhamster/gerbil/hooks
useAgent(options: UseAgentOptions): UseAgentReturn
// Node/imperative — @tryhamster/gerbil/gpu
engine.generateWithTools(
prompt: string,
options: { tools: AgentTool[]; maxSteps?: number; onStep?: (s: AgentStep) => void }
): Promise<{ text: string; steps: AgentStep[] }>

Model Compatibility

Tool calling works best with:

  • Qwen3.5-0.8B, Excellent tool calling and JSON formatting (native default)
  • ~LFM2.5-350M, Faster/smaller; usable for simple tool calls, less reliable JSON

Best Practices

  • 1.Clear descriptions, The model uses descriptions to decide when to call tools
  • 2.Simple parameters, Use basic types when possible
  • 3.Helpful errors, Return clear error messages
  • 4.Validate inputs, Zod schemas validate before execution
  • 5.Keep responses concise, Long tool outputs consume context