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()

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