Actor-model agentic UI
Actor-model agentic UI is DevCentr’s second cross-org harness UX contract (after grounded tokens).
In plain language: agent chat is a graph of communicating nodes on disk — not one drifting scroll that pretends to be one topic forever.
A node is an actor: a bounded conversation context with a type, an address, parent/child spawn links, and a mailbox summary coworkers read without loading its full transcript.
Why linear chat is wrong
Today’s chat models assume one thread drifts until the user starts over. Agent harnesses violate that assumption constantly:
-
A coordinator discussion spawns twelve task nodes across repos
-
A task node’s chat tangents into a CLA permissions digression — that should become a discussion child, not noise in the merge transcript
-
Parallel jobs need a grid view; tangents need a graph view — same node store, different projections
Serializing all of that into one scroll repeats aggregate headers and buries blockers. The fix is not "better summaries" alone — it is a persisted node graph the UI renders in parallel.
Node model
| Field | Meaning |
|---|---|
|
Stable node identifier (also actor address suffix) |
|
|
|
Disk path root for this node ( |
|
Parent node id — child always knows where it came from |
|
Child node ids — parent records every node it forked |
|
Edges that are not spawn (waits-on, references, soft dependency) |
|
Mailbox — latest state for subscribers |
|
Which layout snapshot last included this node in the grid (optional; see temporal layout) |
|
When true, temporal fade must not treat the node as idle garbage |
Structs mirror disk (1:1)
In-memory node records are not a shadow cache. meta.json and chat.jsonl are authoritative; runtime structs deserialize from them and serialize back on every mutation. Model invocations receive a slice of that struct (mailbox + recent lines + declared wait subscriptions), not a separate RAM-only transcript.
Spawn on drift
When a node’s chat drifts from its declared topic, the harness spawns a child node:
-
Parent appends child id to
spawned[]; child setsspawnedByto parent id -
Parent and child addresses are written to disk before either accepts new messages
-
Parent chat gets a one-line routing marker; new lines belong to the child
Primary trigger: model-proposed spawn. The model running in the parent should propose a spawn when the user mixes a continuation of the current subject with new ideas that deserve their own bounded context — a common pattern in long agent sessions.
Secondary triggers: user gesture (Spawn tangent), or policy (non-task utterance in a task node).
Boundary ambiguity
When the model cannot tell whether new lines continue the parent topic or fork it, spawn a disambiguation child instead of guessing:
-
Parent pauses routing until the disambiguation node resolves the boundary
-
Disambiguation links parent to one or more candidate children (
discussion,task, …) -
Resolution is recorded in the orchestrator metathread (refs below) and parent
spawned[]
Node types (initial set)
| Type | Role |
|---|---|
|
Root orchestrator; grid owner; continuous top-level voice with summarization; may invoke Mixr before spawning routed task nodes |
|
Exploratory context; may spawn task, discussion, or disambiguation children |
|
Bounded job (repo PR, merge, docs sync); publishes status to grid |
|
Short-lived fork resolver when parent/child boundary is unclear |
More types can extend the same struct — the harness treats type as routing hints, not a separate storage silo.
Disk layout (required)
All graph state is persisted — not RAM-only. I/O cost is acceptable; losing spawn lineage is not.
$CHAT_ROOT/
graph.json # index: node ids, types, spawn edges, related edges
orchestrator/
meta.jsonl # metathread: coordinator voice + refs into node logs
index.json # optional aggregate: nodeId → line ranges, summaries
nodes/
{nodeId}/
meta.json # full node struct (addresses, spawnedBy, spawned, status, …)
chat.jsonl # authoritative messages for this node only
summaries/
latest.json # mailbox
v{n}.json # compaction chain
views/
epoch-1.json # optional layout snapshot (which task nodes, positions)
epoch-2.json
thread.jsonl # optional export/replay merge — not source of truth
Every spawn is two writes minimum (parent meta.json, child meta.json + graph.json edge update) before the child accepts traffic. The harness batches where the filesystem allows; correctness beats micro-latency.
Metathread and audit
Each session has two conversational surfaces:
-
Interactive thread — whichever node the user is typing in (
nodes/{id}/chat.jsonl) -
Orchestrator metathread — cross-node narrative plus line refs into child logs
Ref example (one line in orchestrator/meta.jsonl):
{"kind":"ref","node":"task-bitwarden","line":42,"summary":"CLA blocker cleared"}
Audit = walk metathread refs; do not replay one serialized scroll. Subscribers read mailboxes and peek at referenced lines — not a filtered projection of a mega-log.
Wait graph
ACTOR_WAIT_GRAPH default is warn.
Subagents declare subscriptions ahead of time:
| Mode | Meaning |
|---|---|
|
Mailbox may be stale or partial; subagent may continue independent work |
|
Block until terminal status — merges, releases, irreversible actions |
Example subscription on a node struct: waitOn: [{ node: "task-cla", mode: "warn" }].
UI projections
One graph; multiple views over the same disk store:
-
Grid — filter
type=task, show status dot + summary (replaces the old "inbox" layout) -
Fork panel — selected node; load
nodes/{id}/chat.jsonl -
Graph strip — spawn lineage: parent → children
-
Coordinator / global thread — continuous top-level chat with summarization; not every subagent line
-
Temporal layout — planned view engine (below); embeddable widget, not a second storage model
Hierarchical coordinator mode and global personal-context mode are different views of the same graph, not different stores. Reset in global mode is a scope filter (archive nodes, trim personal context to a bookmark) — not delete disk.
Temporal layout (planned view engine)
Parallel nodes do not naturally "chapter" together. Automatic epoch breaks by clock alone lie to the model when the user left a question unanswered.
Temporal layout is a reusable widget that accepts:
-
node activity timestamps (
lastEventAt,lastTouched) -
optional token-volume series
-
agent-generated timeline bookmarks (keywords, images) as chat progresses
Behaviors (phased):
-
Fade, not delete — idle nodes dim after inactivity unless
awaitingUser: trueor terminal status -
Horizontal scrubber — seek a time window; replay which nodes were visible then
-
Heatmap / token strip — faint overlay on the scrubber; resizable height
-
Bookmark panel — sidebar index; map-style detail as user zooms in
Scopes: a global timeline (all nodes under $CHAT_ROOT) and sub-timelines scoped to one node subtree. The widget mounts in the global shell and inside a node detail panel — same engine, different scope input.
Epoch layout snapshots (views/epoch-N.json) become optional bookmark-freeze artifacts when the coordinator explicitly reframes a batch — not the primary navigation model.
Harness variables
ACTOR_AGENTIC_UI = graph-grid | serialized | off
ACTOR_GRAPH_EPOCHS = on | off
ACTOR_WAIT_GRAPH = warn | enforce | off
ACTOR_NODE_STORE = jsonl-per-node | off
Defaults when ACTOR_AGENTIC_UI=graph-grid: ACTOR_NODE_STORE=jsonl-per-node, ACTOR_WAIT_GRAPH=warn.
| Variable | Behavior |
|---|---|
|
Persist node graph; emit grid updates for task nodes; route detail chat to node forks; coordinator summaries in main thread |
|
Legacy — all lines in one scroll |
|
Single-node mode |
|
Optional layout snapshots when coordinator explicitly reframes a batch (supplements temporal bookmarks) |
|
Emit maybe-stale warnings on sibling mailbox reads; subagent may continue parallel work |
|
Block model calls that depend on non-terminal sibling mailboxes |
|
Per-node |
Migration: PROJECT_INBOX* names are deprecated aliases for one release cycle; map grid-forks → graph-grid, PROJECT_INBOX_CHAPTERS → ACTOR_GRAPH_EPOCHS.
Reference implementation
DevCentr ships a reference runtime at dev-centr/harness (see Agent harness).
| Phase | Ships |
|---|---|
v0 |
Disk layout, spawn API, metathread refs, CLI |
v1 |
Mixr model router, wait-graph ( |
v2 |
Temporal layout engine — scrubber, fade, heatmap, scoped bookmarks |
HCI Nerdz demo remains a static teaching surface; the harness is the live $CHAT_ROOT backend.
Relationship to HCI Nerdz
HCI Nerdz owns the attention surface — grid, graph strip, spawn UX, demo, essay.
DevCentr owns harness routing, disk schema, and $CODE_ROOT/harness.md variables.