Files
penguin-harness/packages/docs/content/architecture.en.md
T
Yaowei Zheng 45bfae6e94 Initialize repository with harness code and assets
Initial import of all source code, config, and README assets: the
packages workspace (cli, core, server, web, docs, landing, skills),
build scripts, tooling config, and CI workflows.

Includes the data-layout revision made on this branch: the local data
root defaults to ~/.penguin/data (PENGUIN_HOME still overrides; the
installer keeps its binaries in ~/.penguin), and every Agent lives
under <project>/agents/<agent>/ — path helpers, the three
agent-enumeration scans, the system prompt, built-in Skills, tests
and docs all follow the new layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ihk8iQuo3kv2aPjAYEPuR
2026-07-19 14:06:53 +08:00

11 KiB

title, description
title description
Architecture How the three-interface boundary, the context_engine and OmniMessage organize the SDK, CLI, Server and Web App into one system.

PenguinHarness is a pnpm monorepo whose center is the execution engine in @prismshadow/penguin-core; the CLI, the Server and the Web App are just different "Human implementations" of that same engine.

Layers

┌─────────────┐  ┌─────────────────────────────┐
│   CLI       │  │  Web App (React SPA)        │
│  (penguin)  │  │    ↑ OmniMessage over SSE   │
│             │  │  Server (Hono + SQLite)     │
└──────┬──────┘  └──────────────┬──────────────┘
       │      session.run(...)  │        ← Human boundary
┌──────┴────────────────────────┴──────────────┐
│  core: context_engine (ReAct loop)           │
│    ├── LLMInterface ──→ AgentHub ──→ models  │
│    ├── EnvironmentInterface ──→ builtin tools│
│    ├── Agent State (editable files)          │
│    └── Trace (append-only JSONL)             │
└──────────────────────────────────────────────┘
Package Role
packages/core SDK and engine: context_engine, OmniMessage, LLM/Environment interfaces, State and Trace
packages/cli Terminal Human implementation: REPL and one-shot runs, embeds core in-process
packages/server Web Human implementation: HTTP for input and approvals, SSE for the output stream
packages/web Rendering SPA: streams by the OmniMessage protocol, contains no engine logic
packages/skills The built-in skill library (a set of SKILL.md files)

Division of responsibilities

To place a design in a layer, ask where its source of truth lives. The four layers split as:

Layer Owns Does not own
SDK (core) Protocol and execution — everything that makes messages flow persisted user state, multi-user, any rendering
Server The resident process and the multi-user runtime engine logic (fully delegated to the SDK)
File layer (~/.penguin/data) Everything editable and everything recorded any computation
CLI / Web Rendering and interaction business state

Item by item (design → owner → carrying file or module):

Design Owner Carried by
The OmniMessage protocol, message parsing and partial aggregation SDK core/src/omnimessage/ — see The OmniMessage Protocol
The ReAct loop, carry-over, reconnect, compaction SDK core/src/engine/context-engine.ts — see The Agent Loop
The approval mechanism (one decision per tool_call) SDK ApproveFn (core/src/interfaces.ts); the concrete mode is injected by CLI/Server
Tool execution and centralized close-out SDK core/src/environment/ — see Tools & Approval
Model access (provider protocol adaptation) SDK → AgentHub core/src/llm/ + @prismshadow/agenthub — see Models & Providers
Trace writing and Session-recovery logic SDK core/src/trace/ (the records themselves live in the file layer)
Subagent spawning and message forwarding SDK the run_subagent tool + the injected SubagentRunner
Multi-user auth and Project authorization Server server/src/auth/, server/src/services/project-service.ts
Session indexing, per-Session mutex, SSE forwarding Server server/src/runtime/ — see Server API
Scheduled tasks (execution) Server server/src/runtime/scheduler.ts; the task definitions live in the file layer at agent_state/schedule/*.toml
Approval-mode persistence and manual decisions Server server/src/runtime/approvals.ts + SQLite
Usage persistence and cost statistics Server server/src/runtime/usage-recorder.ts, services/usage-service.ts
Agent behavior definition (prompts, runtime params) File layer agent_state/system_config.yaml, AGENTS.md — see the Configuration Reference
Skills File layer agent_state/skills/<name>/SKILL.md — see Skills
Secrets File layer Vault: agent_state/.vault.toml; model credentials: .project_config.toml (both 0600)
The model table and the default model File layer <project>/.project_config.toml
Run history (the sole source of truth for recovery) File layer traces/<date>/<session>_<index>.jsonl — see Sessions & Traces
Benchmark cases and scores File layer benchmarks/<id>/ — see Self-Improvement
Snapshots File layer snapshots/v<version>.tar.gz; the export/import service lives in the Server
Streaming rendering, approval UI, charts CLI / Web cli/src, web/src (pure rendering, no engine logic)

The one-line rule: what is editable or recorded lives in files; what makes messages flow lives in the SDK; what needs a resident process and multiple users lives in the Server; the rest is rendering. The Server's SQLite stores only indexes and aggregates — it never competes with the file layer as a source of truth.

Source layout

How each package is organized (single-purpose files, split by layer; every file's header comment is its design note):

packages/
├── core/src
│   ├── agent.ts / session.ts       # the createAgent composition layer and Session (run / compact / generateTitle)
│   ├── session-title.ts            # one-shot title generation (out-of-band LLM call, never in Trace)
│   ├── engine/context-engine.ts    # ReAct loop orchestration: turn lifecycle, approvals, carry-over, reconnect, compaction
│   ├── omnimessage/                # types.ts protocol types · builders.ts constructors · aggregate.ts partial aggregation
│   ├── llm/                        # generative-model.ts AgentHub adapter · tool-call-ids.ts id uniqueness
│   ├── environment/                # environment.ts execution close-out · tools/ registry, 6 builtin tools, background sessions
│   ├── state/                      # paths · default-config · project-config · model-catalog
│   │                               # agent-state (Skill install, prompt assembly) · agent-vault · builtin-agents
│   ├── trace/                      # writer.ts append-only JSONL · resume.ts replay-based recovery
│   └── internal/                   # date and Session helpers
├── cli/src                         # commander entry + run / chat / config / serve commands and approval prompts
├── server/src                      # app assembly · db (node:sqlite) · auth · http/routes · runtime · services
├── web/src                         # api client · state · lib/omni stream rendering · components · feature pages
├── skills/                         # loader + the skills/<name>/SKILL.md library
├── landing/                        # the product landing page (with the blog)
└── docs/                           # this documentation site

The internals of server and web are detailed on Server API and the Web App Guide.

The three-interface boundary

The context_engine is the heart of the system and does exactly two things: it maintains the linear message history, and it orchestrates the event flow between three interfaces. It speaks only OmniMessage and performs no protocol conversion:

  • Human — the user-side boundary. It is deliberately not an interface class: the SDK's single entry point session.run(newMessages, { approve, signal }) is the Human boundary. Input is a list of new OmniMessages plus an approval callback; output is streamed OmniMessages. The CLI and the Server are its two shipped implementations.
  • LLM — the model-side interface (LLMInterface). Translates OmniMessage to requests against the AgentHub model gateway and streamed events back into OmniMessage. All provider protocol adaptation happens inside AgentHub; core never imports a vendor SDK.
  • Environment — the tool-execution interface (EnvironmentInterface). Runs approved tool calls and streams results back.

Why this boundary matters: the kernel contains no provider, tool or UI specifics, so each side swaps by configuration (local shell today, other sandboxes tomorrow; CLI, Web, or programmatic callers) without touching the core. See Core Interfaces for the signatures.

Data flow of one Task

  1. Human hands a Prompt (a list of OmniMessages) to session.run;
  2. the engine issues a Request: the LLMInterface streams partial_* fragments and complete messages;
  3. every complete tool_call triggers one approve decision; approved calls run concurrently in the Environment;
  4. tool outputs are re-fed in their original order as the next Request's input;
  5. the Task ends when a turn produces no tool_call (the final answer).

Every message and event flows to two destinations at once: streamed live to the Human, and appended to the Trace. Loop details (interruption, reconnect, compaction) are on The Agent Loop.

The state layer

Below the engine sits a purely file-based state layer rooted at ~/.penguin/data (override with PENGUIN_HOME), organized as <project>/agents/<agent>/:

  • Agent State — the agent_state/ directory: system_config.yaml, AGENTS.md, Skills, Vault. An Agent's entire behavior is editable files.
  • Project config — .project_config.toml: the model table and credentials; model identity is always the (provider, model_id) pair.
  • Trace — the traces/ directory: append-only JSONL, the single source of truth for Session recovery.

The Server keeps an additional SQLite index (users, authorization, usage stats) but never duplicates the file layer's facts — the CLI, SDK and Web share one data directory and can be mixed freely.

Key design decisions

  • One protocol, three jobs: OmniMessage is simultaneously the SDK's external interface, the Trace on-disk format and the engine's internal currency — what streams, what is stored and what the model sees are the same thing.
  • Errors converge into messages: the LLM and Environment never throw into the engine; results carry a five-value stop_reason (completed | failed | aborted | timeout | malformed), and only LLM-side timeout / malformed trigger an in-run reconnect.
  • A thin model layer: core defines only LLMInterface; provider adaptation lives entirely in AgentHub (@prismshadow/agenthub), which is what makes any OpenAI-compatible endpoint reachable. See Models & Providers.

Source entry points: packages/core/src/engine/context-engine.ts, packages/core/src/interfaces.ts.