跳到主内容
Spark
中文

Open sourceMIT · One protocol, four surfaces · Event-sourced

Web Workbench

Streaming chat, tool-call visualization, fail-closed human approval. 27 event types drive four surfaces in real time; sessions persist as append-only JSONL — replayable, forkable, rollback-safe.

MIT License · Node.js ≥ 24 · Binds to 127.0.0.1:4318 · Data in ~/.spark

Give the agent a job

Read first, then edit — every change goes through approval. Write-class tools and bash require human confirmation — once / always / reject; an unanswered ask settles to reject (fail-closed).

permission.resolved { reply } · fail-closed

For developers

One protocol drives all four surfaces.

The event vocabulary, zod schemas, applyEvent reducer and Transport all live in @spark/protocol — runtime code, not a types package. Reconnects resume by seq; a replay rebuilds the full UI.

27
Event vocabulary
28
Built-in commands
4
Surfaces
import { createClient }  from "@spark/sdk";// HTTP client: assembles HttpTransport + convenience groups (ADR D30)const client = createClient("http://127.0.0.1:4318");// The event stream is the single source of UI state: SSE resumes by seqclient.events.subscribe((envelope) => {  // 27 event types fold into UI state via applyEvent});

Core capabilities

Four capabilities share one event vocabulary: streaming render, the tool state machine, the approval card and four-surface sync are all branches of the same reducer.

01

Streaming chat

Model output is pushed as assistant.delta events, token by token; each surface folds the stream into UI state with the same applyEvent reducer. Delta events are live-only: never persisted, and the durable assistant.message rebuilds the final state after reconnect.

assistant.delta · reasoning.delta · tool.progress = live-only

Web surface: session list on the left, streaming conversation panel on the right
02

Tool call visualization

The tool state machine started → progress → completed renders end to end: input, output, duration (durationMs) and error flag (isError) are all event fields. Failures surface error codes (E_PATH_OUTSIDE, E_SANDBOX_UNAVAILABLE and friends) on the same screen.

tool.started { input } → tool.completed { output, isError, durationMs }

Desktop surface: tool call details inside the Electron shell
03

Human approval

permission.asked raises a card; the only replies are once / always / reject. Timeouts, errors and interrupts always settle to reject (fail-closed). Approval events are log-only: never in model history, but durably persisted so every decision can be replayed.

permission.resolved { reply: 'once' | 'always' | 'reject' }

Mobile surface: approval dialog over the conversation flow
04

One protocol, four surfaces

Web, Desktop, CLI and Mobile (mini app included) share the 27-event vocabulary and the Transport interface of @spark/protocol. The vocabulary grows via declaration merging with the schema registry as the single source, so the four surfaces cannot drift apart.

@spark/protocol · 27 events · Web / Desktop / CLI / Mobile

CLI surface: Ink 7 terminal TUI with a single-column session flow

Architecture

Five layers: surface UIs / protocol / server / engine / session files.

apps/*Four surface UIs (Web · Desktop · CLI · Mobile)
@spark/protocol27-event vocabulary · zod schemas · Transport
apps/serverFastify · SSE · binds to 127.0.0.1 only
@spark/engineInputQueue → RunLoop → ToolPipeline
sessions/*.jsonldurable append-only · fully replayable

↓ data flow: user input → engine → events → UI

Security model

Loopback by default; approval timeouts, errors and interrupts all settle to reject; out-of-root paths are blocked before approval; events persist append-only end to end.

  1. Loopback by default

    The server listens on the loopback address only; all session data stays in ~/.spark/. When external exposure is needed (non-loopback binding), pairing auth is mandatory: a 6-digit short code exchanges for a long-lived token.

    server: { host: '127.0.0.1', port: 4318 }
  2. Fail-closed approval

    Write-class tools and bash require human confirmation with exactly three replies: once / always / reject. Timeouts, errors, interrupts, cascades and mode switches all settle to reject, audited as system; bash is fully approved by default.

    timeout → permission.resolved { reply: 'reject' }
  3. Hard boundary first

    The allowed root is the cwd; a path that resolves outside throws E_PATH_OUTSIDE before approval, not after it in an audit log. The optional bash sandbox prefixes platform wrappers (Linux bwrap / macOS Seatbelt) — unavailable wrapper means refused, never a bare run.

    resolveInRoot → E_PATH_OUTSIDE
  4. Durable & auditable

    append-only JSONL: line 0 is the header, each following line is one event envelope, and seq equals the file line number. Append-only means replayable, forkable and rollback-safe (checkpoint).

    ~/.spark/sessions/<mungeDir(cwd)>/<ts>_<id>.jsonl

Two ways to start

Run it in a terminal

Before the npm release lands, clone the source: install once, build one server bundle, one command into the TUI.

  • Node.js ≥ 24 · pnpm 9 (from source)
  • node apps/cli/dist/main.js up → TUI, server binds to 127.0.0.1:4318
  • Configure a model once before the first turn: ~/.spark/models.json
  • Exit tears down the server; sessions persist in ~/.spark/sessions/

Read the source and docs first

Start from the protocol package: the event vocabulary and Transport are the shared runtime core of all four surfaces.

  • 27 event types · applyEvent reducer unit-tested one by one
  • MockTransport mirrors HttpTransport; the frontend runs without a backend
  • Contract cases and vocabulary-page artifacts are committed, CI checks sync
  • MIT · keep copyright notices when reusing code

MIT license · binds to 127.0.0.1 only · no account system