Skip to content

State Machine Workflow

Z-M-Huang edited this page Apr 28, 2026 · 4 revisions

State Machine Workflow

An SM-driven workflow runs as a stage pipeline inside the orchestrator session. Each stage is a fresh LLM transcript bounded by its own allowedTools, completionTool, completionSchema, and turnCap. The SM is the authority over stage order; the orchestrator session provides the active interactor set, consent posture, and tool set that every stage inherits.


Setup

  • The project's codeReview SM is attached to the orchestrator session.
  • Pipeline: Plan → Execute → Review.
  • Stage definitions:
    • Plan: allowedTools: [Read, Grep, Glob], completionTool: submit_plan, turnCap: 20.
    • Execute: allowedTools: [Read, Grep, Glob, Edit, Write], completionTool: submit_diff, turnCap: 40.
    • Review: allowedTools: [], completionTool: submit_review, turnCap: 10.
  • Security mode: ask; bundled TUI active as an interactor.

Sequence

sequenceDiagram
    autonumber
    actor User
    participant Orchestrator as orchestrator session
    participant Core as core
    participant StageExec as stage execution
    participant LLM

    User->>Orchestrator: "/run codeReview refactor auth module"
    Orchestrator->>Core: start SM pipeline entry stage
    Core->>StageExec: begin Plan Setup
    StageExec->>StageExec: resolve allowedTools, populate StageContext
    StageExec->>LLM: Init interpolates body against StageContext, sends system prompt
    LLM-->>StageExec: propose Edit tool call (out of allowedTools)
    Core->>Orchestrator: grantStageTool request (auto-issued)
    Orchestrator->>User: approve Edit for this proposal?
    User-->>Orchestrator: deny
    Orchestrator-->>Core: deny
    Core-->>LLM: typed denied-tool-call result
    LLM-->>StageExec: propose Grep tool call
    StageExec->>Core: in allowedTools → SM-approve (mode gate bypassed)
    Core-->>LLM: Grep result
    LLM-->>StageExec: call submit_plan with payload
    StageExec->>StageExec: parse completionSchema ok
    StageExec->>StageExec: Assert ok
    StageExec->>Core: Exit with plan result
    Core->>StageExec: Next returns Execute sequential
    Core->>StageExec: begin Execute Setup
    StageExec->>LLM: Init with Execute prompt plus plan
    LLM-->>StageExec: call Edit tool
    StageExec->>Core: in allowedTools → SM-approve (mode gate bypassed)
    Core-->>LLM: Edit result
    LLM-->>StageExec: call submit_diff
    StageExec->>Core: Exit with diff result
    Core->>StageExec: Next returns Review sequential
    Core->>StageExec: begin Review
    StageExec->>LLM: Init with Review prompt plus diff
    LLM-->>StageExec: call submit_review
    StageExec->>Core: Exit with review result
    Core-->>Orchestrator: pipeline done
    Orchestrator-->>User: workflow complete
Loading

What happened at each boundary

Boundary Who acted Why it matters
Plan Setup Stage execution Resolved the stage's tool set against the orchestrator's live set; populated StageContext. The body template is interpolated at Init, not here.
Plan Init Stage execution Interpolated the Plan body template against StageContext and sent the composed system prompt under a fresh transcript.
Plan Act first LLM turn LLM → stage LLM proposed Edit, which is out of Plan.allowedTools. Core auto-issued a grantStageTool prompt through the orchestrator session's active interactors; the user denied. A typed denied-tool-call result returned on the same transcript; the LLM replanned within the envelope.
Plan Assert Stage execution completionSchema parsed the plan payload; semantic check (non-empty steps) would go here.
Plan Exit → Next SM pipeline code Inspected Plan's StageResult, returned { stages: [Execute], execution: sequential }.
Execute Act Stage execution Different transcript, wider tool set. Still one orchestrator session, same active interactor set.
Review Act Stage execution No tools allowed — the stage must synthesise a review from the plan and diff passed in via ctx.upstream.

Every boundary records an audit event. See Audit Trail.


SM precedence in action

The trace above shows Plan.allowedTools forcing an out-of-envelope Edit through grantStageTool — the security-mode gate (even yolo) never gets a chance to approve the call on its own, because an out-of-envelope proposal is routed to active interactors regardless of mode. If the user had approved, the minted token would have covered exactly that proposal; a later Edit with different arguments would trigger a fresh prompt. See the safety-critical precedence invariant.


One-shot grants mid-stage

When the LLM proposes a tool call not in the stage's allowedTools, core auto-issues a grantStageTool interaction request — the stage author does not invoke this. Example: during Execute the LLM calls Bash("git status") and Bash is not in Execute.allowedTools; core bundles the proposal identity ({stageExecutionId, attempt, proposalId, tool, argsDigest, argsSummary}) and routes it to the orchestrator session's active interactors. On approve the minted token covers exactly this proposal — a later Bash call with different arguments needs a fresh grant. Consuming a matching token is an SM-approve decision: the security-mode gate is bypassed, guard hooks still run. On deny, defer, or no interactor (headless), a typed denied-tool-call result returns to the LLM on the same transcript and Act continues.

See Interaction Protocol grantStageTool and Stage Executions one-shot tool grants.


What the SM does not do

  • Does not read LLM text to decide transitions. Next() inspects StageResult — the parsed completion payload, the Assert verdict, and SM state — not the transcript.
  • Does not gate commands. /tools, /reload, /model, etc. are orthogonal to the SM. See Command Model.
  • Does not replace the orchestrator session's interactors. Every user prompt from inside a stage goes through the session's active interactor set.

Parallel fan-out

A pipeline may fan out: Next() returns { stages: [Test, Lint], execution: "parallel", join: Review }. Siblings run concurrently up to the session's parallel stage cap, their Exit outputs aggregate into ctx.upstream, and Review runs as the join. A failure in any sibling strict-cancels the rest and fails the workflow. See Concurrency and Cancellation and Stage Executions fan-out.


Determinism

With deterministic Next() and deterministic tools, this pipeline is much closer to deterministic than a freeform chat would be. Caveats:

  • The LLM is still non-deterministic — given the same stage prompt, it may propose different tools.
  • Tools that read the network or live filesystem break determinism if the external state changes.
  • Compaction (if fired) introduces non-determinism. Stages have their own transcripts, so the total token budget spreads across stage bodies — keep each stage's budget tight.

See Determinism and Ordering, Ralph.


Related pages

Introduction

Reading

Core runtime

Contracts

Category contracts

Context

Security

Runtime behavior

Operations

Providers (bundled)

Integrations

Reference extensions

Tools

UI

Session Stores

Loggers

Providers

Hooks

Context Providers

Commands

Case studies

Flows

Maintainers

Clone this wiki locally