Exercise - Your first call, and your first tool
In this exercise you install the kernel, make a real model call, and declare a tool with a typed input schema. It runs locally and needs no account.
Every snippet below uses a symbol @namzu/sdk actually exports. The tool examples are pulled by reference from the snippet registry (snippets/greeter/agent.ts) so they stay compile-checked and cannot rot.
Before you start
- Node.js 20 or later, and a package manager. The commands use
pnpm. - TypeScript 5.5 or later.
- Optional, for the local model path: Ollama running on
http://localhost:11434with one model pulled.
1. Install the kernel and one driver
The kernel and the vendor drivers are separate packages, so you install exactly one vendor:
pnpm add @namzu/sdk @namzu/ollama zodTIP
If Ollama is not running, install only @namzu/sdk and skip ahead. The kernel carries a pre-registered MockLLMProvider, so every step below still executes — it simply answers from the mock instead of a model.
2. Register a provider
Registration happens once, at startup. It is the only vendor-specific code in your program:
import { ProviderRegistry, createUserMessage, collect } from "@namzu/sdk";
import { registerOllama } from "@namzu/ollama";
// Register once at startup. Swapping vendor is a change to these two lines and
// to nothing below them.
registerOllama();
const { provider } = ProviderRegistry.create({
type: "ollama",
host: "http://localhost:11434",
});3. Send your first call
// The provider's single entry point is a stream (`chatStream`). When you want
// the whole answer rather than per-token deltas, `collect` drains the stream
// into one aggregated response.
const response = await collect(
provider.chatStream({
model: "llama3.2",
messages: [createUserMessage("Greet a developer named Arda in one sentence.")],
}),
);
// Uniform across every provider:
// { id, model, message: { role, content, toolCalls? }, finishReason, usage }
console.log(response.message.content);Run it. The response shape is the same for every driver:
{ id, model, message: { role, content, toolCalls? }, finishReason, usage }That is the contract the kernel guarantees. A driver that cannot satisfy it is a bug in the driver, not something your code has to work around.
4. Declare a tool
A tool is an action with a typed input schema and a declared authority. Note what defineTool makes you state explicitly — this is the least-privilege model in practice, not a slogan:
import { z } from "zod";
import { defineTool } from "@namzu/sdk";
export const GreetTool = defineTool({
name: "greet",
description: "Return a friendly greeting for a given name.",
inputSchema: z.object({ name: z.string().min(1) }),
// One of: 'filesystem' | 'shell' | 'network' | 'analysis' | 'custom'.
category: "custom",
// Authority is declared, not assumed: an empty permission set is a tool that
// may compute and reach nothing.
permissions: [],
readOnly: true,
destructive: false,
concurrencySafe: true,
// execute returns a ToolResult: `success` plus the `output` string the model
// sees. It never throws to the caller - defineTool wraps failures into
// { success: false, output: "", error }.
async execute({ name }) {
return { success: true, output: `Hello, ${name}, from your first Namzu tool.` };
},
});Four of those fields are declarations about the tool rather than behaviour of it:
| Field | What you are asserting |
|---|---|
permissions | Exactly what the tool may reach. [] means it computes and reaches nothing. |
readOnly | Whether running it can change anything. |
destructive | Whether it can destroy something. May be a function of the input. |
concurrencySafe | Whether two invocations may overlap. |
The kernel uses these to decide what it is allowed to do on your behalf — for example whether it may run a call in parallel, or whether it must stop and defer to a person.
5. Register it, offer it, and execute the result
A ToolRegistry owns your tools. It converts them into the provider-neutral wire format with toLLMTools(), and it is what executes them - so validating the model's arguments is not your job:
import { ToolRegistry } from "@namzu/sdk";
// A registry owns the tools. It converts them to the provider-neutral wire
// format and it is what executes them, so validation is not your job.
const registry = new ToolRegistry();
registry.register(GreetTool);
const withTools = await collect(
provider.chatStream({
model: "llama3.2",
messages: [createUserMessage("Greet a developer named Arda.")],
tools: registry.toLLMTools(),
}),
);
// Every field here is required by ToolContext. A real runtime supplies it; this
// is the minimum a standalone script needs.
const context = {
runId: "run_local" as const,
workingDirectory: process.cwd(),
abortSignal: new AbortController().signal,
env: {} as Record<string, string>,
log: (level: "info" | "warn" | "error", message: string) => console.error(level, message),
};
for (const call of withTools.message.toolCalls ?? []) {
// `toolCalls` is the model's REQUEST. Nothing has run yet - the registry
// validates the arguments against the schema and only then invokes execute.
// A call names its tool under `function.name`, and `function.arguments` is a
// JSON string, so parse it before handing it to the registry.
const result = await registry.execute(
call.function.name,
JSON.parse(call.function.arguments),
context,
);
console.log(call.function.name, result);
}Three things worth noticing:
toolCallsis the model's request, not an execution. Nothing runs until you ask the registry to run it.- The registry validates arguments against your schema before invoking
execute, so it never has to defend against malformed input. - The
ToolContextis where authority actually arrives at run time - the working directory, the environment, the abort signal. A real runtime supplies it; the snippet builds the minimum a standalone script needs.
CAUTION
Do not widen a tool's permissions to make a call succeed. If a tool needs more authority, that is a decision to make deliberately — and to record — not a value to bump until an error stops appearing.
Where to go deeper
The kernel also covers sandboxing (@namzu/sandbox), telemetry over OTLP (@namzu/telemetry), file registry contracts, and a set of built-in tools such as ReadFileTool, GrepTool, and BashTool. The packages reference lists the published surface.