# streamSingleTurn (/docs/stream-single-turn)



`streamSingleTurn` is the per-turn body. It owns the path from
"the runtime entered a turn" to "the synthesis stream closed" —
provider call, tool dispatch, continuation gate, fallback
chain, hallucination check, repetition guard, synthesis. The body
is generic substrate; everything host-specific exits through
typed strategy slots and plugin hooks.

The body lives in the package's internal
`strategies/streamSingleTurn/` tree — an implementation detail, not a
public import surface — and is the
default body the runtime threads through when a host constructs
`SessionRuntime` without a custom strategy. This is its current
home as of the **PO-A1** lift (commit `0a1fde560`); previously it
lived at `src/lib/orchestrator/streamSingleTurn.ts` on the host
side. Hosts that need a non-default body register one through
the module-loader seam — the runtime resolves the body via
`dynamicImportApp("orchestrator.streamSingleTurn")` (wired with
`setHarnessModuleLoader`), so a registered host body replaces the
default at that key.

<SourceMeta subpath="@pleach/core/strategies" source="{ label: &#x22;src/strategies/streamSingleTurn/turnBody.ts&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/strategies/streamSingleTurn/turnBody.ts&#x22; }" />

## Where the body sits [#where-the-body-sits]

`streamSingleTurn` is one of two execution paths the runtime
exposes. The body runs the imperative arc — call provider,
dispatch tools, continuation-or-synthesize, write the ledger — and
hands the [`OrchestratorClient`](/docs/orchestrator-client) handle
to every strategy slot and plugin hook it consumes. The
declarative [graph path](/docs/graph) walks the four-stage lattice
when the turn shape needs the enrichment slots. Both paths write
the same `AuditableCall` rows and honor the same family-lock; the
choice is per-host, not per-turn.

A turn enters the body through `runtime.executeMessage(sessionId,
content)`. The runtime allocates a per-turn `OrchestratorClient`,
resolves the registered strategy, and dispatches. The body
streams `StreamEvent` frames as it runs; the runtime forwards
them to the SSE consumer. See
[Turn lifecycle](/docs/turn-lifecycle) for the wall-clock arc and
[Stream events](/docs/stream-events) for the per-frame shape.

## What the body consumes [#what-the-body-consumes]

The body is a generic per-turn driver. Every host-specific
behavior — model fallback, hallucination detection, repetition
budgeting, continuation gate exemptions, runaway-directive
carry-forward — enters through optional `harnessRuntime?.getX()`
accessors on `OrchestratorConfig.harnessRuntime`. A host that
wires zero accessors gets a working body: every read chains with
`?.` and falls back to a no-op default and the substrate emits
its own baseline behavior.

The body reads four such accessors today — the **C1–C4**
dep-inversion cohort landed alongside the PO-A1 body lift. Each
read sits inline at the point of use:

| Accessor (read site)                                                                   | What the body asks it                                                                                                                                                                                         |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `harnessRuntime?.getDomainContextStrategy?.()` (C1; turnBody.ts \~L1506, L1787, L2119) | Per-turn domain-context strategy supplying the science-context / short-content-garble predicates the body's degradation guards consume. Default: no-op strategy.                                              |
| `harnessRuntime?.getHallucinationDetectorFactory?.()` (C2; turnBody.ts \~L835, L1795)  | Build a per-turn detector. Body calls `feed(chunk)` on every content delta and reads `hallucinationDetected` / `narratedWithoutExecution` after the turn settles. Default: `createNoOpHallucinationDetector`. |
| `harnessRuntime?.getRepetitionGuard?.()` (C3; turnBody.ts \~L419)                      | Per-session repetition tracker. Body calls `setPlanActive`, `getPriorTurnRunawayDirective`, `clearPriorTurnRunawayDirective` if defined. Default: no-op; the runaway-directive arm stays silent.              |
| `harnessRuntime?.getContinuationMetaToolNames?.()` (C4; turnBody.ts \~L410)            | Names exempted from tool-call counting in the depth-zero continuation gate. Default: empty set; every tool counts.                                                                                            |

`EMPTY_SINGLE_TURN_FLAGS` from `@pleach/core/types/singleTurn` is
the four-boolean reset baseline (`truncated`, `modelFallback`,
`contextOverflow`, `hallucinationDetected`) that the body spreads
at turn boundaries.

The accessor table above is the body-facing view of the surface
documented in full at
[Runtime strategies § Newer injection hooks](/docs/runtime-strategies#newer-injection-hooks).
The full strategy inventory — `summaryExtractor`, `toolCatalog`,
`subagentExecutor`, and the other singletons — lives in the same
page.

### Generic-by-injection [#generic-by-injection]

The body itself contains no host-specific logic. Domain behavior
(host vocabulary, vendor backends, sandbox tool prefixes, domain
phrasing — for example a host's domain-specific input handling,
third-party integrations, or safety directives) arrives via
`harnessRuntime.collectX()` and `harnessRuntime.getX()` calls
into the registered plugin surface. The body asks; the plugin
answers; nothing is hardcoded. This is the property that lets
the same body drive arbitrary domains.

## What the body never sees [#what-the-body-never-sees]

The body is generic substrate; the
[domain-string purity gate](/docs/architecture#10-boundary-rules)
asserts it stays generic. Five categories of literal are
forbidden from `packages/core/src/**` and therefore from the body
itself:

* Host vocabulary (the consumer's domain terms)
* Vendor backend names (specific cloud / execution platforms)
* Sandbox tool prefixes (the host's tool namespace convention)
* Identity discriminators (host-specific account or org strings)
* Domain phrasing (host-specific prompt fragments)

A leak fails `audit:domain-string-purity` in CI. The body stays
domain-agnostic by structure, not by review discipline. Plugins
remain the only legitimate channel for any of the five
categories; the body asks the plugin surface for the value it
needs and ships none of it inline.

See
[Language-agnostic contract § How the contract stays honest](/docs/language-agnostic-contract#how-the-contract-stays-honest)
for why the gate is load-bearing.

## The helpers barrel (internal) [#the-helpers-barrel-internal]

`streamSingleTurn` groups its helpers behind a barrel in the
package's internal `strategies/streamSingleTurn/helpers/` tree. This
barrel is an **implementation detail — not part of the public API**.
It sits under the package's `internal/` namespace precisely to signal
that its paths can move or change shape between releases with no
deprecation cycle. Do not import it directly.

The barrel groups both the typed contracts the body consumes —
`TurnAccumulator`, `OrchestratorMessage`, `ProviderFallbackConfig`,
`ToolDefinition` — and a small set of runtime utilities the body
uses inline (`safeStringifyArgs`, `parseToolArgs`,
`dispatchForceSynthesisEchoDetector`,
`buildImperativeProviderAttemptCallback`). They exist for the body's
own use, not for consumer code.

For the public, supported types a strategy needs, reach for the
named type subpaths instead. The generic `ToolDefinition` structural
contract, for example, ships from `@pleach/core/types/generic`:

```typescript
import type { ToolDefinition } from "@pleach/core/types/generic";
```

A host does not import the body's internal helpers to wire a custom
strategy. It registers a replacement body through the module-loader
seam (see [Wiring a custom body](#wiring-a-custom-body)) and consumes
the typed strategy slots the runtime threads in.

### Consumer-surface non-overlap [#consumer-surface-non-overlap]

The body lives at `turnBody.ts` and the chunk-time observer
cohort lives at `streamSingleTurn/consumers/*`. These are locked
as disjoint import territories: a single file may
import from one or the other, never both. `audit:streamSingleTurn-consumer-surface`
runs at PR time and fails any host file that overlaps the two.
If a host genuinely needs both, instantiate two separate
consumer surfaces — one for the body, one for the chunk-time
observers — and keep their import graphs disjoint.

## Wiring a custom body [#wiring-a-custom-body]

The default `streamSingleTurn` body handles the linear-turn case
without configuration. A host that needs a different shape —
typically because the turn needs to interleave a custom planner
or a non-standard tool-dispatch order — registers its own body at
the `orchestrator.streamSingleTurn` module-loader key. The wiring
goes through `setHarnessModuleLoader` (see
[Host adapter](/docs/host-adapter)), not a `SessionRuntimeConfig`
field; the runtime resolves the registered body via
`dynamicImportApp("orchestrator.streamSingleTurn")` and threads the
same per-turn `OrchestratorClient` into it.

A custom body is a sharp tool. The default body is what the
substrate's parity tests run against — replacing it is opting out
of the byte-replay property until the custom body passes the
same fixtures. See [Determinism](/docs/determinism) for the
five contracts the default body holds.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="Runtime strategies" href="/docs/runtime-strategies" description="The full strategy slot inventory the body consumes." />

  <Card title="OrchestratorClient" href="/docs/orchestrator-client" description="The per-turn handle the body threads into every strategy slot." />

  <Card title="Turn lifecycle" href="/docs/turn-lifecycle" description="The wall-clock arc of one turn — what the body does between executeMessage and the final SSE frame." />

  <Card title="Plugin contract" href="/docs/plugin-contract" description="The contribution surface that supplies the body's domain inputs." />
</Cards>
