pleach
Build

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.

Subpath@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

FacetWhat it carries
runtime.sessionsSession lifecycle — create, resume, find, save, delete (returns a SessionDeleteReceipt), updateProviderModel
runtime.checkpointsCheckpoint surface — rollback, list (async-iterable of summaries)
runtime.tenantid + subId properties. See Tenant facet
runtime.syncexecute (callable directly — runtime.sync(sessionId, opts) delegates), resolveConflict (returns a SyncConflictResolveReceiptresolved, 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.diagnosticscheckReadiness() — mailbox, history, perf-state report
runtime.plugins25+ 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.promptslist, get, getAll, listByOrigin, listByPlugin, listByAnnotation, count over registered prompt contributions
runtime.safetylistActivePolicies, listAvailablePolicies, getRegistry
runtime.toolslist, 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.observerRouterHost-side stream observer registration — register, unregister (returns ObserverUnregisterReceipt), enumeration helpers
runtime.degradationgetDegradedModels, isModelDegraded, getDegradedModelRecord (returns receipt), getPlanningContext
runtime.timeTravelapi — the TimeTravelAPI property (lazily constructed; undefined when the runtime has no checkpointer) for state snapshot get/list, fork, revert
runtime.devDevTools-only surface — store + stream-manager accessors, onRepetitionGuard, createTimestamp
runtime.asyncspawn, spawnStream, getResult, getResultWithContext, plus task-manager/subagent-manager accessors
runtime.interruptsresolve (returns InterruptResolveReceipt), manager (the InterruptManager property, undefined when none is attached)
runtime.adapterget / set the model adapter; getCapabilityContextForTool, getCapabilityRegistry
runtime.eventsEvent 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.spansIn-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.observerecord(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.probesemit(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._internalSubstrate-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 stagegraph.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 facetStageCarries
graph.anchoranchor-plannodes, getNode(id)
graph.toolLooptool-loopnodes, getNode(id)
graph.synthesizesynthesizenodes, getNode(id), getSeamIdentity()
graph.postTurnpost-turnnodes, 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.

FacetWhat it carries
client.configTurn config inspection — get, update, getBackend
client.historyMessage history reader/writer — get, load, clear, add
client.contextPer-turn context — setArtifacts, setJobHistorySummary, clearArtifactsByJobId, setLastUserMessageFileRefs
client.toolsTool execution surface — getResolved, resolveForCurrentTurn, getPreResolvedIntent, getCurrentIntent, getPendingJobs
client.modelModel selection + fallback — selectForQuery, getLastSelection, registerPreHook, registerPostHook, setOnProviderAttempt
client.promptsComposed-prompt surface — buildSystemPrompt, getGraphConfig, drainPendingSystemNotices
client._internal.graphINTERNAL 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 (observerRouter for local-flag/halt bookkeeping, lifecycle for 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 / *ForGraph graph-internal methods the per-turn graph consumes. The public surface for graph inspection is runtime.graph.* inside the graph layer, not _internal.graph on 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:

  1. Minor bump. Adds the facet. Marks the flat method @deprecated with a pointer at the facet.
  2. 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 OrchestratorClientTurnOrchestrator 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

On this page