Skip to content

Architecture: replace agentic while-loop with an explicit state machine #60

Description

@zjshen14

Background

Agent.run() in src/agent/core.ts is a single while(true) loop managing multiple mutable variables — turns, lastCallSig, stuckCount, pendingCalls, responseText — all in one function. The phases of the loop (streaming, collecting tool calls, executing, feeding results back) are implicit, interleaved in the same control flow.

Problems this causes

  • Safety guards are hardcoded: max-turns and stuck-loop detection are if-statements in the loop body. Adding a new guard (cost limit, time limit, human interrupt checkpoint) means editing the loop itself.
  • No middleware layer: there is no clean injection point for cross-cutting concerns (observability, rate limiting, token tracking) between phases.
  • Hard to test phase transitions: to unit-test stuck-loop detection you must run the full loop; you can't test the guard in isolation.
  • Pause/resume is impossible: if the user presses Ctrl+C mid-execution, there is no clean way to capture and restore the in-flight state.

Proposed design

Model the loop as an explicit state machine with named states and named transitions:

IDLE
  → STREAMING        (LLM generating text + function calls)
  → EXECUTING_TOOLS  (parallel/sequential tool dispatch)
  → FEEDING_RESULTS  (tool results assembled into user message)
  → DONE
  → ERROR

Each state is a method; each transition is a function call. Guards (max-turns, stuck-loop, cost limit) become composable TransitionGuard objects that run at well-defined checkpoints, not scattered if-statements.

interface AgentState { type: "idle" | "streaming" | "executing" | "feeding" | "done" | "error" }
interface TransitionGuard {
  check(state: AgentState, context: AgentContext): GuardResult; // pass | block(reason)
}

This is the approach taken by production agent frameworks (LangGraph, Temporal workflows) — the state graph is the architecture.

Acceptance criteria

  • Agentic loop phases are represented as distinct, named states
  • Max-turns and stuck-loop detection are TransitionGuard implementations, not inline if-statements
  • A new guard can be added by implementing TransitionGuard and registering it — no changes to the core loop
  • Existing behaviour (streaming, tool execution, plan mode) is preserved
  • Unit tests cover individual guards in isolation

Location

src/agent/core.tsAgent.run()

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    architectureStructural design decisions and system-wide refactorsenhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions