Skip to content
Write an extension

Remember things

Store rows that survive restarts and read them back.

State is a set of typed, keyed collections the extension owns. Shiitake stores them durably next to the Session, so they survive a restart. This page turns bookmarks.check into bookmarks.add, which saves the link, and lists the saved links in the prompt.

extension.ts
import { Mycelium } from "@fungi.computer/mycelium";
import {
defineCollection,
defineExtension,
} from "@fungi.computer/shiitake/extensions";
import * as Schema from "effect/Schema";
const Link = Schema.Struct({ url: Schema.String, title: Schema.String });
const Bookmark = Schema.Struct({ ...Link.fields, sessionId: Schema.String });
/** The model saves links; the next prompt lists what this Session saved. */
export const bookmarks = defineExtension("bookmarks")
.state({
bookmarks: defineCollection(Bookmark, {
key: (b) => JSON.stringify([b.sessionId, b.url]),
index: (b) => b.sessionId,
}),
})
.define({
prompt: (ctx, run) => {
const saved = ctx.state.read((s) =>
s.bookmarks.page({ index: run.sessionId, limit: 32 }),
);
const titles = saved.map((b) => b.title).join(", ");
return [{ name: "saved", text: `Saved links: ${titles || "none"}` }];
},
tools: (ctx, run) =>
Mycelium.module({
id: "example.bookmarks",
name: "bookmarks",
operations: {
add: Mycelium.operation({
replay: "safe",
description: "Save a link; saving the same URL replaces it.",
input: Link,
execute: (link) =>
Promise.resolve(
ctx.state.update((s) => {
s.bookmarks.put({ ...link, sessionId: run.sessionId });
return null;
}),
),
}),
},
}),
});

.state({ bookmarks: defineCollection(...) }) declares one collection. Its schema is the only decoder for stored rows. key names a row; saving the same key again replaces it. index groups rows so the prompt can read one Session’s links without scanning everyone’s.

The tool writes through ctx.state.update, one short synchronous transaction. The prompt reads through ctx.state.read. Both are plain functions: no promises inside, and no network calls while the transaction is open.

run.ts
import {
callTool,
echoSystemPrompt,
memoryHost,
} from "@fungi.computer/shiitake/testing";
import { bookmarks } from "./extension.js";
/** The model saves a link; its next request shows the saved list. */
export async function storedStateRun() {
const host = await memoryHost({
extensions: [bookmarks],
script: [
callTool("bookmarks.add", {
url: "https://shiitake.shiit.app/",
title: "Shiitake docs",
}),
echoSystemPrompt(),
],
});
try {
await host.agent
.session("reading")
.send("Save the Shiitake docs.", { requestId: "save" });
return await host.settle("reading");
} finally {
await host.close();
}
}
main.ts
import { storedStateRun } from "./run.js";
const snapshot = await storedStateRun();
const reply = snapshot.messages.at(-1)?.value;
if (reply?.role === "assistant")
for (const part of reply.content)
if (part.type === "text") console.log(part.text);
TERMINAL
node --import tsx main.ts

The model saves the link, and its next request already lists it:

TEXT
Complete this test run.
[extension:bookmarks.saved]
Saved links: Shiitake docs

Next: Show it to a client.