Skip to content

packages web server

Zachary BENSALEM edited this page Aug 15, 2026 · 1 revision

Web server

Active contributors: Zachary BENSALEM

Purpose

web/server is the HTTP adapter for the Qredence web chat interface. It turns browser HTTP requests into calls on the Prime Agent runtime and bridges runtime events back into browser-visible stream frames. It is the only web package that imports @earendil-works/* (packages/ai, packages/agent, packages/coding-agent, wired through pnpm link:), so it is the sole seam between the standalone web/ UI and the in-tree agent runtime.

The package is pure TypeScript with no React and no HTTP framework. It exports Request → Response handler functions and a PrimeBridge session coordinator. TanStack Start routes in web/app are thin wrappers over these handlers. Because the bridge and handlers are plain functions, they are process-agnostic and can run behind any host; the browser is pointed at a remote runtime via the optional VITE_FLEET_PI_CHAT_RUNTIME_URL env var.

The wire contract it speaks is defined in web/protocol (@prime-agent/web-protocol), not in this package. web/server imports the ChatStreamEvent and ChatMessage types plus zod schemas from there.

Directory layout

web/server/
├── package.json                 # @prime-agent/web-server, private
├── tsconfig.json
├── vitest.config.ts
└── src/
    ├── index.ts                 # public exports (handlers + PrimeBridge + config)
    ├── prime-bridge.ts          # session coordinator (core file)
    ├── event-mapper.ts          # AgentSessionEvent -> ChatStreamEvent[] (pure)
    ├── ring-buffer.ts           # per-session frame store + seq for SSE replay
    ├── pending-dialogs.ts       # confirm/select/input registry with 60s timeout
    ├── prime-config.ts          # process-wide config singletons (settings/auth/models)
    ├── prime-provider-env-map.ts# provider -> api-key env var map for the UI
    ├── singleton.ts             # one bridge per Node process (globalThis-pinned)
    ├── sse-replay.ts            # decides which ring-buffer events to replay
    ├── wrap-api-handler.ts      # error -> { status, message } JSON envelope
    ├── workspace-root.ts        # default workspace root resolution
    ├── workspace-tree.ts        # file tree read
    ├── workspace-file.ts        # file preview read
    ├── workspace-browse.ts      # directory listing for pickers
    ├── workspace-paths.ts       # path helpers for workspace routes
    ├── handlers/                # one Request -> Response function per route
    │   ├── chat.ts              # POST /api/chat (NDJSON turn stream)
    │   ├── chat-abort.ts        # POST /api/chat/abort
    │   ├── chat-command.ts      # POST /api/chat/command (slash commands)
    │   ├── chat-commands.ts     # GET /api/chat/commands (autocomplete)
    │   ├── chat-events.ts       # GET /api/chat/events (SSE + replay)
    │   ├── chat-model.ts        # POST /api/chat/model
    │   ├── chat-models.ts       # GET /api/chat/models
    │   ├── chat-models-discover.ts # POST /api/chat/models/discover (OCC probe)
    │   ├── chat-new.ts          # POST /api/chat/new
    │   ├── chat-providers.ts    # GET/POST/DELETE /api/chat/providers
    │   ├── chat-providers-oauth.ts # POST /api/chat/providers/oauth
    │   ├── chat-question.ts     # POST /api/chat/question (answer a dialog)
    │   ├── chat-resources.ts    # GET /api/chat/resources (skills/prompts/extensions)
    │   ├── chat-resume.ts       # POST /api/chat/resume
    │   ├── chat-session.ts      # GET /api/chat/session
    │   ├── chat-sessions.ts     # GET /api/chat/sessions
    │   ├── chat-settings.ts     # GET/PATCH /api/chat/settings
    │   ├── health.ts            # GET /api/health
    │   ├── workspace-browse.ts  # GET /api/workspace/browse
    │   ├── workspace-file.ts    # GET /api/workspace/file
    │   ├── workspace-root.ts    # POST /api/workspace/root
    │   └── workspace-tree.ts    # GET /api/workspace/tree
    └── __tests__/               # 12 vitest suites

Key abstractions

Type Full path One-line description
PrimeBridge web/server/src/prime-bridge.ts Owns all live sessions, ring buffers, pending dialogs, and the kernel-readiness gate; the single coordinator the route layer calls.
WebUIContext web/server/src/prime-bridge.ts Structural ExtensionUIContext per session; maps select/confirm/input to tool-Question frames and notify/setStatus to state frames.
BridgeSession web/server/src/prime-bridge.ts Per-session bundle: AgentSession, cwd, session path, OpenUI prompt state, mapper state, and WebUIContext.
mapAgentSessionEvent web/server/src/event-mapper.ts Pure AgentSessionEvent → ChatStreamEvent[] translator (no I/O).
EventMapperState web/server/src/event-mapper.ts Per-session accumulator (runId, message id/seq, current text/thinking/tool parts).
RingBuffer web/server/src/ring-buffer.ts Per-session frame store with monotonic seq; supports replay-since with overflow detection.
PendingDialogRegistry web/server/src/pending-dialogs.ts Registers and resolves confirm/select/input promises; 60s auto-cancel timeout.
PrimeConfig web/server/src/prime-config.ts Process-wide AuthStorage, ModelRegistry, per-cwd SettingsManager and DefaultResourceLoader.
getBridge web/server/src/singleton.ts Lazy singleton bridge pinned on globalThis so all handlers share one bridge.
shouldReplaySseEvent web/server/src/sse-replay.ts Filters ring-buffer frames for first-time SSE clients (only still-pending questions).
wrapApiHandler web/server/src/wrap-api-handler.ts Wraps handlers to convert thrown errors into { message, status } JSON responses.

How it works

The bridge is a session coordinator with no HTTP and no React. Each BridgeSession wraps a live AgentSession created through createAgentSessionFromServices and a SessionManager that persists JSONL transcripts under ~/.prime/agent/sessions/. Sessions subscribe to runtime events, and mapAgentSessionEvent translates each event into zero or more ChatStreamEvent frames that #dispatch writes to the session ring buffer and forwards to every SSE listener.

Interactive prompts from the agent loop are the out-of-turn path. When the agent needs user input it calls select/confirm/input on WebUIContext, which registers a pending dialog and emits a tool-Question frame into the ring buffer. The SSE connection delivers it to the browser, the user answers, the browser sends POST /api/chat/question, and PrimeBridge.answerDialog resolves the matching PendingDialog so the awaiting tool call resumes.

sequenceDiagram
    participant B as Browser
    participant R as TanStack route (web/app)
    participant H as Handler (web/server)
    participant PB as PrimeBridge
    participant AG as AgentSession (coding-agent)
    participant RB as RingBuffer
    participant SSE as SSE connection

    B->>R: POST /api/chat {sessionId, message}
    R->>H: handleChatPost(req)
    H->>PB: getBridge().getSession(sessionId)
    H->>PB: addEventListener (NDJSON writer)
    H->>PB: bridge.prompt(sessionId, message)
    PB->>AG: session.prompt(text, ...)
    AG-->>PB: tool question (ExtensionUIContext.select)
    PB->>PB: dialogs.open(...)
    PB->>RB: push tool-Question frame
    RB-->>SSE: tool-Question frame (event: message)
    SSE-->>B: tool-Question frame
    B-->>SSE: answer form / card
    B->>R: POST /api/chat/question {toolCallId, answer}
    R->>H: handleChatQuestionPost(req)
    H->>PB: bridge.answerDialog(sessionId, toolCallId, answer)
    PB->>PB: dialogs.answer(...) resolves promise
    PB-->>AG: select resolves with choice
    AG-->>PB: tool_execution_end, message events...
    PB->>RB: push mapped frames
    RB-->>SSE: agent frames
    H->>H: NDJSON stream closed on done frame
    H-->>B: NDJSON stream (start ... done)
Loading

Turns stream as NDJSON over POST /api/chat. The handler subscribes a listener to the bridge, writes a leading start frame, then forwards every matching session frame until a done or error frame closes the ReadableStream. Out-of-turn pushes (questions, status/notify state frames, agent messages) go over a separate SSE connection at GET /api/chat/events with ring-buffer replay: the client reconnects with Last-Event-ID: <last seq>, and RingBuffer.replaySince returns everything with a higher seq. If the client is behind the oldest retained frame the buffer reports overflow, the handler emits state: resync-required, and the UI falls back to GET /api/chat/session.

Integration points

  • Runtime: packages/coding-agent (createAgentSessionFromServices, AgentSession, AgentSessionEvent, ExtensionUIContext, IpythonKernelProvisioner, SessionManager, AuthStorage, ModelRegistry, SettingsManager, DefaultResourceLoader). See coding-agent.
  • Wire contract: web/protocol (ChatStreamEvent, ChatMessage, ChatQuestionAnswer, zod schemas). See web-protocol.
  • Frontend host: web/app TanStack Start routes call these handlers; see web-app.
  • Rendering: web/design renders the streamed ChatStreamEvent frames (tool cards, question cards); see web-design.
  • Streaming flows: streaming-chat describes the NDJSON + SSE flows end to end.
  • HTTP endpoints: web-api lists each route and its response shape.

The process boundary is in-process: web/server calls createAgentSessionFromServices directly inside the Node process rather than attaching to a daemon, so there is no daemon wire protocol here and no daemon attach/reconnect. One PrimeBridge instance exists per Node process, held in singleton.ts and pinned on globalThis so Vite SSR full-module restarts do not wipe live sessions, open dialog promises, or SSE subscriptions.

Entry points for modification

  • Add or change a route: edit the matching file under web/server/src/handlers/ and export it from web/server/src/index.ts. Handlers stay thin Request → Response functions; put shared logic on PrimeBridge.
  • Change how runtime events surface to the browser: edit web/server/src/event-mapper.ts (pure mapping) and web/server/src/sse-replay.ts (replay filtering). Keep mapper state on EventMapperState, not module globals.
  • Change session lifecycle or tool-question handling: edit web/server/src/prime-bridge.ts, web/server/src/pending-dialogs.ts, and web/server/src/ring-buffer.ts.
  • Change config resolution: edit web/server/src/prime-config.ts.
  • Add or change a test: add suites under web/server/src/__tests__/ and run them from the web/server package root.

When a change in packages/coding-agent touches the public surface this adapter consumes (createAgentSession, AgentSessionEvent, ExtensionUIContext, IpythonKernelProvisioner, SessionManager), update web/server in the same change, and do not add web-specific exports to coding-agent.

Key source files

File Role
web/server/src/prime-bridge.ts Session coordinator: createSession, resumeSessionByPath/ById, listSessions, getSession, deleteSession, prompt/steer/followUp, abort, setModel, setThinkingLevel, answerDialog, pendingDialogsFor, the slash-command surface (getContextUsage, getSystemPrompt, setSessionName, exportSession, reloadResources, navigateTree, getSessionTree, forkSession), getMessages, and the kernel-readiness gate ensureKernelReady.
web/server/src/event-mapper.ts Pure AgentSessionEvent → ChatStreamEvent[]; toPascalCase tool naming (tool-IPython, tool-Bash, tool-Edit, tool-Thinking); createEventMapperState; toChatMessageFromAssistant/toChatMessageFromUser for transcript hydration.
web/server/src/ring-buffer.ts Frame storage with monotonic seq; replaySince with overflow detection for SSE replay.
web/server/src/pending-dialogs.ts open/answer/cancel/cancelAll for confirm/select/input; 60s timeout auto-cancel emitting tool-Question with output-error.
web/server/src/prime-config.ts AuthStorage, ModelRegistry, per-cwd SettingsManager, DefaultResourceLoader, defaultCwd rebinding, reloadAuth.
web/server/src/singleton.ts getBridge() returns the one per-process PrimeBridge pinned on globalThis.
web/server/src/handlers/chat.ts POST /api/chat: validates ChatRequestSchema, resolves/creates the session, streams NDJSON frames over a ReadableStream.
web/server/src/handlers/chat-events.ts GET /api/chat/events: SSE with ring-buffer replay, Last-Event-ID, heartbeat, and resync-required overflow.
web/server/src/handlers/chat-question.ts POST /api/chat/question: routes an answer to PrimeBridge.answerDialog; 404 when the question is no longer active.
web/server/src/handlers/chat-new.ts POST /api/chat/new: bridge.createSession with cwd/model/thinking level; backgrounded kernel prewarm.
web/server/src/handlers/chat-command.ts POST /api/chat/command: slash-command surface (session, name, context, system-prompt, export, reload, tree, fork, clone).
web/server/src/index.ts Public exports for all handlers, PrimeBridge, PrimeConfig, and the bridge singleton accessors.

Clone this wiki locally