Extension reference
Choose an extension contribution and its required capability.
An extension declares the behavior it supplies. Its host then grants the capabilities that behavior needs. The public extensions module owns the complete types; the table below helps choose a contribution.
Identity
Section titled “Identity”defineExtension("id") names an extension; defineExtension({ id, version })
also sets a SemVer version (default 0.0.0). The id, version, declared schemas,
config and requested capabilities together form the extension’s revision. See
Ship it for what the version is for.
.state({ name: defineCollection(schema, { key, index }) }) declares state
version 1. Use .state({ version, collections, migrations }) when a row schema
changes. The names read, update and observe are reserved, and a collection
named collections, version or migrations needs the full form.
| Where | Read | Write |
|---|---|---|
| Prompt section, tool | ctx.state.read((s) => s.name.page(...)) |
ctx.state.update((s) => …) |
| Command, hook (Effect) | ctx.state.name.get(...), .page(...) |
ctx.state.name.put(...) |
| Projection | ctx.state.name.peek(...), .peekPage(...) |
none |
State migrations are pure row transformations, not a place to call a network service.
Commands, tools and projection
Section titled “Commands, tools and projection”import { Mycelium } from "@fungi.computer/mycelium";import { defineCollection, defineExtension,} from "@fungi.computer/shiitake/extensions";import * as Effect from "effect/Effect";import { Bookmark, bookmarkContract } from "./contract.js";
/** Bookmarks: the complete extension, built on the shared contract. */export const bookmarks = defineExtension(bookmarkContract.id) .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"}` }]; }, commands: { specs: bookmarkContract.commands, handlers: (ctx) => ({ add: ({ sessionId, input }) => Effect.gen(function* () { const bookmark = { ...input, sessionId }; yield* ctx.state.bookmarks.put(bookmark); yield* ctx.observe(sessionId); return bookmark; }), }), }, tools: (_ctx, _run, commands) => Mycelium.module({ id: "example.bookmarks", name: "bookmarks", operations: { add: commands.tool("add", { description: "Save a link; saving the same URL replaces it.", }), }, }), projection: { schema: bookmarkContract.projection.schema, read: (ctx, sessionId) => ctx.state.bookmarks.peekPage({ index: sessionId, limit: 32 }), }, });Command specs pair a scope with input and output schemas. The handler receives
the admitted Session and decoded input. Return a typed ExtensionCommandError
for a command refusal, rather than stopping the extension for an expected user
mistake.
The tools function’s third argument exposes a command to the model:
commands.tool("add", { description }) runs the same handler, scoped to the
calling run’s Session. It needs both the run_tools and the commands grant.
Mycelium operations take an Effect schema or any Standard Schema that also describes itself as JSON Schema, such as Zod 4, so an extension needs only one schema library.
Custom schema checks need a stable representation so core can fingerprint the
contract. Give a Schema.makeFilter check a representation annotation with an
id and its parameters, for example
{ id: "example/web-url", payload: { protocols: ["http:", "https:"] } }.
A projection reads committed state for one Session and returns the value (or an
Option). Its schema is the client contract. Use observe(sessionId) after
changes so a client receives the committed view.
start acquires activation-scoped resources. onSettled consumes a bounded
page of settled runs, and onTimer consumes a fired timer. Core commits the
cursor or timer consumption with the hook’s state changes. Keep transaction
hooks synchronous; acquire asynchronous resources in start instead.
import { defineCollection, defineExtension,} from "@fungi.computer/shiitake/extensions";import { memoryHost } from "@fungi.computer/shiitake/testing";import * as Effect from "effect/Effect";import * as Option from "effect/Option";import * as Schema from "effect/Schema";
/** Count successful settlements; core owns the durable delivery cursor. */export async function settledCounterRoundTrip() { const counter = defineExtension("settled-counter") .state({ counts: defineCollection( Schema.Struct({ sessionId: Schema.String, count: Schema.Int }), { key: (row) => row.sessionId }, ), }) .define({ hooks: { onSettled: (ctx, page) => Effect.gen(function* () { for (const run of page) { if (run.outcome !== "completed") continue; const previous = yield* ctx.state.counts.get(run.sessionId); yield* ctx.state.counts.put({ sessionId: run.sessionId, count: Option.isSome(previous) ? previous.value.count + 1 : 1, }); yield* ctx.observe(run.sessionId); } }), }, projection: { schema: Schema.Int, read: (ctx, sessionId) => Option.match(ctx.state.counts.peek(sessionId), { onNone: () => 0, onSome: (row) => row.count, }), }, }); const host = await memoryHost({ extensions: [counter] }); try { const session = host.agent.session("counter-example"); await session.send("First.", { requestId: "counter-first" }); const first = await host.settle("counter-example"); await session.send("Second.", { requestId: "counter-second" }); const second = await host.settle("counter-example"); await host.reconcile(); const repeated = await session.read(); return { first, second, repeated }; } finally { await host.close(); }}This exact example counts two completed runs, reconciles again and checks that
the count stays at two. Core commits the delivery cursor with the hook’s writes;
the hook does not maintain a second cursor. It publishes only its own Session
projection. The settled grant is separate from state and projection.
Fungi’s Goals feature uses onSettled to respond to completed runs. The public
ExtensionDefinitionV0 type describes the same hook contract for your own
extensions.
Timers and attention
Section titled “Timers and attention”A timer schedules extension work through the Agent host; it does not create an
independent process loop. Attention is extension-owned data on an existing
Session. Clearing one extension’s attention cannot clear another owner’s items.
See TimersV0 and AttentionV0 in the public extensions module for the
admitted operations.
Prompt sections, run tools and files
Section titled “Prompt sections, run tools and files”prompt adds extension-owned sections before each model request. tools
supplies a Mycelium module for one run; files supplies Knapsack-minted file
tools. Grant the matching capability and keep the operation gate. A definition
is not authority to execute arbitrary tools.
Client entries
Section titled “Client entries”The client entry turns a decoded projection into presentation data: panel rows, chips, timeline cells and waiting state. It uses the public client-entry module, without importing host state. Core owns the bounded row grammar and command-action admission. See Draw it in a client for the Bookmarks entry.
Testing
Section titled “Testing”memoryHost({ extensions: [ext], script: [...] }) grants each listed extension
what it requests and scripts the model: strings are replies,
callTool("alias.operation", input) calls a tool through the admitted
operation, and echoSystemPrompt() replies with the system prompt. Pass a
roster instead of a list to test real grants. See Test it.
Compaction
Section titled “Compaction”An extension may supply a compaction contribution. The exclusive compaction
handoff is separately granted; do not assume that adding a summarizer gives it
control of the whole Session. Read BorrowedCompactionViewV0 and
CompactionHandoffV0 before implementing a policy.