pleach
Plugins

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 clusterSessionRuntime 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.

Subpath@pleach/core/runtimeSourcesrc/runtime/turnOrchestrator.ts

The 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.

FacetWhat it carries
client.configTurn config — channel, model overrides, abort signal
client.historyThe message history this turn sees, post-trim
client.contextPer-turn context object — variables, scratchpad
client.toolsTools registered and available this turn
client.modelResolved model + family info — provider, family, model id
client.promptsPrompt building, graph-config snapshot, system-notice drain
client._internal.graphINTERNAL 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 streamSingleTurn body 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

On this page