Skip to content
Write an extension

Show it to a client

Publish a typed view of one Session for clients.

A projection is the extension’s public view of one Session. Clients read it; they never open the extension’s tables. This page projects the saved links.

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 });
/** Page 3's extension plus a projection: clients see the saved links. */
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 });
s.observe(run.sessionId);
return null;
}),
),
}),
},
}),
projection: {
schema: Schema.Array(Bookmark),
read: (ctx, sessionId) =>
ctx.state.bookmarks.peekPage({ index: sessionId, limit: 32 }),
},
});

Two lines are new since the last page. projection declares the schema clients decode and a read function that returns the value for one Session. It runs while Shiitake builds a Session snapshot, so it reads with the synchronous peekPage. An empty list is a real value: no bookmarks is useful to a client.

In the tool, s.observe(run.sessionId) tells Shiitake the Session changed. Subscribed clients receive the new projection once the write commits.

run.ts
import { callTool, memoryHost } from "@fungi.computer/shiitake/testing";
import { bookmarks } from "./extension.js";
/** The model saves a link; read the Session as a client would. */
export async function projectionRun() {
const host = await memoryHost({
extensions: [bookmarks],
script: [
callTool("bookmarks.add", {
url: "https://shiitake.shiit.app/",
title: "Shiitake docs",
}),
"Saved.",
],
});
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 { projectionRun } from "./run.js";
const snapshot = await projectionRun();
console.log(snapshot.extensions.bookmarks);
TERMINAL
node --import tsx main.ts

snapshot.extensions.bookmarks holds the slot a client sees: its status, the extension’s revision and the projected value, with the saved link and its Session.

Next: Let people act.