TurnOrchestrator (formerly OrchestratorClient)
Per-turn handle threaded through the stream body — six typed facets (config, history, context, tools, model, prompts) plus the graph snapshot.
TurnOrchestrator is the per-turn handle. When a turn begins,
the runtime constructs one, threads it through the stream body
and the substrate's tool / model / prompt resolution paths, and
disposes of it at turn end. The instance holds what's in
scope for this one turn — the channel config, the message
history, the per-turn context, the tools available, the resolved
model, the composed prompt.
It's the turn-scoped sibling of SessionRuntime
inside the runtime-lifecycle cluster —
SessionRuntime hosts the session arc; TurnOrchestrator hosts
each turn arc inside it.
Both TurnOrchestrator and the deprecated OrchestratorClient
alias export from the @pleach/core/runtime barrel — the canonical
import path for hosts that need a typed handle on the per-turn
surface — for example, when a custom strategy needs to read
client.history or query client.model.
import type { TurnOrchestrator } from "@pleach/core/runtime";Renamed from OrchestratorClient
The class was renamed under D-PO-3. OrchestratorClient survives
as a deprecated re-export from the same @pleach/core/runtime
barrel so existing consumers keep compiling:
// Still resolves; deprecated; removed at @pleach/core@2.0.0.
import { OrchestratorClient } from "@pleach/core/runtime";Throughout the rest of this page we use client as the variable
name to match the field naming on SessionRuntime and the
plugin-hook context objects.
@pleach/core/runtimeSourcesrc/runtime/turnOrchestrator.tsThe six facets
TurnOrchestrator exposes a facet surface that mirrors the
SessionRuntime facet pattern — domain-grouped accessors instead
of a flat method dump. Six user-facing facets plus one
_internal namespace that consumer code must not touch.
| Facet | What it carries |
|---|---|
client.config | Turn config — channel, model overrides, abort signal |
client.history | The message history this turn sees, post-trim |
client.context | Per-turn context object — variables, scratchpad |
client.tools | Tools registered and available this turn |
client.model | Resolved model + family info — provider, family, model id |
client.prompts | Prompt building, graph-config snapshot, system-notice drain |
client._internal.graph | INTERNAL graph-internal composite — substrate-only |
client.prompts covers the surface that used to live as flat
methods on the class. Use client.prompts.buildSystemPrompt(...)
instead of the deprecated client.buildSystemPrompt(...). Same
shape applies to client.prompts.getGraphConfig() and
client.prompts.drainPendingSystemNotices(). The flat methods
remain callable with @deprecated JSDoc and are removed at
@pleach/core@2.0.0.
client._internal.graph absorbs the seven *Public / *ForGraph
suffixed methods the graph nodes call into the client with
(executeTool, applyPreModelTransforms, runPostModelHooks,
getAuthToken, getThinkingConfig, getFallbackConfig,
recordToolOutcomes). The leading underscore is the contract —
the field is intentionally not part of the user-facing surface
and is callable only from graph nodes inside the substrate.
For the full facet inventory across SessionRuntime and
TurnOrchestrator see Facets. The
audit:orchestrator-facet-coverage CI gate asserts every new
public field on the client lands under a facet (or under
_internal). The migration is complete — today's baseline shows
zero remaining call sites on the deprecated flat methods, and the
gate runs strict (FAIL on any novel direct call). See
Facets § CI gates.
_internal.graph — do not use
TurnOrchestrator._internal.graph is a substrate-only composite
the graph nodes call into for bookkeeping (executeTool,
applyPreModelTransforms, recordToolOutcomes, and the
configuration accessors named above). The leading underscore is
the contract: the field can change shape without a deprecation
cycle. Consumer code that reads _internal.graph will break the
next time the snapshot shape changes.
The supported surface for graph inspection is runtime.graph.* on
the SessionRuntime, not _internal.graph
on the client. See Facets § What's internal — don't
use for the broader rule.
Where the client comes from
Hosts don't construct TurnOrchestrator directly. The runtime
allocates one per turn during executeMessage and disposes it at
turn end. A host receives the client in three places:
- Inside a custom strategy. Strategy slots that the per-turn body consumes (see Runtime strategies) take the client as part of their typed input bundle when the substrate needs to thread per-turn state through to the host.
- Inside a plugin hook. Plugin hooks that participate in the
turn —
contributePrompts,contributeRuntimeAwarePrompts,contributeStreamObservers— receive a context object that carries the relevant client facets. - Inside a custom turn body. Hosts that swap the
streamSingleTurnbody for their own strategy receive the client as an explicit argument. See streamSingleTurn for the body contract.
The client itself is not part of the language-agnostic wire contract — it's a TypeScript-side convenience. A Go implementation threads equivalent per-turn state through its own types. See Language-agnostic contract § What's NOT in the contract for the broader split.
Coexistence with the graph path
The substrate runs two execution paths side by side: the
imperative TurnOrchestrator path (the per-turn body calls
seams directly through the client) and the declarative graph path
(the CompiledGraph walks the four-stage lattice).
Both write to the same AuditableCall ledger; both honor the same
family-lock and singleton synthesize seam. The choice is a host
decision, not a runtime fork.
A host picks the imperative path when its turn shape is linear — one tool loop, one synthesis, no plan stage. The declarative path is the choice when the turn needs the lattice's enrichment slots (intent detection, plan generation, quality scoring) or when multiple plugins contribute graph nodes.
Minimal consumer usage
Most hosts never reach for TurnOrchestrator directly. The
runtime threads it through the substrate; plugin hooks and
strategies receive what they need via typed input bundles. A host
that does need the typed handle — typically for a custom
strategy implementing a streamSingleTurn swap — imports the
class for the type, not the constructor:
import type { TurnOrchestrator } from "@pleach/core/runtime";
function myCustomTurnBody(client: TurnOrchestrator) {
const config = client.config.get(); // OrchestratorConfig snapshot
const selection = client.model.getLastSelection(); // ModelSelection | null
const tools = client.tools.getResolved(); // ToolDefinition[]
// ... drive the seams using the typed facets
}Where to go next
Facets
The full facet inventory and the three audit gates that enforce coverage.
streamSingleTurn
The canonical per-turn body the substrate threads the client through.
SessionRuntime
The runtime that owns TurnOrchestrator — construction, sessions, executeMessage.
Graph
The declarative path that runs side-by-side with the imperative client.
Typed auditable-call records
Five audit-ledger records promoted from untyped JSON to typed, stage-correlated slots — Interrupt, TokenCost, ToolSelection, PlanGeneration, SynthesisQuality.
Eval and replay
Regression testing and deterministic replay against the @pleach/core substrate — fingerprint, audit ledger, checkpoints, runtimeMode. The DIY pattern.