跳到主内容
Spark
中文

Core capabilities

Four capabilities: streaming chat, tool-call visualization, human approval, and one protocol across all four surfaces.

Streaming chat

Token-level incremental rendering. Three live-only event types (assistant.delta / reasoning.delta / tool.progress) go straight to the UI without persistence; the turn is finalized on disk by assistant.message when it ends.

All 27 events arrive over one SSE endpoint (GET /api/event, resuming from since=seq after a disconnect). Each surface folds the stream into UI state with the same applyEvent reducer — the protocol layer is runtime code, not a types package. Note the two "projections" differ: the UI projection is the per-surface reducer; the engine-side Projector projects model context (surface events → LlmMessage).

typescript
// packages/protocol/src/events.ts — 三分类是类型级事实,不是注释
export type LiveOnlyEventType =
  | 'assistant.delta'
  | 'reasoning.delta'
  | 'tool.progress'

/** surface 事件强制带 surface:true(编译期纪律) */
export type SurfaceEventType = 'user.message' | 'assistant.message'

export type DurableEventType = Exclude<SparkEventType, LiveOnlyEventType>

// 词表共 27 种(EventSchemas 键数):durable 24 / live-only 3;surface 2
// 生成物 apps/docs/events.md 由 CI 重跑并 git diff --exit-code 校同步

Tool call visualization

Every tool call is a collapsible execution block in the session flow: input, output, duration and error flag shown in full, with error codes on screen when things fail.

The tool state machine runs started → [progress] → completed. tool.completed carries isError and durationMs, both durable — replaying the file rebuilds exactly what was on screen, no extra instrumentation needed.

typescript
// packages/protocol/src/events.ts — 工具(状态机 started → [progress] → completed)
'tool.started': z.strictObject({
  turnId: TurnIdSchema,
  callId: CallIdSchema,
  name: z.string(),
  input: z.unknown(),
}),
'tool.progress': z.strictObject({
  turnId: TurnIdSchema,
  callId: CallIdSchema,
  chunk: z.string(),
}), // live-only
'tool.completed': z.strictObject({
  turnId: TurnIdSchema,
  callId: CallIdSchema,
  output: z.unknown(),
  isError: z.boolean(),
  durationMs: z.number().int().nonnegative(),
}),

Human approval (fail-closed)

Write-class tools raise an approval card inline at the call site. Timeouts, errors and interrupts always settle to reject, never allow.

permission.asked carries requestId / action / resource / reason and persists durably; the user's reply lands as permission.resolved with exactly three values: once / always / reject. The engine settles timeouts to reject — deny-by-default is a code path, not a slogan. Approval events are log-only and never enter model history.

typescript
// packages/protocol/src/primitives.ts
export const PermissionReplySchema = z.enum(['once', 'always', 'reject'])

// packages/engine/src/permission/service.ts — 超时即拒绝(fail-closed)
timer: setTimeout(() => {
  void this.settle(entry, false, 'reject', 'timeout')
}, this.deps.timeoutMs),

// timeoutMs 来自 ~/.spark/spark.json 的 engine.permissionTimeoutMs,缺省 300_000(5min)
// origin: 'reply' | 'timeout' | 'abort' | 'shutdown' | 'cascade' | 'mode-change'

One protocol, four surfaces

Web, desktop, CLI, mobile and the mini app all share @spark/protocol: the event vocabulary, zod schemas, the applyEvent reducer and Transport live in one package — runtime code, not type declarations.

Transport is the only data-channel abstraction on the frontend; HttpTransport (SSE) and MockTransport are structurally mirrored, so development continues at full speed without a backend. @spark/sdk adds two sub-entries: the root entry speaks HTTP (zero engine dependency, browser-safe) and ./inprocess connects to the engine in-process (engine as an optional peer).

typescript
// packages/protocol/src/transport.ts — 接口面节选(全量 84 个方法)
export interface Transport {
  /** 订阅事件流;返回退订函数 */
  onEvent(handler: (e: SparkEventEnvelope) => void): () => void
  sendMessage(sessionId: SessionId, text: string, opts?: SendMessageOptions): Promise<SubmitOutcome>
  interrupt(sessionId: SessionId): Promise<void>
  replyPermission(requestId: RequestId, reply: PermissionReply, feedback?: string): Promise<void>
  /** GET /api/sessions/:id:meta + durable 事件(seq 升序——冷启动回放数据源) */
  getSession(sessionId: SessionId, query?: SessionEventsQuery): Promise<SessionDto>
  /** POST /api/pair:短码兑长效 token(移动端鉴权自举,ADR D24) */
  redeemPair(body: PairRedeemBody): Promise<PairTokenDto>
  dispose(): void
}

// 实现:HttpTransport(protocol,SSE)/ MockTransport(apps/web 开发态)