A presentation draws tool calls and colours; it executes nothing and validates nothing. Both cost it typebox anyway, which is 17 MB of module graph: 682 ESM modules whose narrowest entry re-exports ~300 of them, so there is no cherry-picking and both doors had to close before any process stopped paying. ToolExecutionComponent no longer looks up createAllToolDefinitions by tool name; callers pass what to render, and its parameter type now says so. Each built-in tool's presentation moves to core/tools/renderers/<tool>.ts, leaving the schema and execute path behind; bash and powershell share createShellRenderers(prompt). Tool definitions keep their exact public shape and spread the renderers in. withBuiltInRenderers() preserves the old fallback, where an extension override without renderers still draws like the built-in - that behaviour used to live inside the component, and two tests caught its absence. theme.ts keeps palettes; the typebox schema and its error formatting move to theme-json.ts, installed through setThemeJsonValidator. Built-in themes were never validated, so only processes that load user-authored themes need it. pi installs it during startup, before the first theme loads, and the invalid-theme message is unchanged. Measured with mini, running built output: the presentation drops from 87 MB and 99 files to 72 MB and 88 files, with typebox absent from its graph, while still rendering tool output through the real built-in renderers. Also: packages/server's vitest aliases were prefix matches, so a subpath import resolved onto index.ts/utils/uuid and failed five suites; they are exact now with explicit subpath rules. And mini-test.sh spawns mini the way pi-test.sh spawns pi, with --fresh/--stop for the detached session server. |
||
|---|---|---|
| .. | ||
| src | ||
| test | ||
| CHANGELOG.md | ||
| package.json | ||
| README.md | ||
| tsconfig.build.json | ||
| tsconfig.test.json | ||
| vitest.config.ts | ||
@earendil-works/pi-server
Experimental local server for the new durable Session and Agent Harness interfaces.
The current slice supports server- and Session-scoped plugin-service routing, multi-presentation attachment, prompting, and optional main-lane observation. RoutedServerServiceHost.attachClient() creates one connection-scoped server service endpoint with narrow attachment-management capabilities. RoutedSessionHandle.attachClient() returns a presentation-scoped Session capability. Its optional invokeService() forwards an opaque service/member envelope to the selected Session endpoint; the server validates the attachment route but does not load the plugin contract. Its optional watch() supplies an authoritative snapshot plus buffered events.
- server service calls and subscriptions route opaquely through the connection's
RoutedServerServiceAttachment; - the application-owned
SessionDirectoryprojects the private catalog into replicated presentation-safe state; - the application-owned
SessionManagementcreates, removes, attaches, and detaches Sessions without exposing route IDs in business results; - attachment changes are published out of band after the router installs or clears the live route;
- the compatibility
list,create, andattachoperations remain available to low-level clients during migration; promptexecutes one serializable prompt through the requesting presentation's attachment capability.- unknown Session service calls route through
invokeServicewithout server-side business-payload decoding; - service subscription updates remain scoped to the requesting attachment;
watch,startWatch, andstopWatchprovide snapshot-first lane observation when supported by the host.
A Session may have multiple presentation attachments. Repeating attach from one connection is idempotent; every successful attachment has a server-generated attachmentId delivered only as routing control data. Session requests carry { serverId, sessionId, attachmentId }, and the server rejects stale or mismatched routes. Losing a connection rejects its local responses but releases its attachment only after admitted prompts settle. The host decides when zero presentation demand and worker-local Harness activity permit worker retirement. Server shutdown closes every routed Session handle, releasing its worker and Session writer ownership.
import { randomUUID } from "node:crypto";
import { MemorySessionRepo, type Session } from "@earendil-works/pi-agent-core";
import {
type RoutedSessionHandle,
type ServerHost,
} from "@earendil-works/pi-server";
import { createUnixServer, getUnixSocketPath } from "@earendil-works/pi-server/unix";
async function startServer(
openRoutedSession: (session: Session) => Promise<RoutedSessionHandle>,
) {
const sessions = new MemorySessionRepo();
const host: ServerHost = {
sessions: {
list: () => sessions.list(),
async create({ id }) {
const session = await sessions.create({ id });
try {
return session.metadata;
} finally {
await session.close();
}
},
},
async openSession(metadata) {
const session = await sessions.open(metadata);
try {
return await openRoutedSession(session);
} catch (error) {
try {
await session.close();
} catch (cleanupError) {
throw new AggregateError(
[error, cleanupError],
"Harness creation and Session cleanup failed",
);
}
throw error;
}
},
};
const serverId = randomUUID();
const server = createUnixServer(host, {
serverId,
path: getUnixSocketPath(serverId, "/run/user/1000/pi"),
});
await server.start();
return server;
}
Applications supply a private Session catalog and a routed Session factory. The server projects catalog records to summaries before transport; the host receives the repository's concrete metadata and owns acquiring the worker-local Session and Harness. Failures are cleaned up in that worker. Neither an open JavaScript Session nor a Harness crosses the process boundary.
serverId is a logical identity supplied by the launcher, not a socket address. The Unix preset requires an explicit physical path; getUnixSocketPath() derives one from a caller-selected directory. Choose a short, private runtime directory rather than deriving the route from an unbounded home-directory path. A long-lived launcher can reuse the same ID and path when replacing a server process.
Server composes authenticated transports through ServerListener. The Unix submodule provides createUnixListener() and createUnixServer(). Low-level CBOR framing and validation come from @earendil-works/pi-protocol.
Server and worker lifecycle is managed outside the public Pi protocol. The replaceable application server converts connection attachments into private demand updates; the worker combines generation-tagged demand with authoritative Harness activity. The experimental coordinator only supplies stable routing and reports generic server-generation connection changes.