CogitaveLearn

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:11434 with 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:

bash
pnpm add @namzu/sdk @namzu/ollama zod

TIP

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:

typescriptsnippets/greeter/agent.ts#snippet_register
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

typescriptsnippets/greeter/agent.ts#snippet_chat
// 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:

typescript
{ 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:

typescriptsnippets/greeter/agent.ts#snippet_tool
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:

FieldWhat you are asserting
permissionsExactly what the tool may reach. [] means it computes and reaches nothing.
readOnlyWhether running it can change anything.
destructiveWhether it can destroy something. May be a function of the input.
concurrencySafeWhether 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:

typescriptsnippets/greeter/agent.ts#snippet_toolcalls
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:

  • toolCalls is 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 ToolContext is 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.