pi-mono/packages/server
Mario Zechner eb3e9feed1 feat(coding-agent): split tool renderers and theme validation from their implementations
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.
2026-08-26 23:08:57 +02:00
..
src feat(agent): expose durable lane operations 2026-08-26 21:14:59 +02:00
test feat(agent): expose durable lane operations 2026-08-26 21:14:59 +02:00
CHANGELOG.md Add [Unreleased] section for next cycle 2026-08-24 12:50:52 +02:00
package.json feat: merge main into dev 2026-08-24 13:13:03 +02:00
README.md feat(agent): remove pi prefixes from client and server APIs 2026-08-20 10:51:04 +02:00
tsconfig.build.json chore: rename orchestrator to server (#6898) 2026-07-21 13:32:42 +02:00
tsconfig.test.json feat(protocol): add service-addressed session RPC 2026-08-13 18:24:27 +03:00
vitest.config.ts feat(coding-agent): split tool renderers and theme validation from their implementations 2026-08-26 23:08:57 +02:00

@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 SessionDirectory projects the private catalog into replicated presentation-safe state;
  • the application-owned SessionManagement creates, 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, and attach operations remain available to low-level clients during migration;
  • prompt executes one serializable prompt through the requesting presentation's attachment capability.
  • unknown Session service calls route through invokeService without server-side business-payload decoding;
  • service subscription updates remain scoped to the requesting attachment;
  • watch, startWatch, and stopWatch provide 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.