Skip to content
Run it

Run it on Node

Keep a durable Agent in a SQLite file, serve its routes and survive restarts.

@fungi.computer/shiitake/node runs one durable Agent in a SQLite file on disk, on the Node event loop. It needs Node with node:sqlite. Accepted work is durable: close the process mid-run and reopening the same file resumes it.

node-agent.ts
import { fauxAssistantMessage } from "@earendil-works/pi-ai/providers/faux";
import { rejectingSandbox } from "@fungi.computer/mycelium/testing";
import { openNodeShiitake } from "@fungi.computer/shiitake/node";
import { fauxModelSource } from "@fungi.computer/shiitake/testing";
import { createShiitakeClient } from "@fungi.computer/shiitake/client";
import { Hono } from "hono";
import type { ShiitakeHttpEnv } from "@fungi.computer/shiitake/http";
/**
* Open a durable Agent in a SQLite file, serve its Session routes behind
* middleware that names the caller, and wait for one reply.
*/
export async function nodeAgent(path: string) {
const host = await openNodeShiitake({
path,
extensions: { roster: [], grants: [] },
instructions: {
load: () => Promise.resolve({ revision: "v1", text: "Reply briefly." }),
},
model: fauxModelSource([fauxAssistantMessage("Ready on Node.")]),
tools: { sandbox: rejectingSandbox("This Agent has no tools") },
});
// Only your middleware sets `shiitakeMeta`; request bodies cannot.
const app = new Hono<ShiitakeHttpEnv>()
.use(async (context, next) => {
context.set("shiitakeMeta", { author: { id: "ada", name: "Ada" } });
await next();
return undefined;
})
.route("/", host.routes);
// Any HTTP client reaches the same routes; here, Shiitake's own client.
const client = createShiitakeClient({
baseUrl: "http://agent.local",
fetch: (input, init) =>
Promise.resolve(app.request(new Request(input, init))),
});
try {
const sessionId = await client.createSession("first");
await client
.session(sessionId)
.send("Say hello.", { requestId: "hello", delivery: "followUp" });
// The Node event loop runs the accepted prompt; poll until it settles.
for (let attempt = 0; attempt < 200; attempt += 1) {
const snapshot = await host.agent.session(sessionId).read();
if (snapshot.terminals.length > 0) return snapshot;
await new Promise((resolve) => setTimeout(resolve, 10));
}
throw new Error("The prompt did not settle");
} finally {
await client.close();
await host.close();
}
}

openNodeShiitake({ path, ... }) takes the same model, instructions, extensions and tools as Shiitake.make; it owns the store and the runtime host. It returns the in-process agent, the Session routes as a Hono app, and close.

Serve routes with any server, such as @hono/node-server or Bun.serve. Mount it behind your own middleware: authenticate the caller there and set the shiitakeMeta variable to the metadata their prompts carry. Request bodies cannot carry metadata; every command body is closed. Pass meta.schema to make Shiitake check each value against your own schema.

The process holds an exclusive lease on the store until close. A second openNodeShiitake of the same file fails at once instead of sharing it, under any name: a store reached through a symbolic link, or with a second hard link, is refused. The store and its lease file are created private to your user (0600), whatever the umask, and a missing directory is created 0700. close stops waking, closes the Agent, then releases the database and lease.

run-node-agent.ts
import { nodeAgent } from "./node-agent.js";
const snapshot = await nodeAgent("./agent.sqlite");
console.log(JSON.stringify(snapshot.messages.at(-1)?.value, null, 2));
TERMINAL
npm init -y
npm pkg set type=module
npm install @fungi.computer/shiitake @fungi.computer/mycelium hono @earendil-works/pi-ai@1.3.0
npm install --save-dev tsx@4.23.12
node --import tsx run-node-agent.ts

The package’s runtime test calls the same nodeAgent() function against a temporary file and checks the completed run, the reply and the attributed prompt.

Swap fauxModelSource for your provider’s model source, and give the Agent tools and grants as Grant only what it needs shows.