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
Location
src/agent/core.ts — Agent.run()
Background
Agent.run()insrc/agent/core.tsis a singlewhile(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
Proposed design
Model the loop as an explicit state machine with named states and named transitions:
Each state is a method; each transition is a function call. Guards (max-turns, stuck-loop, cost limit) become composable
TransitionGuardobjects that run at well-defined checkpoints, not scattered if-statements.This is the approach taken by production agent frameworks (LangGraph, Temporal workflows) — the state graph is the architecture.
Acceptance criteria
TransitionGuardimplementations, not inline if-statementsTransitionGuardand registering it — no changes to the core loopLocation
src/agent/core.ts—Agent.run()