Starphix
Agentic Engineering / Sources
The Lab / Sources / Agentic Engineering / Harness/Fundamentals/Agent-Loop.md

The Agent Loop

The agent loop is the beating heart of any harness. It's the control structure that determines how the agent thinks, acts, and adapts until it completes a task or needs human input.

The Core Cycle

At its simplest, every agentic harness runs some variation of this loop:

graph TD
    A[User Input/Goal] --> B[Build Context]
    B --> C[Think/Plan]
    C --> D{Need to Act?}
    D -->|Yes| E[Select Tools]
    E --> F[Check Permissions]
    F --> G[Execute Tools]
    G --> H[Observe Results]
    H --> I[Update State]
    I --> B
    D -->|No| J[Respond to User]
    J --> K{Task Complete?}
    K -->|No| B
    K -->|Yes| L[Done]

    style A fill:#e1f5fe
    style C fill:#fff3e0
    style G fill:#f3e5f5
    style J fill:#e8f5e9
    style L fill:#c8e6c9

Loop Phases

Phase Purpose What Happens
1. Input Receive goal/task User message, continuation, or sub-agent callback
2. Context Building Assemble relevant information System prompt, history, files, memory, tools, project context
3. Reasoning/Planning Decide next action Model thinks about what to do next given goal and context
4. Tool Selection Choose appropriate tools Model returns tool calls with arguments
5. Permission Check Validate safety Check against allowlists, rules, and approval requirements
6. Execution Run tools Execute in sandbox/environment, handle errors
7. Observation Process results Format results, check for errors, update understanding
8. Evaluation Decide whether to continue Is task complete? Need more info? Blocked?
9. Response Communicate with user Text response, summary, or ask for clarification

Loop Variants

ReAct (Reason + Act)

The most common pattern. Model alternates between reasoning and acting.

while not done:
    thought = model.think(context)
    if thought.action == "tool_call":
        result = execute(thought.tool, thought.args)
        context.add_observation(result)
    elif thought.action == "respond":
        return thought.response

Plan-Execute

Plan the entire approach first, then execute steps systematically.

plan = model.plan(goal)  # Decompose into steps
for step in plan:
    result = execute_step(step, context)
    context.update(result)
    if needs_replan(result):
        plan = model.replan(goal, plan, results)
return summarize(results)

Thought-Tool-Thought

Model can interleave multiple tool calls and reasoning in one turn.

response = model.generate(context, tools=tools)
for tool_call in response.tool_calls:
    result = execute(tool_call)
    context.add(tool_call, result)
# Model sees all results and decides next step

Control Flow Decisions

The harness must decide: - When to stop - Task complete, error unrecoverable, user asked to stop - When to ask user - Ambiguous requirements, destructive action, missing info - When to delegate - Task is complex, can be parallelized, needs specialization - When to replan - Approach not working, new information discovered - When to compact context - Approaching token limits

Loop Termination Conditions

Condition Description Action
Task Complete Goal achieved Summarize and stop
User Response Model gives final answer with no tool calls Present response, wait for input
Max Turns Exceeded turn limit Stop with explanation
Max Cost Exceeded budget Stop with cost summary
User Interrupt User cancels/stops Gracefully stop
Error Threshold Too many consecutive failures Ask for help or stop
Timeout Task taking too long Warn and potentially stop

State Between Iterations

The harness must maintain state across loop iterations:

interface AgentState {
  sessionId: string;
  messages: Message[];      // Conversation history
  taskList: Todo[];         // Current tasks
  toolResults: ToolResult[];
  memory: Memory[];
  filesRead: Set<string>;
  subAgents: SubAgent[];
  turnCount: number;
  tokensUsed: TokenCount;
  status: 'thinking' | 'tool_call' | 'responding' | 'done';
}

Implementation Patterns

Turn-Based Loop

async def run_loop(goal, max_turns=50):
    state = initialize_state(goal)
    for turn in range(max_turns):
        context = build_context(state)
        response = await model.generate(context)
        if response.tool_calls:
            for call in response.tool_calls:
                if not has_permission(call): 
                    await ask_user(call); continue
                result = await execute_tool(call)
                state.add_tool_result(call, result)
        elif response.text:
            state.add_message(response.text)
            if is_complete(response): break
        state.turn_count += 1
    return state.summary()

Event-Driven Loop

More flexible, handles streaming and interrupts better.

class AgentLoop:
    async def run(self, input):
        await self.emit('start', input)
        while self.should_continue():
            await self.emit('thought')
            action = await self.decide_next_action()
            if action.type == 'tool':
                await self.execute_tool(action)
            elif action.type == 'respond':
                await self.respond(action)
                break
            elif action.type == 'delegate':
                await self.spawn_subagent(action)
            await self.check_interrupts()
        await self.emit('end')

Best Practices

  1. Keep the harness in control - The loop logic should be deterministic, not fully delegated to the model
  2. Limit turns - Prevent infinite loops with reasonable limits
  3. Checkpoint often - Save state after significant actions
  4. Handle interrupts - Allow user to pause/stop/correct
  5. Log everything - Every iteration should be traceable
  6. Compact aggressively - Don't let context grow unbounded
  7. Parallelize safely - Only parallelize independent tool calls
  8. Validate before acting - Permission checks before execution

Common Pitfalls

Pitfall Problem Solution
Runaway loops Agent keeps calling tools without progress Track tool call history, detect loops, add circuit breaker
Context bloat Too much history, slow and expensive Summarize old turns, prune irrelevant files
Blind execution Running dangerous commands without checking Permission system, dry-run mode
Lost state Crash loses progress Checkpointing after each turn/tool batch
Poor recovery One tool failure derails everything Try alternatives, ask user, continue if possible
  • Harness Architecture — Overall architecture
  • Task Management — Planning and task tracking
  • Error Recovery — Handling failures
  • Sub Agents — Delegation
  • Context Builder — Building context

Sources

  • ReAct paper (Yao et al., 2022)
  • Plan-and-Solve (Wang et al.)
  • Anthropic - Building Effective Agents
  • OpenAI Agents SDK patterns
  • LangGraph agent patterns

From the Agentic Engineering research vault · CC BY 4.0 · all notes · raw markdown