Skip to content

Agentic Loop

The agentic loop is the two-phase reasoning mechanism that enables agents to collect tool results and delegation responses, then produce a final streamed response.

How It Works

Configuration

The agentic loop is controlled by the max_steps parameter passed to the Agent:

python
agent = Agent(
    name="my-agent",
    model_api=model_api,
    max_steps=5  # Maximum action collection iterations
)

max_steps

Prevents infinite loops in Phase 1. When reached, returns message:

"Reached maximum reasoning steps (5)"

Guidelines:

  • Simple queries: 2-3 steps
  • Tool-using tasks: 5 steps (default)
  • Complex multi-step tasks: 10+ steps

Action Format (JSON)

Actions are simple JSON objects. The agent recognizes three action types:

Tool Call

json
{"tool": "calculator", "arguments": {"expression": "2 + 2"}}

Delegation

json
{"agent": "researcher", "task": "Find information about quantum computing"}

No Action (Proceed to Final Response)

json
{}

System Prompt Construction

The agent builds an enhanced system prompt with action instructions:

python
async def _build_system_prompt(self) -> str:
    parts = [self.instructions]
    
    if self.mcp_clients:
        tools_info = await self._get_tools_description()
        parts.append("\n## Available Tools\n" + tools_info)
        parts.append(TOOLS_INSTRUCTIONS)
    
    if self.sub_agents:
        agents_info = await self._get_agents_description()
        parts.append("\n## Available Agents for Delegation\n" + agents_info)
        parts.append(AGENT_INSTRUCTIONS)
    
    if self.mcp_clients or self.sub_agents:
        parts.append(NO_ACTION_INSTRUCTIONS)
    
    return "\n".join(parts)

Tool Instructions Template

To use a tool, respond with ONLY this JSON (no other text):
{"tool": "tool_name", "arguments": {"arg1": "value1"}}

Wait for the tool result before providing your final answer.

Delegation Instructions Template

To delegate a task to another agent, respond with ONLY this JSON (no other text):
{"agent": "agent_name", "task": "task description"}

Wait for the agent's response before providing your final answer.

No Action Instructions Template

When you have all the information needed to provide a final answer, respond with ONLY:
{}

Then the system will ask you to provide your final response.

Response Parsing

python
def _parse_action(self, content: str) -> Dict[str, Any]:
    """Parse JSON action from model response."""
    content = content.strip()
    
    # Try parsing entire content as JSON
    try:
        parsed = json.loads(content)
        if isinstance(parsed, dict):
            return parsed
    except json.JSONDecodeError:
        pass
    
    # Look for JSON on a line by itself
    for line in content.split("\n"):
        line = line.strip()
        if line.startswith("{") and line.endswith("}"):
            try:
                return json.loads(line)
            except json.JSONDecodeError:
                continue
    
    return {}  # No action found

Progress Blocks

During Phase 1, the agent emits progress blocks when starting tool/delegation execution:

json
{"type": "progress", "step": 1, "action": "tool_call", "target": "calculator"}
{"type": "progress", "step": 2, "action": "delegate", "target": "researcher"}

Execution Flow

Tool Execution

  1. Parse tool and arguments from JSON action
  2. Emit progress block
  3. Log tool_call event to memory
  4. Execute tool via MCP client
  5. Log tool_result event to memory
  6. Add result to conversation
  7. Continue to next action loop iteration

Delegation Execution

  1. Parse agent and task from JSON action
  2. Emit progress block
  3. Log delegation_request event to memory
  4. Invoke remote agent via A2A protocol
  5. Log delegation_response event to memory
  6. Add response to conversation
  7. Continue to next action loop iteration

Final Response (Phase 2)

  1. When {} or no action detected, exit action loop
  2. Add "provide your final response" prompt
  3. Call model with streaming enabled
  4. Stream tokens directly to client
  5. Log agent_response event to memory

Memory Events

The loop logs events for debugging and verification:

python
# After tool execution
events = await agent.memory.get_session_events(session_id)
# Events: [user_message, tool_call, tool_result, agent_response]

# After delegation
events = await agent.memory.get_session_events(session_id)
# Events: [user_message, delegation_request, delegation_response, agent_response]

Testing with Mock Responses

Set DEBUG_MOCK_RESPONSES environment variable to test loop behavior deterministically.

The two-phase pattern requires:

  1. Action responses (tool/delegate JSON or {} for no action)
  2. Final response text
bash
# Test tool calling (action -> no-action -> final)
export DEBUG_MOCK_RESPONSES='["{\"tool\": \"echo\", \"arguments\": {\"text\": \"hello\"}}", "{}", "The echo returned: hello"]'

# Test delegation (action -> no-action -> final)
export DEBUG_MOCK_RESPONSES='["{\"agent\": \"researcher\", \"task\": \"Find quantum info\"}", "{}", "Based on the research, quantum computing uses qubits."]'

# Simple response (no-action -> final)
export DEBUG_MOCK_RESPONSES='["{}", "Hello! How can I help you?"]'

For Kubernetes E2E tests, configure via the Agent CRD:

yaml
spec:
  container:
    env:
    - name: DEBUG_MOCK_RESPONSES
      value: '["{\"agent\": \"worker\", \"task\": \"process data\"}", "{}", "Done."]'

Best Practices

  1. Set appropriate max_steps - Too low may truncate reasoning, too high wastes resources
  2. Clear instructions - Tell the LLM when to use tools vs. respond directly
  3. Test with mocks - Include {} to signal end of action phase
  4. Monitor events - Use memory endpoints to debug complex flows
  5. Handle errors gracefully - Tool failures are fed back to the loop for recovery

Released under the Apache 2.0 License.