Skip to content

Latest commit

 

History

History
220 lines (197 loc) · 7.89 KB

File metadata and controls

220 lines (197 loc) · 7.89 KB

OpenSeek Architecture

How the pieces fit together: the module map, the core data model, and the life of one agent turn. Package-level details live in each package's README (see the table in the root README); this page is the map, not the territory. Diagrams are Mermaid, rendered natively by GitHub.

System overview

One shared engine, three frontends. cmd/openseek is the engine (headless automation: run/serve/review/subrun/mcp/sessions; the desktop app and the viz server are separate executables that spawn or read from it): its serve mode reads JSONL commands on stdin and streams typed JSONL events (bobzhang/openseek_protocol) on stdout. The terminal UI (openseek_tui, in its own repository, moonbitlang/openseek_tui), the desktop app, and headless run all drive that same engine and event stream. Durable state lives in append-only session files that the visualizer reads directly.

flowchart LR
  subgraph frontends [Frontends]
    TUI["openseek_tui — terminal UI<br/>(separate repo: moonbitlang/openseek_tui)"]
    DESKTOP["openseek_desktop — desktop app<br/>(CEF shell + JS frontend)"]
    VIZ["inspect + viz —<br/>session visualizer (browser)"]
  end

  subgraph engine ["openseek engine (cmd/openseek)"]
    SERVE["serve / run / review / sessions / mcp"]
    AGENT["agent — turn loop"]
    TOOLS["agent_tool — registry:<br/>shell·shell_output·shell_stop<br/>read·edit·multi_edit·write<br/>remove·plan·goal·finish"]
    MCP["mcp — client + tool bridge<br/>(mcp__server__tool)"]
    PROMPT["prompt + agent_skill —<br/>system prompt & skills"]
    SESSION["agent_session (+ store) —<br/>durable event log"]
  end

  subgraph outside [Outside]
    API["DeepSeek / Kimi chat API"]
    MCPSRV["MCP servers<br/>(stdio & Streamable HTTP)"]
    FILES[("openseek_session-&lt;id&gt;.jsonl<br/>under .openseek/")]
  end

  TUI -- "JSONL commands (stdin)" --> SERVE
  DESKTOP -- "JSONL commands" --> SERVE
  SERVE -- "protocol events (stdout)" --> TUI
  SERVE -- "protocol events" --> DESKTOP
  SERVE --> AGENT
  PROMPT --> AGENT
  AGENT -- "chat + tool schemas" --> API
  AGENT -- dispatch --> TOOLS
  MCP -- "tool definitions<br/>(extra_tools)" --> TOOLS
  MCP --> MCPSRV
  AGENT -- append --> SESSION
  SESSION --> FILES
  FILES --> VIZ
Loading

The engine ↔ frontend wire contract is bobzhang/openseek_protocol: commands in (prompt, steer, cancel, compact, goal), events out (steps, deltas, tool results, goal/plan reminders, compaction, terminals). The protocol module is backend-neutral so any frontend — including the JS ones — can decode it; only protocol/emit (the writer) is native-only.

Core data model

The durable log and the tool boundary are the two type families everything else leans on.

classDiagram
  class Session {
    id: SessionId
    system_prompt: String
    events: Vector~SessionEvent~
    append(SessionItem) Session
    chat_messages() Array~ChatMessage~
    compact(content, from, to) Session
    current_goal() StandingGoal?
  }
  class SessionEvent {
    sequence: Int
    unix_ms: Int64
    item: SessionItem
  }
  class SessionItem {
    <<enumeration>>
    User(UserMessage)
    Assistant(AssistantMessage)
    Tool(ToolResult)
    Runtime(RuntimeNotice)
    Summary(SessionSummary)
    Terminal(TurnTerminal)
  }
  class TurnTerminal {
    <<enumeration>>
    Finished(String)
    Aborted(String)
    Interrupted(String)
    Failed(String)
  }
  Session "1" *-- "many" SessionEvent
  SessionEvent *-- SessionItem
  SessionItem ..> TurnTerminal

  class Tools {
    find(name) AgentToolDefinition?
    function_tools() Array~ToolDefinition~
  }
  class AgentToolDefinition {
    name: String
    description: String
    schema: JsonSchema
    execute: ToolExecutor
    control: Bool
  }
  class ToolExecutor {
    <<enumeration>>
    Sync(fn)
    Async(async fn)
  }
  class ToolAction {
    <<enumeration>>
    Respond(ToolOutput)
    Control(Finish | Abort)
  }
  class AgentRuntime {
    workspace_root() String
    queue_steer(SteerInput)
    drain_steers() Array~SteerInput~
  }
  Tools "1" *-- "many" AgentToolDefinition
  AgentToolDefinition *-- ToolExecutor
  ToolExecutor ..> ToolAction : returns
Loading

Key invariants:

  • The session is the only memory. Session::chat_messages() projects the event log into the provider's chat shape each turn; nothing conversational lives outside the log. Compaction appends a Summary item covering a [from, to] range — raw events are never rewritten — and the projection replaces covered events with their summary while keeping uncovered events and every surviving summary.
  • Events are append-only and sequenced. SessionEvent.sequence is contiguous; the store (agent_session/store) persists a header line plus one JSON event per line, so a torn final line is recoverable (agent_session/log reads leniently).
  • Tools are data. A tool is a name, a JSON schema, and an executor. Normal tools return Respond(ToolOutput); control tools (finish) end the turn via Control. MCP tools enter the same registry namespaced as mcp__<server>__<tool>, so the loop dispatches them identically.

One turn, end to end

A serve turn as driven by the TUI (headless run is the same loop without the stdin command pump):

sequenceDiagram
  participant UI as TUI / desktop
  participant Serve as openseek serve
  participant Turn as agent turn loop
  participant DS as DeepSeek/Kimi API
  participant Tool as agent_tool / mcp
  participant Store as session store

  UI->>Serve: {"command":"prompt","text":…}
  Serve->>Turn: run_turn_in_scope(session, task)
  Turn->>Store: append User(task)
  loop until a plain final answer, a control tool, or the context ceiling
    Turn-->>UI: AgentStep
    Turn->>DS: chat(messages, tool schemas)
    DS-->>Turn: streamed deltas, then the assistant message
    Turn-->>UI: AssistantDelta stream · ReasoningMessage · AssistantMessage
    alt assistant returned tool calls
      Turn->>Store: append Assistant (message + tool calls)
      Turn->>Tool: execute_tool_call(name, args)
      Tool-->>Turn: ToolAction (Respond / Control)
      Turn->>Store: append Tool result
      Turn-->>UI: ToolResult event (Respond actions)
    else plain final answer
      Note over Turn,Store: no Assistant append — the answer persists only in the Terminal
    end
    Note over Turn: queued steers/notices are folded in between steps
  end
  Turn->>Store: append Terminal(Finished(answer))
  alt normal completion
    Turn-->>UI: AgentFinished(answer)
  else context ceiling
    Turn-->>UI: ContextYield ("[context ceiling]" answer)
  end
Loading

Two pressure valves shape long turns:

  • Steps: with --max-steps unset, a turn is bounded by the model's context window instead of a step count — when the window fills, the loop checkpoints (auto-compaction) and yields a [context ceiling] answer that the next turn continues from.
  • Steering: steer commands and background-job completion notices are queued losslessly in AgentRuntime and surfaced to the model at the next step boundary, so a running turn can absorb new instructions without restarting.

Where things live on disk

.openseek/                       # per-workspace session root (--session-root)
  sessions/<id>/
    openseek_session-<id>.jsonl  # header line + append-only events
    session.lock                 # store coordination file
.openseek/skills/<name>.md       # workspace skills (shadow global ones)
~/.openseek/skills/              # global skill library (--global-skills-dir)

The visualizer (openseek sessions in a browser: inspect) and sessions list both read these files directly. The engine's durable state is exactly these per-session directories; the desktop app additionally keeps its own metadata under the session root (workspaces.json, archived/sessions) and its settings in webview localStorage.