-
Notifications
You must be signed in to change notification settings - Fork 0
packages web server
Active contributors: Zachary BENSALEM
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.
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
| 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. |
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)
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.
- 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/appTanStack Start routes call these handlers; see web-app. - Rendering:
web/designrenders the streamedChatStreamEventframes (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.
- Add or change a route: edit the matching file under
web/server/src/handlers/and export it fromweb/server/src/index.ts. Handlers stay thinRequest → Responsefunctions; put shared logic onPrimeBridge. - Change how runtime events surface to the browser: edit
web/server/src/event-mapper.ts(pure mapping) andweb/server/src/sse-replay.ts(replay filtering). Keep mapper state onEventMapperState, not module globals. - Change session lifecycle or tool-question handling: edit
web/server/src/prime-bridge.ts,web/server/src/pending-dialogs.ts, andweb/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 theweb/serverpackage 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.
| 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. |