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

run_skill

Executes any Gerbil skill by name. The model calls it like any other tool:

run_skill.json
{
"tool": "run_skill",
"params": { "skill": "summarize", "input": "Long article text..." }
}

To call a skill directly from your own code, import it from @tryhamster/gerbil/skills instead — 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]
# run_skill [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