Facet accessors
runtime.* / TurnOrchestrator.* / graph.* — a discoverable, typed surface designed to be read by humans and LLM agents. Stable shape, audited for coverage.
The runtime exposes its capabilities through facets — small named
accessors that group related methods by domain. runtime.tenant
gathers every tenant-scoping primitive; runtime.events gathers
every event-log primitive; runtime.spans gathers the in-process
OTel span surface. The grouping is the contract. The methods inside
each facet are the implementation.
The design choice is to make the runtime discoverable by
construction. A human reading runtime.<TAB> in an IDE sees a
short list of named domains, not a flat dump of methods. An LLM
agent introspecting the runtime to plan a next action reads the
same short list. Both audiences route their queries through the
facet name (spans, events, tenant) and arrive at the right
method.
Facets are a thematic island. Not one of the six cluster triplets — a facet is a surface pattern (typed accessors grouped by domain), not a three-concept cluster. See What lives outside the cluster pattern.
@pleach/coreSourcesrc/facets/The facet inventory
Facets live on two host objects: the SessionRuntime your host
code constructs, and the TurnOrchestrator handle each turn
receives. The per-turn graph also exposes a runtime.graph.*
namespace that mirrors the facet pattern for graph-layer-only
helpers.
On SessionRuntime
| Facet | What it carries |
|---|---|
runtime.sessions | Session lifecycle — create, resume, find, save, delete (returns a SessionDeleteReceipt), updateProviderModel |
runtime.checkpoints | Checkpoint surface — rollback, list (async-iterable of summaries) |
runtime.tenant | id + subId properties. See Tenant facet |
runtime.sync | execute (callable directly — runtime.sync(sessionId, opts) delegates), resolveConflict (returns a SyncConflictResolveReceipt — resolved, sessionId, conflictId, resolution, resolvedAt; FSL build: last-writer-wins — logs the resolution + returns resolved: true with NO CRDT merge or state mutation; enterprise builds perform the merge), subscribeToStream |
runtime.diagnostics | checkReadiness() — mailbox, history, perf-state report |
runtime.plugins | 25+ collectX accessors (fabrication detectors, finalization passes, synthesis directive blocks, detection rules, intent classifiers, tool-coupling hints, intent-tool map, sandbox bridge, interrupt UI handlers, citation rule set, chat manifest provider, fabrication guard, guest denied tools, entity name counter, structure prefetcher, artifact cache reader, get-data handler factory, stream observers, ...) plus listAvailablePromptContributions, listAvailableSafetyPolicies, getActiveSafetyPolicies, getSafetyRegistry, registerExtension, getPluginManager. The canonical surface for reading what plugins have contributed. |
runtime.prompts | list, get, getAll, listByOrigin, listByPlugin, listByAnnotation, count over registered prompt contributions |
runtime.safety | listActivePolicies, listAvailablePolicies, getRegistry |
runtime.tools | list, get, listByApprovalRequired, listByAnnotation, count over registered tools. (listByCallClass is deprecated — it returns [] unconditionally because ToolDefinitionLite carries no callClass field; use list() + your own call-class mapping.) |
runtime.observerRouter | Host-side stream observer registration — register, unregister (returns ObserverUnregisterReceipt), enumeration helpers |
runtime.degradation | getDegradedModels, isModelDegraded, getDegradedModelRecord (returns receipt), getPlanningContext |
runtime.timeTravel | api — the TimeTravelAPI property (lazily constructed; undefined when the runtime has no checkpointer) for state snapshot get/list, fork, revert |
runtime.dev | DevTools-only surface — store + stream-manager accessors, onRepetitionGuard, createTimestamp |
runtime.async | spawn, spawnStream, getResult, getResultWithContext, plus task-manager/subagent-manager accessors |
runtime.interrupts | resolve (returns InterruptResolveReceipt), manager (the InterruptManager property, undefined when none is attached) |
runtime.adapter | get / set the model adapter; getCapabilityContextForTool, getCapabilityRegistry |
runtime.events | Event log read surface — iterate({chatId, fromSequenceNumber?}) paginated async-iterable + fold(projection) GraphProjection reducer; extends the live event-bus (on / once / off). See Event log projections |
runtime.spans | In-process OTel span surface — start, flush, shutdown, inFlightCount, isShutdown, snapshot. See OTel observability. Wire config.otelExporter or spans are dropped — the default is a NoopOtelExporter that accepts every span and discards it on flush (inFlightCount/isShutdown/snapshot still track accurately). |
runtime.observe | record(row) — emit one ObserveRow to the registered observe destination; silent no-op when none is installed (host call sites stay guard-free). Shares the dispatch slot @pleach/observe's init() writes to, so one init() activates both the top-level recordCall path and runtime.observe.record. See Observe |
runtime.probes | emit(label, payload) plus a TS-typed typed<K>(key, payload) over the augmentable ProbesRegistry. Emits a probe into the per-runtime registry aggregated by collectProbes (the contributeProbes plugin hook); no-ops when no plugin contributes a probe at that label |
runtime._internal | Substrate-only escape hatches. Two sub-facets: _internal.observerRouter (local-flag/observer-halt bookkeeping per messageId) and _internal.lifecycle (seam prompt resolution, job-dispatch/complete notifications). Off-limits to consumer code. |
Several facets return receipts rather than bare booleans —
sessions.delete, sync.resolveConflict, observerRouter.unregister,
degradation.getDegradedModelRecord, interrupts.resolve. The
receipt shape carries enough context (resolution timestamps,
the resolved id, the resolution decision) to be logged or asserted
against without a follow-up read.
runtime.graph.* is not a facet on SessionRuntime. It is the
graph-layer-only namespace assembled inside the per-turn graph
itself; the K.1 ladder carved graph.recovery, graph.heuristics,
graph.config, and graph._internal.builders into the graph
facets tree. The per-turn graph constructs these; the runtime does
not re-expose them.
Alongside those, the graph namespace exposes one read-only
introspection facet per stage — graph.anchor, graph.toolLoop,
graph.synthesize, and graph.postTurn, mirroring the four-stage
lattice. Each carries the compiled-graph nodes whose stageId
matches that stage (a frozen nodes array in deterministic
registration order) plus getNode(id) for a stage-scoped lookup.
graph.synthesize additionally exposes getSeamIdentity() — the
reader for the singleton synthesize seam's nodeId. The graph builder
holds no reference to the per-runtime seam holder, so this reader
always returns undefined today; a future layer may inject a live
seam-identity reader from SessionRuntime.
| Graph stage facet | Stage | Carries |
|---|---|---|
graph.anchor | anchor-plan | nodes, getNode(id) |
graph.toolLoop | tool-loop | nodes, getNode(id) |
graph.synthesize | synthesize | nodes, getNode(id), getSeamIdentity() |
graph.postTurn | post-turn | nodes, getNode(id) |
On TurnOrchestrator
The per-turn orchestrator handle each turn receives. TurnOrchestrator
was renamed from OrchestratorClient under D-PO-3; the old name
survives as a @deprecated path re-export until @pleach/core@2.0.0.
Facets here expose what's in scope for the current turn — config,
history, tools, model resolution.
| Facet | What it carries |
|---|---|
client.config | Turn config inspection — get, update, getBackend |
client.history | Message history reader/writer — get, load, clear, add |
client.context | Per-turn context — setArtifacts, setJobHistorySummary, clearArtifactsByJobId, setLastUserMessageFileRefs |
client.tools | Tool execution surface — getResolved, resolveForCurrentTurn, getPreResolvedIntent, getCurrentIntent, getPendingJobs |
client.model | Model selection + fallback — selectForQuery, getLastSelection, registerPreHook, registerPostHook, setOnProviderAttempt |
client.prompts | Composed-prompt surface — buildSystemPrompt, getGraphConfig, drainPendingSystemNotices |
client._internal.graph | INTERNAL composite for the 7 *Public / *ForGraph graph-internal cohort — executeTool, applyPreModelTransforms, runPostModelHooks, getAuthToken, getThinkingConfig, getFallbackConfig, recordToolOutcomes, getConversationHistory. Substrate-only; not on the user-facing surface. |
The flat methods that existed before the facet carve — getConfig,
getHistory, selectModelForQuery, buildSystemPrompt, and the
rest — remain on the class with @deprecated JSDoc pointing at the
facet equivalent. They forward to the same implementation; the
deprecation cycle is the contract.
Why facets, not flat methods
A flat namespace with fifty methods is searchable only if you
already know what you're looking for; a facet surface is searchable
by typing the concern. runtime.tenant.<TAB> lists every
tenant-scoping primitive; runtime.events.<TAB> lists every
event-log primitive; runtime.spans.<TAB> lists every in-process
span accessor. The grouping IS the documentation — a new
contributor reads the facet names first, picks the one that names
their concern, and the IDE shows them every primitive inside. The
same property holds for an LLM-driven coding agent reading the
runtime's type surface to plan its next action: a handful of facet
names with one-line descriptions costs far less token budget than
fifty method signatures with overlapping naming.
The flat methods (runtime.createSession(), client.getConfig())
remain available as forwarders for back-compat. The facet path is
canonical; the flat surface is a migration aid.
Surface stability
A facet groups methods that share a domain. Refactors inside that domain — renaming a private helper, swapping the internal store, adding a new method to the group — leave the facet shape unchanged. Consumer code that destructures the facet keeps working:
const { id, subId } = runtime.tenant;
const { iterate, fold } = runtime.events;The facet shape is the contract; the methods inside are the implementation. Code that pins to a facet sees fewer breakage events at minor bumps than code that pins to individual methods, because the substrate can churn underneath the facet without changing the facet's exported shape.
What's internal — don't use
Two namespaces are off-limits to consumer code:
runtime._internal.*— substrate-only escape hatches with two sub-facets (observerRouterfor local-flag/halt bookkeeping,lifecyclefor seam prompt resolution + job notifications). Subject to change without a deprecation cycle. The leading underscore is the signal; treat the field as if it weren't exported.TurnOrchestrator._internal.graph— same rules. The composite holds the 7*Public/*ForGraphgraph-internal methods the per-turn graph consumes. The public surface for graph inspection isruntime.graph.*inside the graph layer, not_internal.graphon the orchestrator. Consumer code reading it will break the next time the snapshot shape changes.
Code that touches _internal is breakage waiting to happen at the
next patch release. CI gates (see below) flag consumer imports of
_internal symbols so the breakage surfaces in CI, not at runtime.
CI gates enforcing facet coverage
Three audit gates ride on the facet surface. Each gate runs in upstream CI; all three are WARN-DRIFT today (non-blocking) and will promote to strict per D-AGENT-8 once in-tree migration of the last flat-method call sites finishes.
audit:facet-coverage
Asserts every public capability on SessionRuntime has a facet
entry. Adding a new public method without placing it under a
facet — or under the explicit "stays flat" allowlist — surfaces in
the report. The intent is to keep the surface organized by domain as
the runtime grows; new capabilities land under the facet that names
their concern.
audit:orchestrator-facet-coverage
The same gate, scoped to TurnOrchestrator. Adding a new field on
the client handle without a facet placement (or an _internal
prefix) surfaces in the report.
audit:graph-facet-coverage
The same gate, scoped to the runtime.graph.* sub-tree. The graph
namespace is fast-growing as the per-turn graph absorbs more
substrate logic; the gate keeps the sub-tree organized by domain
(recovery, heuristics, config, _internal.builders, plus the
per-stage introspection facets anchor / toolLoop / synthesize
/ postTurn) rather than letting every new helper land at the top
level.
Deprecation contract for flat methods
Flat methods (e.g. runtime.createSession(), client.getConfig())
ship with @deprecated JSDoc that points at the facet equivalent
(runtime.sessions.create(), client.config.get()). IDEs render
the strike-through; TypeScript surfaces the deprecation in the
language service. The lifecycle:
- Minor bump. Adds the facet. Marks the flat method
@deprecatedwith a pointer at the facet. - Next major. Removes the flat method. The facet is the only path.
Flat methods don't drop in minor versions. The deprecation cycle
is the contract — consumer code that ignores the @deprecated
warning still works through the minor series, and the next major
is the announced breakage window. The OrchestratorClient →
TurnOrchestrator rename follows the same shape: the old name is a
path re-export until @pleach/core@2.0.0.
Migrating to the facet path
The rename is mechanical — the flat method's name appears verbatim inside the facet, prefixed with the facet accessor.
// Flat (back-compat forwarder)
const id = await runtime.createSession({ provider });
const messages = client.getHistory();
const model = client.selectModelForQuery(userMessage);
// Canonical (facet path)
const id = await runtime.sessions.create({ provider });
const messages = client.history.get();
const model = client.model.selectForQuery(userMessage);
// Facet-first from the start
for await (const row of runtime.events.iterate({ chatId })) {
// ...
}The last block uses a facet that was facet-first from the start. Mixed codebases are expected during the migration window; the deprecated JSDoc on the flat methods is what flags the lines that still need rewriting.
A codemod that walks the AST and rewrites every flat-method call into its facet equivalent is straightforward — the rename map is 1-to-1. Hosts running large migrations should write the codemod once and run it across the consumer tree rather than rewriting call sites by hand.
Where to go next
SessionRuntime
The class your host constructs — the host object every facet hangs off.
Tenant facet
`runtime.tenant.id` and the multi-tenant scoping surface.
OTel observability
`runtime.spans.snapshot()` and the in-process span surface facets route through.
Event log projections
`runtime.events.iterate` / `fold` — the read surface on the per-session event log.