Skip to content

Execution Flow

This page traces a task from the moment a user submits a message through DAG resolution, tool dispatch, sub-agent calls, and final result delivery.

User message
Chat API endpoint (/api/chat)
Session Manager ← Loads agent config, memory context
ExecutionEngine
├─ Task planner ← Decomposes goal into tasks if project context
│ ↓
│ DAG Resolver ← Resolves task order from dependencies
│ ↓
│ Task Executor ← Runs tasks sequentially or in parallel
Agent Loop ← Multi-turn conversation with LLM
├─ LLM API call ← Anthropic/OpenAI/etc.
├─ Tool use response
│ ↓
│ ToolDispatcher
│ ├─ Built-in tools (bash, file, web_search, browser, ...)
│ ├─ MCP tools (mcp__server__tool_name)
│ └─ Sub-agent (spawn_sub_agent)
│ ↓
│ SubAgentLifecycle
│ ↓
│ [Nested agent loop]
│ ↓
│ ResultAggregator
Result delivery ← SSE stream to UI / channel message
Memory update ← Episode stored

The chat API endpoint receives a POST:

POST /api/chat/{agent_id}
{"message": "Analyze the test failures in the CI pipeline"}

It:

  1. Validates auth (API key check)
  2. Loads the agent config from agents table
  3. Creates or resumes a session (session_id)
  4. Starts an SSE response stream

The SessionManager:

  1. Loads recent episodic memory (configurable window, default last 20 episodes)
  2. Runs semantic retrieval to find relevant past context
  3. Constructs the system prompt with agent personality + memory context
  4. Injects any project/task context if the message is part of a project

The core execution is an agentic loop:

while True:
response = await llm.create(
model=agent.model,
system=system_prompt,
messages=conversation_history,
tools=available_tools,
)
if response.stop_reason == "end_turn":
break # LLM is done
if response.stop_reason == "tool_use":
for tool_call in response.tool_uses:
result = await tool_dispatcher.dispatch(tool_call)
conversation_history.append(tool_result(tool_call.id, result))
# Continue loop with updated history

Maximum turns: configurable per agent (default: 50).

ToolDispatcher routes tool calls to their handler:

tool_name
├─ "bash" → BashTool.execute()
├─ "read_file" → FileTool.read()
├─ "write_file" → FileTool.write()
├─ "web_search" → BraveSearchTool.search()
├─ "browser_navigate" → BrowserManager.navigate()
├─ "spawn_sub_agent" → SubAgentSpawner.spawn()
└─ "mcp__*__*" → MCPConnectionManager.call_tool()

Tool results flow back into the conversation as tool_result messages.

When the agent calls spawn_sub_agent:

  1. SubAgentSpawner.spawn() validates permissions
  2. Creates a SubAgent record in the database
  3. Starts an isolated session (with filtered tool set)
  4. Runs the sub-agent’s own agent loop concurrently
  5. Returns partial or final result to the parent
  6. ResultAggregator merges results using the configured strategy

Sub-agents can spawn their own sub-agents (up to depth 3).

Certain actions require human approval before proceeding:

  • Execution pauses
  • ApprovalRequest is created and streamed to the UI
  • Agent waits (up to configurable timeout)
  • On approval: continues; on rejection: tool result is “action rejected by user”

After the session ends (user stops interacting or max turns reached):

  1. The entire conversation is serialized as episodic memory entries
  2. If a vector store is configured, embeddings are computed and stored
  3. The knowledge graph is updated with any new entities discovered

Throughout execution, the SSE stream delivers:

event: delta
data: {"text": "I'll analyze the CI failures...", "session_id": "sess_abc"}
event: tool_use
data: {"tool": "bash", "input": {"command": "cat ci.log | grep FAILED"}}
event: tool_result
data: {"tool": "bash", "output": "3 tests failed: test_auth, test_payment..."}
event: delta
data: {"text": "Found 3 test failures. The root cause is..."}
event: done
data: {"session_id": "sess_abc", "token_usage": {"input": 1842, "output": 547}}

The UI renders these events in real time.

When an agent is working on a project (not a free-form chat), the flow changes:

  1. Project goal is decomposed into tasks by the task planner
  2. Tasks are arranged in a DAG based on depends_on relationships
  3. The DAGResolver computes the execution order
  4. Tasks execute per the project’s execution_strategy:
    • sequential: one at a time
    • parallel: all independent tasks simultaneously
    • dag: topological order with concurrency where possible
  5. Each task runs its own agent loop
  6. Project status updates as tasks complete (in_progresscompleted)

Tasks that fail are classified by failure type:

ClassExamplesDefault retry
transientNetwork timeout, rate limitYes — up to 3x with backoff
tool_errorBash command failedYes — up to 2x
llm_errorModel API errorYes — 1x after 10s
context_limitToken limit exceededCompress context and retry
permanentInvalid tool argsNo
human_rejectedUser rejected actionNo

Retry delays use exponential backoff: 10s, 30s, 90s.

Multiple agent sessions can run concurrently. Each session has its own:

  • Conversation history
  • Active tool calls
  • Sub-agent tree

Limits:

  • Max concurrent sessions per agent: 5 (configurable)
  • Max concurrent sub-agents globally: 8
  • Max concurrent workflow runs: 10 (configurable via WORKFLOW_MAX_CONCURRENT_RUNS)