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)
Intent Dispatcher ← Tier 1 routes USER_MESSAGE → DeliverToChat
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

Before the session is loaded, the chat API builds an IntentEnvelope with source=USER_MESSAGE, the room_id from the conversation, and the user’s agent_hint. The Unified Intent Dispatcher’s Tier 1 rule for room_id + kind=MESSAGE matches immediately, producing a DeliverToChat action that resolves to the TurnOrchestrator path below. Dispatch is fire-and-forget on the streaming path — the dispatch hot path adds ≤2 ms p50 and never blocks the first SSE byte.

For non-chat producers (channel webhooks, scheduler ticks, hooks, proactive insights, device events) the same IntentDispatcher.dispatch() call decides which executor receives the signal. The chat case is the fast path because the envelope carries explicit hints; ambiguous envelopes go through Tier 3 (LLM classification).

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

The ToolDispatcher (not the Intent Dispatcher — see the disambiguation note in Step 2) 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)