DSHarness Systems Dissection Pinned baseline 47f943859b · 36 verified / 0 in progress / 36 chapters
中文
Core Runtime·Chapter 06

The Turn / Step / Loop State Machine

From input claims through model calls and tools to turn stop points

VerifiedUpstream 47f943859bScope: Trace durable turns, steps, waterfalls, empty steps, failures, and next-step input branch by branch.

Conclusion: the loop is small, but it guards event order and reconstructability

The default loop owns one semantic spine: claim input → open Turn/Step → reconstruct a request from the log → call the model → persist stream and completion → execute tools → continue or stop. Compaction, permission, retry policy, planning, and multi-Agent behavior participate through plugin seams instead of rewriting this spine.

“Small” does not mean simple. Its value is concentrated in exact commit order: every model request actually dispatched can be independently reconstructed from the Session log immediately before dispatch.

1. Do not conflate four levels

LevelBegins whenWhat it ownsEnds when
Driver activityWaking input starts from idleOne AbortController, initiator scope, and the drain of consecutive queued turnsNo continuable work remains, or failure/cancellation converges
TurnThe driver calls turn()Durable accounting for a unit of work, from turn/start to one turn/endcompleted / max-tokens / blocked / aborted / error
StepPre-step accepts a non-empty first batch, or tool continuation requires another requestOne model call and its tool batch, from step/start to step/endModel completion, tool continuation, tool conclusion, or exception
Request attemptbuildRequest() runs inside a StepFrozen route/system/tools/messages; provider failures may retry inside the same StepSuccessful finish or an unrecovered failure
Architecture fact

The official sequences separate turns, steps, prompt assembly, LLM streaming, tool settlement, and stop points. Turn/step boundaries are durable SessionEvents rather than a mirrored agent/* stream. ARCH-TURNTURN-SEQUENCE

2. The outer driver only loops, contains, and converges

Production control flow

wakeDriver() synchronously changes the phase to running and starts kick() inside ctx.agents.withInitiator(agent, ...). kick() only performs while (await turn()); reported failures and cancellation are contained there, then phase returns to idle and replays a wake latched during cancellation convergence. LOOP-DRIVER-PRESTEP

One running interval may execute several queued turns. If the Inbox still has pending work after a turn ends, the loop installs a fresh AbortController, resets the step counter, and continues. It exits the driver only when no work remains.

Design assessment

An exception does not destroy the Agent object. Failure belongs to the current turn, the driver converges to idle, and a later wake may start a new turn. Loop survival and work success are deliberately separate facts.

3. A Turn opens before the system knows whether it will have a Step

1

Append turn/start

The turn number is the previous turn plus one and becomes durable before any input is claimed.

2

Propose the first pre-step

The target is next-turn: claim every next-step item plus one next-turn prompt, assemble prompt/context, and enter the waterfall.

3

Possibly open a Step

Only an accepted branch requiring a model call appends step/start, commits accepted user messages, and calls step().

4

Close every opened Step

step/end is appended in finally, so an opened Step has a terminal boundary even if the model, a tool, or a plugin fails.

5

Offer turn-stopping

When a Step has a terminal result and no next-step input is pending, serial listeners get one last chance to steer another Step.

6

Append turn/end

Except for failures earlier than a successful turn/start append, an opened Turn records exactly one ending in finally.

Production state machine

If the first pre-step is rejected, the Turn ends blocked with no step/start, user/message, or model request. If the first decision enters with no messages, the Turn ends completed without opening a Step. LOOP-TURN-STATE

Branch tests

Tests prove that a blocked prompt leaves only turn boundaries, an empty post-completion pre-step opens no phantom Step, and inject/steer arriving after the claim cannot enter the current request but remain for the next Step or wake. LOOP-INTERCEPTION-TEST

4. Pre-step order determines context ownership

Exact order

preStep() atomically claims the Inbox, assembles the Agent's system prompt and tool schemas, renders dynamic context sections, asks RuntimeContextProjection whether a new snapshot message is needed, and finally invokes the agent/pre-step waterfall. The default decision combines claimed messages with a changed runtime-context message. LOOP-DRIVER-PRESTEPLOOP-RUNTIME-CONTEXT

This produces three important semantics:

  • Claim is the exclusive boundary. Messages arriving while the waterfall runs remain in the Inbox and are not part of the frozen proposed batch.
  • Prompt assembly precedes agent/pre-step. A listener may change messages entering history, but the proposed position and cancellation signal are already established.
  • Dynamic runtime context is not a hidden string spliced into a request. When it changes, it becomes a candidate UserMessage with a plugin source and is logged with the accepted batch.
Design assessment

A plugin can reject, wrap, or extend a Step, but it must accept that claiming already occurred. Rejection does not automatically put the prompt back. The durable blocked Turn accounts for work consumed without reaching the model.

5. A Step is one reconstruct-request, stream, and tool-settlement cycle

Model path

A Step renders system text from that assembly and calls session.deriveMessages() at each request attempt. Every streamed chunk is immediately appended as assistant/chunk and fed to BlockAssembler. Every successful finish writes an assistant/message completion anchor—even with empty content—with usage, provenance, and exact sourceEventSeqs. LOOP-STEP-STATE

Model finishStep returnsTool behaviorTurn tendency
Normal, no tool callcompletedNoneEnd after turn-stopping
max-tokensmax-tokensDo not dispatch even if content contains tool callsReason is sticky within this Turn
Normal, tool calls, not concludednullExecute and persist resultsOpen another Step so the model reads results
Normal, tool calls, concludedcompletedExecute and persist resultsMay end immediately unless steering is pending
Round-trip tests

A simple-turn test proves strict boundary order—turn/start → step/start → step/end → turn/end—with Inbox receipt earlier. A tool test proves call/result enter the Session log and the next derived request in the same Turn. LOOP-ORDER-TEST

6. A Request is a foldable log snapshot, not an opaque memory object

1

Seed route proposal

The first Step starts from AgentOptions. Later Steps remove fields marked adapter-derived from the latest header so a new route can resolve its own defaults.

2

agent/request waterfall

Plugins may return provider/model/temperature/maxTokens/stop and related config; both route fields must exist before dispatch.

3

Prepare the exact adapter call

llm.prepareCall() validates adapter-owned fields, materializes exact-model defaults, and freezes the adapter registration and retry policy.

4

Commit request/header

The canonical header contains effective config, adapter-default markers, system, and tools. A loop instance writes initial/resume first, then change snapshots only when needed.

5

Commit request/context

A capacity record is added only when provider, model, or contextWindow changes.

6

Freeze the dispatch object

Construct and mark the loop request from durable-derived messages, header system/tools, sessionId, and the turn signal.

Production control flow

The prepared call retains the exact adapter registration that resolved defaults, preventing HMR from combining one adapter's capability decision with another adapter's dispatch. An unregistered route temporarily keeps the proposal so llm/stream middleware may take full ownership; unhandled terminal dispatch still fails with NO_ADAPTER. LOOP-REQUEST-BUILD

Runtime invariant

The invariant companion checks only loop-marked requests: request and messages must be frozen, the Session must be live, and step/start plus request/header must exist. It then independently compares session.deriveMessages() and the folded header, reporting any mismatch as reconstruction desynchronization. LOOP-REQUEST-INVARIANT

Theorem-style test

Across tool Steps, later Turns, system-prompt changes, and request-config changes, the test replays the pre-dispatch event prefix into a new Session and proves field-by-field equality with the real messages and header. LOOP-RECONSTRUCTION-TEST

7. End reasons are durable accounting, not merely UI state

TurnEndReasonTriggerMay have a Step?Meaning for claimed work
completedNormal finish, a concluding tool, or an empty first batchYes or noWork settled through a normal boundary
max-tokensAny Step reaches the output capYesThe truncation fact remains even if a forced later Step succeeds
blockedPre-step rejectionNoClaimed input was consumed without reaching the model
abortedThe turn signal is aborted by user/parent/disposal causePossiblyRecords cancellation source; started effects still drain
errorUnrecovered model, plugin, or tool failurePossiblyLlmError retains structured failure; other errors become UNKNOWN + errorChain
Outcome tests

Mid-stream cancel records aborted with its cause. Max-tokens is sticky inside one Turn even if turn-stopping forces a successful next Step, but does not contaminate the next Turn. Tool calls from a max-token-truncated finish do not execute. LOOP-OUTCOME-TEST

8. Recovery owns model-request failures, not every exception

Request recovery

When BlockAssembler produces an error/aborted finish, the loop invokes the agent/request-error waterfall with turn/step, provider, structured failure, the prepared registration's immutable retryPolicy, and signal. Only {kind:'retry'} rebuilds and resends within the same Step; otherwise the loop throws LlmError. LOOP-STEP-STATE

Boundary tests

An agent/request middleware throw bypasses request recovery. Provider failures may be repeatedly recovered by one listener. If a listener cancels and also returns retry, cancellation wins. A recovery listener that fails ends the Turn in error. LOOP-REQUEST-ERROR-TEST

Other failures—prompt rendering, pre-step plugins, tools/result processing, and request middleware—go directly to the Turn error boundary. throwError() first emits live agent/error; Turn finally records the durable error reason; the driver contains the reported error and returns to idle. LOOP-ERROR-CONTAINMENT-TEST

9. Extension seams decide which layer owns a capability

Extension needOwning seamWhy it is outside the loop
Compaction trigger / overflow repairagent/pre-step / agent/request-errorContext policy, not mechanical Turn structure
Model route and request configagent/requestProvider selection is replaceable and must enter the logged header
Retry wait and policyagent/request-errorThe loop understands a retry action, not backoff policy
Tool permission, approval, and sandboxingtools/pre-execute / guards / post / resultPer-call governance must compose across plugins
Force continuation or terminationagent/turn-stoppingThe loop exposes its natural stop point; policy may steer or cancel
Persistence, UI, and telemetrysession/eventThe durable fact is already shared; the loop need not invoke each consumer

The default loop has no built-in turn budget. Tools or steering can keep producing Steps. A deployment that needs runaway-turn limits must count and cancel at an existing seam. This is deliberate “mechanism in core, policy in plugins,” and a risk control operators must actively provide.

10. Benefits, costs, and semantic boundaries

ChoiceBenefitCost
Durable Turn/Step boundariesFailure, cancellation, and tool continuation are replayable and attributableEven no-step and blocked work adds lifecycle events
deriveMessages on every StepNo hidden conversation buffer; resume and replay are isomorphicEvent-vocabulary and projection compatibility become core burdens
Request-header snapshotsPrompt, tools, route defaults, and model capacity are explainableCanonicalization and default markers must remain exact for cache and replay
Raw chunks plus completion anchorUI fidelity and canonical history coexistLarger logs and explicit chunk-to-message provenance
Narrow request-error recoveryRetry only repeats provider requests that are safe to reconstructPlugin authors must distinguish request failure from Step/Turn failure
Plugin-owned policyThe loop stays stable while compaction, permission, and planning compose independentlyBehavior spreads across waterfall ordering, making composition tests essential
Chapter assessment

The default loop is not designed as the “Agent brain.” It is an event-commit machine. Its strongest principle is that what the model saw and how work ended become reconstructable log facts. Its largest risk is extension dispersion: correctness no longer belongs to agent.ts alone, but to every waterfall, tool pipeline stage, and Session projection jointly honoring the order contract.

Chapter verification checklist

  • Separated driver, Turn, Step, and request attempt.
  • Traced turn/start, claim, pre-step, Step boundaries, and turn/end across principal branches.
  • Verified reject, empty initial Step, empty continuation, and input arriving after a claim.
  • Traced streamed chunks, assistant completion anchors, tool continuation, and concluded tools.
  • Verified request proposals, adapter defaults, header/context logging, and frozen dispatch.
  • Used the invariant and reconstruction test to verify independent request reconstruction from the log.
  • Verified completed, max-tokens, blocked, aborted, and error outcomes.
  • Separated request-error recovery from other Step/Turn exceptions.

My Learning Notes

Autosaved only in this browser. Nothing is uploaded or committed. Export Markdown whenever you want to keep a copy.