P1: both merge parents shipped SCHEMA_VERSION=3 for different layouts (surface columns vs seed_length), so an on-disk 3 was ambiguous and wrongly accepted. Bump to 4 (merged layout) so the version check rejects both sibling v3s. P2: a surface-eligible event with no surfaceOp lands in the log but vanishes from deriveMessages() (surface is the sole derivation path). The typed append overload enforces the marker only when the type arg is a literal; it collapses to optional when widened to the union (a caller iterating raw events). Guard at runtime in both append() and the seed constructor — no backward-compat for surface-less logs. Shared seed fixtures carry surfaceOp explicitly and the appendLog helper forwards it verbatim (no synthesized default). Exports isSurfaceEligibleType. Regression tests for all three, each verified to fail on the unfixed code. Gates: typecheck, test (1115), snapshot (14), doc-sync, lint, build, hygiene green.
core/ — product API spine
The packages every harness build is assembled from: the session log, the system-prompt assembly, the tool registry, the agent vocabulary, and the one concrete loop that drives them. These are product packages — the stable surface plugins and consumers build against.
| Package | Role | ctx key |
|---|---|---|
session/ |
Event-sourced session log + in-memory store | ctx.sessions |
system-prompt/ |
Prompt-section + tool-schema assembly registry | ctx.systemPrompt |
tools/ |
Tool registry + tools/execute waterfall |
ctx.tools |
agent/ |
Agent interface, registry, agent/* event vocabulary |
ctx.agents |
agent-loop/ |
The concrete loop plugin: ReactLoopAgent + the loop driver |
ctx.agentLoop |
agent-core/ |
Bundle plugin: the providerless/executor-less/UI-less spine as code | (loads the spine) |
agent-loop is the one concrete implementation of the agent seam and lives here because it is the harness's default product loop; everything else in core/ is interface/vocabulary. Plugins depend on the agent vocabulary, never on agent-loop directly, so the loop stays swappable.
agent-core is the composition counterpart: one bundle plugin that loads the whole providerless spine (timer + llm + sessions + system-prompt + tools + agents + invariants + tool-bash + agent-loop) and forwards agent-loop's agents list as its own config. App packages (ui/stdio-agent, ui/acp-agent) consume it and add only a front door; a leaf adds only the swappable backends. It lives in core/ because it composes exclusively core/ + interface packages and ships no provider, executor, or UI of its own.