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
| Component | Role |
|---|---|
engine | Statechart infrastructure, system state |
query | Pathom3 EQL registry, query-in |
ai | Provider streaming, model registry (Anthropic, OpenAI) |
agent-core | LLM agent lifecycle statechart + EQL resolvers |
agent-session | Full coding-agent session: tools, extensions, OAuth, canonical state |
app-runtime | Shared interactive application runtime for adapter-neutral session/UI semantics |
history | Git log resolvers |
introspection | Engine queries itself — self-describing graph |
rpc | Transport, framing, subscriptions, request/response adaptation |
tui | JLine3 + charm.clj terminal adapter |
emacs-ui | Emacs RPC client adapter |
The architecture target is:
app-runtimecontains everything common between TUI and Emacs. RPC is a transport layer on top ofapp-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:
/tree, /resume, /model, /thinking)new / resume / switch / fork)If both TUI and Emacs need the same answer, app-runtime should answer it once.
Adapters should differ only in:
For runtime-owned interactive projections, canonical state changes first and public payloads are derived later:
:projection/context-changed and :projection/ui-changedapp-runtime remains the owner of canonical public projection modelsapp-runtime ownsrpc ownsapp-runtime models onto the RPC protocolsession-id routing whenever the operation can reasonably carry it:projection/context-changed, :projection/ui-changed) with per-connection payload recomputationRPC should not be the long-term home for selector semantics, footer semantics, or session navigation domain logic.
tui ownsemacs-ui ownspsi-tool request to fail.[:psi.agent-session/system-prompt][:psi.agent-session/ui-type] ; :console | :tui | :emacs[{:psi.agent-session/request-shape [:psi.request-shape/system-prompt-chars :psi.request-shape/estimated-tokens :psi.request-shape/total-chars]}][:psi.agent-session/last-prepared-request-summary :psi.agent-session/last-execution-result-summary]: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:cache-breakpoints such as :system and :tools:system-prompt-blocks / tool :cache-controlcache_control only for supported directives ({:type :ephemeral}):psi.agent-session/prompt, :psi.agent-session/instructions, :psi.agent-session/messages unless resolvers are added for them.: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:
Current runtime handles on ctx:
| Handle | What it is | Projection in :state* |
|---|---|---|
:agent-ctx | agent-core loop, queues, event stream | turn context, provider captures |
| extension registry | loaded extensions, flags, event bus | extension prompt contributions |
| workflow registry | workflow instances, pump thread, statechart env | background jobs, workflow public data |
:oauth-ctx | credential store, token refresh, file locks | authenticated providers, login status |
| nREPL server | live server object | [:runtime :nrepl] endpoint metadata |
| project nREPL registry | managed project/worktree nREPL runtime handles | :psi.project-nrepl/* projected instance state |
| query context | Pathom3 registry, compiled env | (is the query infrastructure itself) |
| engine context | statechart engines, system state, transition log | (is the engine infrastructure itself) |
| memory context | memory 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! is active and queryable via the retained dispatch event log.dispatch_pipeline_active as "dispatch active for migrated slices"
during migration, not yet "all mutations converge through dispatch".Current agent-session dispatch sequencing for pure handler results is:
Current scaffold semantics:
Current default interceptor ids:
:permission:log:statechart:handler:effects:trim-effects-on-replay:validate:applyBecause after fns run in reverse order, the effective after-order is:
:apply -> :validate -> :trim-effects-on-replay -> :effectsThe retained dispatch log now exposes more architectural debugging signal than just event type and timing. Current log entries include:
:db, :root-state-update, :session-update, etc.)Retention/volume tradeoff:
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/failedCurrent guarantees:
dispatch-iddispatch-id through
nested extension/service activitydispatch-id:effect-typeCurrent 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-messageThis canonical trace is the preferred observability surface for end-to-end runtime coordination. It is the fine-grained complement to the dispatch event-log:
dispatch-idAdapter-local debug atoms remain useful for low-level transport diagnosis, but normal architectural debugging should prefer the queryable dispatch trace.
The first explicit conforming vertical slice target is manual compaction.
Current intended slice flow:
manual-compact-in!:session/compact-start:session/manual-compaction-execute:session/compaction-finished:session/compact-doneCurrent intentional boundary:
Current proof surface for the slice:
This slice is the proving ground for broader convergence from partial dispatch ownership toward more reference-architecture-conforming vertical behavior.
The next target slice after manual compaction is prompt / turn lifecycle.
Current implemented outer shell:
prompt-in!:session/prompt-submit:session/prompt:session/prompt-prepare-request:runtime/prompt-execute-and-record:session/prompt-record-responseArchitectural convergence target:
prompt-in!:session/prompt-submit:session/prompt:session/prompt-prepare-request:runtime/prompt-execute-and-record:session/prompt-record-response:session/prompt-continue or :session/prompt-finishCurrent converged slice semantics:
:session/prompt-submit
:session/prompt-prepare-request
:runtime/prompt-execute-and-record
:session/prompt-record-response with the shaped execution result:session/prompt-record-response
:session/prompt-continue / :session/prompt-finish
Current intentional boundary:
:session/tool-run composes two dispatch-owned phases:
: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: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 recordingNear-term architectural direction:
app-runtimeapp-runtimeapp-runtimeapp-runtimeWhat success looks like:
session-id routing becomes the default for targetable RPC operations, with adapter focus used only as fallbackCan you improve this documentation? These fine people already did:
Hugo Duncan & Test AuthorEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |