pleach
Build

SessionRuntime

The main entry point — construct a runtime, create sessions, execute messages, and consume the stream.

A SessionRuntime owns one user's sessions, the per-turn graph, the stream pipeline, the AuditableCall ledger, and the plugin set — it's the one class you instantiate from @pleach/core. Everything else is configuration passed to its constructor or methods called on the instance. See Storage for adapter choices and Stream events for what executeMessage yields.

SessionRuntime is the implementation surface of the runtime-lifecycle cluster — it hosts the three lifecycle concepts (session lifecycle, turn lifecycle, event log) under one constructor.

import { SessionRuntime } from "@pleach/core";

Construction

const runtime = new SessionRuntime(config);

All fields are optional. With no config, you get an in-memory runtime with userId: "anonymous" — fine for a tutorial, never fine for production.

SessionRuntimeConfig fields

FieldTypeDefaultPurpose
storageStorageAdapterfresh MemoryAdapterWhere sessions persist. See Storage.
checkpointerCheckpointernoneWhere checkpoints persist. See Checkpointing.
eventLogWriterHarnessEventWriternoneWrite side of the durable event log.
eventReaderHarnessEventReadernoneRead side of the durable event log — backs runtime.events.iterate / .fold and resumeSession's Layer-3 hydration. Store-agnostic; the host casts its store into it at construction. See Backing the reader.
cacheStorageAdapternoneOptional hot-data cache adapter
userIdstring"anonymous"Session owner. Production deployments must pass this.
organizationIdstringMulti-tenant scoping
clientIdstringgeneratedSync version-vector key
providerAgentProviderLLM provider implementation
pluginsHarnessPlugin[][]Domain extensions. See Plugin contract.
metaToolNamesSet<string>empty setMeta-tool names the runtime treats as flow-control (e.g. set_step_complete, wait_for_jobs)
enabledSafetyPoliciesreadonly SafetyPolicyId[][]Operator opt-in for safety policies plugins registered. Canonical read surface is runtime.safety.{listAvailable, getActive, getRegistry}.
seedCoreDefaultsbooleantrueAuto-seed the 11 core.* prompt fragments
interruptInterruptConfigHuman-in-the-loop configuration
enableSubagentConcurrencybooleanfalseAllow parallel subagents
maxConcurrentSubagentsnumber4Concurrency cap when enabled
chatIdstringProvenance link to an upstream chat row
isGuest / guestIdboolean / stringGuest-mode session ownership
authTokenstringBearer token forwarded to API calls
jobDispatchEndpointstringURL the runtime POSTs queued jobs to

enabledSafetyPolicies participates in the fingerprint cache key — toggling a policy invalidates the cache automatically.

metaToolNames is load-bearing after R-1.A. Hosts that don't pass it see a one-shot [UXParity:metaToolNames-config-missing] probe plus a startup warning, and continuation guards silently disable. The continuation guards are the runtime nodes that decide whether a named flow-control tool (set_step_complete, wait_for_jobs) signals "this step is done" or "this is just another tool call"; when the guards disable, the tool loop keeps iterating on what should have been a terminal signal, which is the symptom most hosts notice before they read the probe.

Strategy injection

SessionRuntimeConfig also carries a second set of fields the substrate calls strategies — typed slots the host fills with domain code, each with one well-defined shape and one consumer. A strategy that's unset gracefully degrades; the runtime stays functional and the strategy-backed behavior goes inert.

Representative slots include summaryExtractor, toolCatalog, subagentExecutor, fabricationGuardStrategy, hallucinationDetectorFactory, and the orchestrator-side provider-fallback registry. The full surface, the wiring patterns, and the per-turn vs. per-runtime allocation rule live on Runtime strategies.

Facets

Each facet returns a typed slice of the runtime — runtime.sessions for session lifecycle, runtime.events for the event log, runtime.sync for bidirectional sync, runtime.dev for diagnostics. Reading a facet gets you the methods grouped under that name and the typed receipts those methods return.

Runtime-level facet inventory:

FacetCovers
runtime.sessionsSession lifecycle — create, resume, find, save, delete, updateProviderModel
runtime.syncBidirectional sync — execute, resolveConflict, subscribeToStream. Callable: runtime.sync(sessionId) preserves the legacy shape.
runtime.eventsEvent-log iteration + fold + the live event bus (on, once, off)
runtime.spansSpan start / flush / shutdown plus inFlightCount, isShutdown, snapshot introspection
runtime.tenantTenant identity — frozen at construction; identity-stable across reads
runtime.pluginsPluginManager access plus the full collect* cohort and registry lookups
runtime.promptsPrompt-contribution introspection (list, get, getAll, listByOrigin)
runtime.safetylistActivePolicies, listAvailablePolicies, getRegistry
runtime.toolsRegistered tool catalog introspection
runtime.observerRouterLate-binding stream-observer registration; receipts on unregister
runtime.degradationModel degradation reads; getDegradedModelRecord returns a receipt
runtime.checkpointsrollback, list
runtime.timeTravelTime-travel API accessor
runtime.asyncAsync-task manager, subagent spawn, result lookup
runtime.interruptsInterrupt manager; resolve returns an InterruptResolveReceipt
runtime.adapterOrchestrator-adapter wiring + capability registry
runtime.diagnosticsBoot-time readiness probes for the host-extension bag
runtime.devDiagnostic sub-API — store, stream manager, repetition-guard wiring
runtime.graph.recoveryPer-turn recovery hooks
runtime.graph.heuristicsTunable graph heuristics
runtime.graph.configGraph-build configuration
runtime._internalINTERNAL graph-consumption surface — substrate-only, do not call

Receipt-shaped returns

Mutating accessors across sessions, sync, observerRouter, degradation, and interrupts return typed receipts rather than booleans or void. The pattern is deliberate: every call site that mutates runtime state echoes a structured outcome callers can branch on, audit, or log without re-derivation.

Examples:

  • runtime.sessions.delete(sessionId)SessionDeleteReceipt (deleted, sessionId, removedFromCache).
  • runtime.sync.flushOutbox()OutboxFlushReceipt with a four-verdict outcome ("all-flushed" | "partial" | "all-stuck" | "none-pending") plus failedEntries, flushedAt, and the pre-widening counts.
  • runtime.sync.resolveConflicts(...) includes a conflictedPaths array describing every diverged field (path, localValue, remoteValue, chosenSide).
  • runtime.sync.resolveConflict(...)SyncConflictResolveReceipt (resolved, sessionId, conflictId, resolution, resolvedAt).
  • runtime.observerRouter.unregister(reg){ removed, wasRegistered }.
  • runtime.degradation.getDegradedModelRecord(modelId) → receipt with the active routing entry or null when none is held.
  • runtime.interrupts.resolve(id, decision)InterruptResolveReceipt.

Deprecation contract

Existing flat methods stay around with @deprecated JSDoc that points to the facet. For example, runtime.getTenantId() redirects readers to runtime.tenant.getTenantId(), and runtime.getSafetyRegistry() redirects to runtime.safety.getRegistry(). The minor release that adds the facet also marks the flat method @deprecated; the next major removes the flat method. Flat methods are never removed in a minor bump.

For the full facet inventory, the TurnOrchestrator.* facet set, and the audit gates that enforce coverage, see Facets.

Creating a session

const session = await runtime.createSession({
  provider: { type: "anthropic" },
  model:    { id: "claude-sonnet-4-20250514" },
  tools:    { enabled: ["search", "calculator"] },
});

createSession accepts a Partial<SessionConfig>. Defaults:

Config fieldDefault
provider{ type: "anthropic" }
model{ id: "claude-sonnet-4-20250514" }
tools{ enabled: [] }
projectId / campaignIdinherited from runtime if set

Returns a Session instance bound to a generated UUIDv7 id — session.id is what executeMessage takes. The session is written to storage before this resolves.

Executing a message

executeMessage is an async generator. Iterating it streams the turn; every yielded value is a StreamEvent.

for await (const event of runtime.executeMessage(session.id, "Hello")) {
  switch (event.type) {
    case "message.delta":
      process.stdout.write(event.delta);
      break;
    case "tool.completed":
      console.log("tool finished:", event.toolCall.name);
      break;
    case "error":
      console.error(event.code, event.error);
      break;
  }
}

Signature

runtime.executeMessage(
  sessionId: string,
  content: string,
  options?: {
    fileReferences?: unknown[];
    abortSignal?: AbortSignal;
  },
): AsyncGenerator<StreamEvent>;

Aborting a turn

Pass an AbortSignal. The runtime cancels in-flight provider calls, unwinds the tool loop cleanly, and flushes a final ledger row with outcome.status: "user-aborted". The user-aborted row carries the same (sessionId, turnId, stageId, seqWithinTurn) identity tuple as a successful row, which is what lets a per-turn cost rollup count an abort against the same turnId as the calls that ran before it — the aborted call still consumed input tokens through the provider's prefill, and the row exists so that spend isn't invisible.

const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 30_000);

for await (const event of runtime.executeMessage(
  session.id,
  prompt,
  { abortSignal: ctrl.signal },
)) {
  // ...
}

Deleting a session

await runtime.deleteSession(session.id);

Cascades into the checkpointer when one is wired. Audit-ledger rows are not deleted — the ledger is append-only by contract, which is why ProviderDecisionLedger has no update or delete primitive in any conforming language. The GDPRSoftDelete plug-point on the ledger interface (no-op default; production wiring lands in @pleach/compliance@0.1.0 alongside its scrubber cohort and attestation surface) is the path for subject-key-derived redaction: it leaves the recordId in place (so the hash chain holds) and clears the identifying fields under the subject key, satisfying a deletion request without breaking the append-only invariant. Wire your own implementation today via HarnessPlugin.

Flat method surface (legacy)

The tables below mirror the facets above as flat methods on the runtime; the methods are @deprecated and forward to their facet equivalents — prefer Facets for new code.

Session lifecycle methods

MethodSignatureUse
createSession(config?: Partial<SessionConfig>) => Promise<Session>Mint a new session, persist to storage
resumeSession(sessionId: string) => Promise<Session>Load an existing session from storage
saveSession(state: SessionState) => Promise<void>Flush an explicit state mutation to storage
deleteSession(sessionId: string) => Promise<SessionDeleteReceipt>Remove the session; cascades to checkpointer. Returns a receipt with deleted, sessionId, and removedFromCache.
getActiveSession() => Session | nullLast session loaded into the runtime
rollbackToCheckpoint(sessionId, checkpointId) => Promise<Session>Restore a session from a named checkpoint
resolveConflict(sessionId, conflictId, "local" | "remote") => Promise<SyncConflictResolveReceipt>Resolve a sync conflict surfaced via sync.conflict. Returns a per-path receipt.
destroy() => Promise<void>Tear down stream + interrupt managers, unsubscribe listeners

Stream subscription

Two ways to consume events. executeMessage is the per-turn async generator above. For cross-cutting subscribers, the runtime also exposes a mode-filtered subscription via its embedded StreamManager.

const unsubscribe = runtime.subscribeToStream("messages", (event) => {
  // event is StreamEvent — already filtered to the mode
});
ModeIncludes
"values"session.created, session.resumed, checkpoint.created
"updates"message.added, message.complete, tool lifecycle, job lifecycle, artifact.created
"messages"message.delta, message.complete, thinking.delta, thinking.complete
"custom"Any event whose type starts with custom.
"debug"All events — unfiltered
"all"All events — unfiltered

getStreamManager() returns the underlying manager when you need to set a throttle or an event-type allowlist directly: manager.setThrottleInterval(ms), manager.setEventFilter(types).

Accessor methods

The runtime exposes its internal coordinators behind named getters. Returns are undefined when the coordinator wasn't wired at construction.

MethodReturns
getInterruptManager()InterruptManager | undefined — present when interrupt.enabled: true
resolveInterrupt(id, decision)InterruptResolveReceipt — submit an ApprovalDecision and receive a receipt echoing the outcome
getTimeTravelApi()Checkpoint list / restore API (see Checkpointing)
getAsyncTaskManager()Background job manager (see Stream events)
getPluginManager()Registered plugin set
getStreamManager()StreamManager — throttle + filter the live stream
getStore()The storage adapter passed at construction
getSubagentManager()SubagentManager | undefined when concurrency is enabled
getCapabilityRegistry()Registered capability strategies
getSafetyRegistry()SafetyPolicyRegistry
listActiveSafetyPolicies()readonly SafetyPolicySummary[]
getDegradedModels()Models the degradation tracker is routing around
isModelDegraded(modelId)Boolean check for a single model
getPlanningContext()Current planning context (advanced)

The published .d.ts is the canonical signature reference; your IDE autocompletes from it.

Lifecycle events

SessionRuntime extends EventEmitter. The same shapes that come out of executeMessage also fire as events, so a long-lived runtime can attach cross-cutting subscribers without owning the async iterator:

runtime.on("checkpoint.created", (event) => {
  metrics.increment("checkpoints", { sessionId: event.checkpoint.sessionId });
});

The full event union is the StreamEvent type — see Stream events for the catalog.

Plugin contributions

runtime.plugins is the canonical read surface for everything a plugin contributed. Beyond PluginManager access, the facet exposes a collect* cohort that mirrors PluginManager.collect* one-for-one — roughly two dozen accessors covering retry policy, continuation policy, fabrication detectors, finalization passes, intent classifiers, tool-coupling hints, intent-tool maps, sandbox bridge, interrupt UI handlers, citation rules, manifest providers, fabrication guard, guest-denied tools, stream observers, hallucinated-tool detectors, stream-chunk handlers, middleware, and runtime-aware middleware. The full hook list lives on Plugin contract.

const retryPolicies = runtime.plugins.collectRetryPolicy();
const continuationPolicies = runtime.plugins.collectContinuationPolicy();
const detectors = runtime.plugins.collectFabricationDetectors(sessionId);

Two convenience accessors round out the facet:

  • runtime.plugins.listAvailablePromptContributions() — every prompt contribution every plugin registered, as a snapshot array.
  • runtime.plugins.getSafetyRegistry() — the active SafetyPolicyRegistry. Equivalent to runtime.safety.getRegistry().

Collection-shaped accessors return [] when no plugin contributed; capability-shaped accessors return null. Empty results never throw.

Event-log projections

runtime.events.iterate(...) returns a paginated async-iterable over harness_event_log rows ordered by (chat_id, sequence_number, created_at, id). The accessor reads the durable Supabase log server-side; PA-1 Phase A.2 promoted this from a stub to a real projection surface.

for await (const row of runtime.events.iterate({ chatId })) {
  // EventLogRow — id, session_id, event_type, payload, sequence_number, …
}

const result = await runtime.events.fold(myProjection, { chatId });
// result.state, result.rowsProcessed, result.projection

runtime.events.fold(projection, options?) composes a GraphProjection<T> over the iteration. options.fromSequenceNumber skips at the SQL layer — rows with sequence_number IS NULL (pre-PA-1 legacy) are always included for back-compat.

See Event-log projections for the projection contract and the canonical bundled projections.

The live event bus (runtime.events.on, once, off) is co-located on the same facet but is unrelated to the persisted log — ordering between the two is not guaranteed; treat the bus as a hot stream.

Span introspection

runtime.spans exposes the OTel-shaped exporter the runtime threads through every turn:

runtime.spans.start({ name: "host.custom", kind: "host.custom" });
await runtime.spans.flush();    // call from a waitUntil tail
runtime.spans.inFlightCount();  // 0 when the exporter doesn't track
runtime.spans.isShutdown();     // false when the exporter doesn't track
runtime.spans.snapshot();       // { inFlightCount, isShutdown } in one read

inFlightCount, isShutdown, and snapshot are introspection methods backed by optional getInFlightCount?() / isShutdown?() hooks on the exporter contract. When a host-supplied exporter doesn't implement them, the facet returns 0 / false — documented as "unknown — assume zero / false". The default NoopOtelExporter and the reference CapturingOtelExporter both track accurately.

cacheBackend default-policy promotion

memoryCacheBackend is the substrate default for cacheBackend. The runtime constructor default-constructs one when config.cacheBackend is undefined; the seam factories receive the backend automatically via createSeam.ts. Previously the field had to be configured explicitly; today it is always-on with a default cap of 1000 entries and 64 MB.

The change is observable in runtime.cache?.metricsSnapshot() on day zero. No consumer action is required.

To override, pass a custom cacheBackend field on SessionRuntimeConfig. Set it to null to disable caching entirely.

For the contract, the two read modes, and the 4-gap fingerprint thread, see Cache.

Event-log hash chain

The persistence layer can stamp a per-chat hash chain onto every harness_event_log row. The wire-in lives on EventLogWriter and is gated by the c9PhaseBEnabled flag; when on, the writer maintains a prevHashCache keyed by chat_id, links each new row to the previous row_hash, and surfaces verifyChainForChat(...) and generateProof(...) from @pleach/core/eventLog for downstream audit. Hosts that don't care about the chain leave the flag off and the writer behaves exactly as before.

Where to go next

On this page