Skip to content
Write an extension

Give the model a tool

Add a typed operation the model can call.

A tool is a typed function the model can call. This page adds one to a new bookmarks extension: bookmarks.check takes a link and says whether it is an http or https URL. It stores nothing yet; the next page does.

extension.ts
import { Mycelium } from "@fungi.computer/mycelium";
import { defineExtension } from "@fungi.computer/shiitake/extensions";
import * as Schema from "effect/Schema";
const isWebLink = (url: string) =>
URL.canParse(url) && ["http:", "https:"].includes(new URL(url).protocol);
/** One stateless tool: the model checks a link before it shares it. */
export const bookmarks = defineExtension("bookmarks").define({
prompt: () => [
{ name: "guide", text: "Check a link with bookmarks.check first." },
],
tools: () =>
Mycelium.module({
id: "example.bookmarks",
name: "bookmarks",
operations: {
check: Mycelium.operation({
replay: "safe",
description: "Check that a link is an http(s) URL; return its host.",
input: Schema.Struct({ url: Schema.String }),
output: Schema.Struct({ ok: Schema.Boolean, host: Schema.String }),
execute: ({ url }) =>
Promise.resolve(
isWebLink(url)
? { ok: true, host: new URL(url).host }
: { ok: false, host: "" },
),
}),
},
}),
});

Tools are Mycelium operations grouped in a module. The module’s name is how the model reaches it: bookmarks.check. Each operation declares its input and output with an Effect schema. Mycelium decodes the model’s input before execute runs and describes both shapes to the model, so the model learns the tool from the schema itself.

replay: "safe" says calling it twice with the same input is harmless. Mark a tool "unsafe" (the default) when a repeated call would change something twice.

run.ts
import { callTool, memoryHost } from "@fungi.computer/shiitake/testing";
import { bookmarks } from "./extension.js";
/** A scripted model calls bookmarks.check, then replies. */
export async function modelToolRun() {
const host = await memoryHost({
extensions: [bookmarks],
script: [
callTool("bookmarks.check", { url: "https://shiitake.shiit.app/" }),
"That link is fine.",
],
});
try {
await host.agent
.session("links")
.send("Is this link OK?", { requestId: "check" });
return await host.settle("links");
} finally {
await host.close();
}
}

callTool("bookmarks.check", input) scripts one model turn that calls the tool; the string after it is the model’s next reply. The test host runs the scripted call through the same admitted operation a real model would reach. It evaluates no model-written code, so it is for tests and examples only.

main.ts
import { modelToolRun } from "./run.js";
const snapshot = await modelToolRun();
// The tool result the model received, then its reply.
for (const { value } of snapshot.messages.slice(-2))
if (value.role === "toolResult" || value.role === "assistant")
for (const part of value.content)
if (part.type === "text") console.log(part.text);

Install Mycelium and Effect alongside Shiitake, then run it:

TERMINAL
npm install @fungi.computer/mycelium effect@4.0.0
node --import tsx main.ts

It prints the tool result the model received, then the model’s reply:

TEXT
{"ok":true,"host":"shiitake.shiit.app"}
That link is fine.

Next: Remember things.