Liking cljdoc? Tell your friends :D

Architecture

Engine      (statecharts) → substrate
Query       (EQL/Pathom3) → capability surface
AI          (providers)   → streaming LLM layer
Agent       (statechart)  → per-turn lifecycle
App Runtime (interactive) → shared adapter-neutral UI/session domain
RPC         (transport)   → remote adapter over app-runtime
TUI         (charm.clj)   → terminal adapter over app-runtime
Emacs       (rpc client)  → editor adapter over app-runtime

Components

ComponentRole
engineStatechart infrastructure, system state
queryPathom3 EQL registry, query-in
aiProvider streaming, model registry (Anthropic, OpenAI)
agent-coreLLM agent lifecycle statechart + EQL resolvers
agent-sessionFull coding-agent session: tools, extensions, OAuth, canonical state
app-runtimeShared interactive application runtime for adapter-neutral session/UI semantics
historyGit log resolvers
introspectionEngine queries itself — self-describing graph
rpcTransport, framing, subscriptions, request/response adaptation
tuiJLine3 + charm.clj terminal adapter
emacs-uiEmacs RPC client adapter

Adapter convergence target

The architecture target is:

app-runtime contains everything common between TUI and Emacs. RPC is a transport layer on top of app-runtime, not a second home for session or UI-domain logic.

Current duplication pressure exists where the same user-visible question is answered in more than one adapter path, for example:

  • session selector/tree ordering and fork-point interleaving
  • footer/status semantic composition
  • session summary fragments used by headers/diagnostics
  • picker definitions (/tree, /resume, /model, /thinking)
  • session navigation result shaping (new / resume / switch / fork)
  • background job and context snapshot presentation data

Convergence rule

If both TUI and Emacs need the same answer, app-runtime should answer it once. Adapters should differ only in:

  • rendering
  • local interaction mechanics
  • transport/protocol concerns

Projection delivery rule

For runtime-owned interactive projections, canonical state changes first and public payloads are derived later:

  • session/runtime handlers mutate canonical state
  • handlers emit semantic invalidations such as :projection/context-changed and :projection/ui-changed
  • app-runtime remains the owner of canonical public projection models
  • RPC delivers those projections to subscribed clients by recomputing payloads from current canonical state plus connection-local focus
  • runtime-owned context/session-tree and shared UI updates are event-driven rather than polling-driven

Ownership target

app-runtime owns

  • adapter-neutral session navigation operations
  • focus-scoped session operations parameterized by adapter-owned focus
  • selector/picker models and item ordering
  • footer semantic model
  • shared session-summary/model-label/status fragments for adapter diagnostics/header use
  • context snapshot / session tree model, including canonical session-tree widget projection when adapters need the same rendered structure
  • canonical transcript message reconstruction from journal state
  • transcript rehydration packages and other shared presentation-facing domain projections
  • canonical UI action/result vocabulary
  • shared public summaries for jobs, statuses, and extension UI state where both adapters need the same meaning

rpc owns

  • transport framing
  • subscriptions and event delivery
  • request/response correlation
  • transport-focused handshake / protocol negotiation
  • adaptation of app-runtime models onto the RPC protocol
  • explicit session-id routing whenever the operation can reasonably carry it
  • RPC-local focus pointer only as transport-scoped adapter fallback state
  • subscriber-aware fanout of runtime-owned projection invalidations (:projection/context-changed, :projection/ui-changed) with per-connection payload recomputation

RPC should not be the long-term home for selector semantics, footer semantics, or session navigation domain logic.

tui owns

  • terminal layout
  • key handling
  • local widget/view state
  • adapter-specific rendering concerns

emacs-ui owns

  • buffer rendering
  • minibuffer completion
  • overlays/faces
  • local widget/view state
  • adapter-specific rendering concerns

EQL Introspection Tips

  • Query only attributes that exist in the graph; unknown attrs can cause the whole psi-tool request to fail.
  • For the active system prompt, use:
    • [:psi.agent-session/system-prompt]
  • For runtime UI surface detection (extension/UI branching), use:
    • [:psi.agent-session/ui-type] ; :console | :tui | :emacs
  • For prompt sizing (chars + estimated tokens), use:
    • [{:psi.agent-session/request-shape [:psi.request-shape/system-prompt-chars :psi.request-shape/estimated-tokens :psi.request-shape/total-chars]}]
  • For prompt lifecycle introspection summaries, use:
    • [:psi.agent-session/last-prepared-request-summary :psi.agent-session/last-execution-result-summary]
  • For normalized prompt lifecycle fields, use attrs such as:
    • :psi.agent-session/last-prepared-turn-id
    • :psi.agent-session/last-prepared-message-count
    • :psi.agent-session/last-prepared-tool-count
    • :psi.agent-session/last-execution-turn-id
    • :psi.agent-session/last-execution-turn-outcome
    • :psi.agent-session/last-execution-stop-reason
  • Anthropic prompt caching is session policy projected into request shape:
    • session state stores :cache-breakpoints such as :system and :tools
    • executor projects those into conversation :system-prompt-blocks / tool :cache-control
    • the Anthropic provider emits cache_control only for supported directives ({:type :ephemeral})
  • Avoid non-existent attrs like :psi.agent-session/prompt, :psi.agent-session/instructions, :psi.agent-session/messages unless resolvers are added for them.

State boundary: canonical root vs runtime handles

:state* owns queryable session truth — one atom, one root. Everything else on ctx is a handle to a running subsystem.

Principle: when a subsystem has observable status worth querying (OAuth login state, nREPL endpoint, workflow progress), that status is projected into :state* as canonical data through dispatch. The handle itself stays external.

A runtime handle is any object that:

  • owns internal mutable lifecycle (atoms, watches, threads)
  • performs side-effecting I/O (disk, network, locks)
  • is infrastructure machinery (compiled envs, registries, engines)

Current runtime handles on ctx:

HandleWhat it isProjection in :state*
:agent-ctxagent-core loop, queues, event streamturn context, provider captures
extension registryloaded extensions, flags, event busextension prompt contributions
workflow registryworkflow instances, pump thread, statechart envbackground jobs, workflow public data
:oauth-ctxcredential store, token refresh, file locksauthenticated providers, login status
nREPL serverlive server object[:runtime :nrepl] endpoint metadata
project nREPL registrymanaged project/worktree nREPL runtime handles:psi.project-nrepl/* projected instance state
query contextPathom3 registry, compiled env(is the query infrastructure itself)
engine contextstatechart engines, system state, transition log(is the engine infrastructure itself)
memory contextmemory stores, store registry(is the memory infrastructure itself)

These are all the same kind of thing: opaque subsystems with their own internal mutable lifecycle. They are not queryable domain state.

Dispatch migration status

  • dispatch! is active and queryable via the retained dispatch event log.
  • Current dispatch ownership is partial, not full-system.
  • Migrated families include:
    • statechart action handlers
    • auto flags / ui type
    • model / thinking
    • session name / worktree / cache breakpoints
    • active tools
    • system prompt recomposition
    • prompt contribution mutations
    • startup/bootstrap lifecycle + summary writes
    • context usage / extension prompt telemetry / runtime prompt retargeting
    • rpc trace / oauth projection / recursion projection setters
    • extension UI mutations (widget/widget-spec/status/notify/dialog + renderer registration)
  • Remaining direct mutation pockets still exist outside those migrated slices.
  • Treat dispatch_pipeline_active as "dispatch active for migrated slices" during migration, not yet "all mutations converge through dispatch".

Dispatch sequencing contract

Current agent-session dispatch sequencing for pure handler results is:

  1. handler computes a pure result
  2. apply writes state and surfaces declared effects onto interceptor context
  3. validate checks the post-apply interceptor context
  4. replay trimming may suppress effects
  5. effects execute last

Current scaffold semantics:

  • validation is post-apply, not pre-commit
  • invalid validation suppresses effects but does not roll back already-applied state
  • replay suppresses effects but preserves state application and return values

Current default interceptor ids:

  • :permission
  • :log
  • :statechart
  • :handler
  • :effects
  • :trim-effects-on-replay
  • :validate
  • :apply

Because after fns run in reverse order, the effective after-order is:

  • :apply -> :validate -> :trim-effects-on-replay -> :effects

Dispatch event-log observability

The retained dispatch log now exposes more architectural debugging signal than just event type and timing. Current log entries include:

  • event identity:
    • event type
    • event data
    • origin
    • ext id
  • control flow:
    • blocked?
    • block reason
    • replaying?
    • statechart-claimed?
    • validation error
  • pure-result/effect shape:
    • pure-result kind (:db, :root-state-update, :session-update, etc.)
    • declared effects
    • applied effects
  • bounded state summaries:
    • db-summary-before
    • db-summary-after
  • timing:
    • timestamp
    • duration-ms

Retention/volume tradeoff:

  • the log keeps bounded summaries rather than full root-state snapshots
  • all entries are replay-safe by construction: replay suppresses effects and applies only pure state transforms, so no classification is needed to determine safety
  • this log is the coarse-grained dispatch journal: one summarized entry per dispatch
  • it is the preferred surface for replay-oriented questions like "what events happened?" and "what broad state/effect shape did they produce?"

Canonical dispatch trace observability

In addition to the retained event log, agent-session now keeps a bounded canonical dispatch trace keyed by dispatch-id.

Current trace entry kinds include:

  • :dispatch/received
  • :dispatch/interceptor-enter
  • :dispatch/interceptor-exit
  • :dispatch/handler-result
  • :dispatch/effects-emitted
  • :dispatch/effect-start
  • :dispatch/effect-finish
  • :dispatch/service-request
  • :dispatch/service-response
  • :dispatch/service-notify
  • :dispatch/completed
  • :dispatch/failed

Current guarantees:

  • every dispatch-created trace has one stable dispatch-id
  • dispatch-owned traces now include interceptor stage boundaries, handler-result summaries, and emitted-effect summaries where the flow passes through the dispatch pipeline
  • post-tool flows can create and explicitly thread a dispatch-id through nested extension/service activity
  • managed-service protocol helpers record service request/response/notify events under the explicitly supplied dispatch-id
  • dispatch effect execution records effect start/finish entries including :effect-type
  • trace storage is bounded in memory

Current EQL surface:

  • :psi.dispatch-trace/count
  • {:psi.dispatch-trace/recent [...]}
  • {:psi.dispatch-trace/by-id [...]} from seed [:psi.dispatch-trace/dispatch-id some-id]

Useful attrs on trace entries include:

  • :psi.dispatch-trace/trace-kind
  • :psi.dispatch-trace/dispatch-id
  • :psi.dispatch-trace/event-type
  • :psi.dispatch-trace/interceptor-id
  • :psi.dispatch-trace/method
  • :psi.dispatch-trace/effect-type
  • :psi.dispatch-trace/tool-call-id
  • :psi.dispatch-trace/error-message

This canonical trace is the preferred observability surface for end-to-end runtime coordination. It is the fine-grained complement to the dispatch event-log:

  • use the event-log for replay-oriented, one-entry-per-event journaling
  • use the dispatch trace for correlated stage-by-stage diagnosis under one dispatch-id

Adapter-local debug atoms remain useful for low-level transport diagnosis, but normal architectural debugging should prefer the queryable dispatch trace.

Conforming vertical slice — manual compaction

The first explicit conforming vertical slice target is manual compaction.

Current intended slice flow:

  1. public API entry via manual-compact-in!
  2. dispatch-routed statechart transition via :session/compact-start
  3. synchronous dispatch-owned compaction execution via :session/manual-compaction-execute
  4. dispatch-visible session-data cleanup via :session/compaction-finished
  5. dispatch-routed statechart completion via :session/compact-done

Current intentional boundary:

  • the compaction execution step itself is still synchronous so the caller can receive the compaction result directly
  • the surrounding control flow is dispatch-visible and statechart-visible

Current proof surface for the slice:

  • focused core tests now prove dispatch-visible event sequences for:
    • default stub compaction
    • custom compaction function
    • extension-cancelled compaction
    • extension-supplied compaction result
  • the dispatch event log is the primary slice observability surface; no bespoke local-only debug hooks are required to understand the slice flow

This slice is the proving ground for broader convergence from partial dispatch ownership toward more reference-architecture-conforming vertical behavior.

Next vertical slice — prompt / turn lifecycle

The next target slice after manual compaction is prompt / turn lifecycle.

Current implemented outer shell:

  1. public API entry via prompt-in!
  2. dispatch-visible prompt submission via :session/prompt-submit
  3. dispatch-routed statechart transition via :session/prompt
  4. dispatch-owned request preparation via :session/prompt-prepare-request
  5. runtime execute-and-record boundary via :runtime/prompt-execute-and-record
  6. dispatch-owned response recording via :session/prompt-record-response

Architectural convergence target:

  1. public API entry via prompt-in!
  2. dispatch-visible prompt submission via :session/prompt-submit
  3. dispatch-routed statechart transition via :session/prompt
  4. dispatch-owned request preparation via :session/prompt-prepare-request
  5. runtime execute-and-record boundary via :runtime/prompt-execute-and-record
  6. dispatch-owned response recording via :session/prompt-record-response
  7. dispatch-owned continuation / terminalization via :session/prompt-continue or :session/prompt-finish

Current converged slice semantics:

  • :session/prompt-submit
    • normalize the submitted user message
    • append the user journal entry
    • establish the requested turn as dispatch-visible state
  • :session/prompt-prepare-request
    • project canonical session state into a prepared provider request artifact
    • assemble prompt layers (base prompt, extension contributions, profiles/skills, runtime metadata)
    • project cache policy into system/tool/message cache controls
    • emit the runtime execute-and-record effect
  • :runtime/prompt-execute-and-record
    • perform provider streaming against the prepared request artifact
    • capture provider request/response telemetry
    • dispatch :session/prompt-record-response with the shaped execution result
  • :session/prompt-record-response
    • append assistant output deterministically
    • record usage / telemetry / tool-call outcomes
    • decide continuation from canonical recorded state
  • :session/prompt-continue / :session/prompt-finish
    • route tool execution or follow-up turn continuation
    • return the session lifecycle to its terminal state for the turn

Current intentional boundary:

  • prompt journal append, request preparation, assistant result recording, and continuation decisions are now dispatch-visible, while provider streaming and turn accumulation remain concentrated in the runtime execute-and-record boundary
  • the active slice currently reads as prepare -> execute-and-record -> continue/finish; this is an intentional convergence waypoint toward the stricter prepare -> execute -> record model
  • request preparation is the architectural center for prompt lifecycle convergence; prompt layering, cache breakpoint policy, and provider request shaping should become explicit there rather than remain distributed across string concatenation and runtime orchestration paths
  • tool execution is now dispatch-owned end-to-end:
    • :session/tool-run composes two dispatch-owned phases:
      1. :session/tool-execute-prepared — may run concurrently, emits start/executing lifecycle, performs runtime tool execution through :runtime/tool-execute, and returns a shaped result without final recording
      2. :session/tool-record-result — records the final tool result in deterministic tool-call order, including lifecycle projection, telemetry, journal append, and agent-core tool-result recording
    • the executor now owns only batch scheduling and deterministic ordered recording, not tool transaction semantics

Adapter convergence roadmap

Near-term architectural direction:

  1. move shared selector/session-tree semantics into app-runtime
  2. move shared footer semantic projection into app-runtime
  3. define a canonical adapter-neutral picker/action vocabulary in app-runtime
  4. converge navigation result shaping, context snapshots, and transcript rehydration packages into app-runtime
  5. leave RPC as protocol adaptation and adapters as rendering/mechanics

What success looks like:

  • TUI and Emacs consume the same selector, footer, navigation, context, and transcript rehydration models
  • RPC projects shared runtime models onto transport events instead of owning their semantics
  • adapter bugs no longer require re-solving shared domain questions in multiple places
  • explicit session-id routing becomes the default for targetable RPC operations, with adapter focus used only as fallback

Roadmap

  • ✓ Engine + Query substrate
  • ✓ AI provider layer (Anthropic, OpenAI)
  • ✓ Agent core loop
  • ✓ Coding-agent session
  • ✓ TUI (charm.clj / JLine3)
  • ✓ Extension system + Extension UI
  • ✓ OAuth (PKCE, Anthropic, OpenAI)
  • ✓ Git history resolvers
  • ✓ Session persistence
  • ◇ HTTP API (openapi + martian)

Can you improve this documentation? These fine people already did:
Hugo Duncan & Test Author
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close