execution/ — Agent Runtime Engine
This package contains the runtime core of the agent system: the Claude Agent SDK query loop, prompt assembly, MCP server configuration, SDK hooks, and the human-in-the-loop approval flow.
How It All Fits Together
run_agent_loop() ← entry point (called by AgentManager)
│
├─ build_prompt_content() ← prompt_builder.py
│ └─ resolve_context_paths() ← prompt_context.py
│
├─ create_sdk_hooks() ← agent_hooks.py
│ └─ request_approval() ← approval.py
│ └─ get_effective_policy() ← mcp_builder.py
│
├─ build_agent_options() ← agent_options.py
│ ├─ resolve_mode() ← prompt_builder.py
│ ├─ compose_system_prompt() ← prompt_builder.py
│ ├─ build_connected_tools_context()← prompt_context.py
│ ├─ build_browser_context() ← prompt_context.py
│ └─ build_mcp_servers() ← mcp_builder.py
│
└─ Claude Agent SDK query() ← streaming loop
├─ StreamEvent → ws_manager (real-time token streaming)
├─ AssistantMessage → Messages (text + tool calls)
└─ ResultMessage → cost/tokens (final accounting)
Files
agent_loop.py — Main Query Loop
The top-level entry point for running an agent. Orchestrates everything else.
run_agent_loop(sessions, session_id, prompt, ...) (async)
- Builds the user prompt via
build_prompt_content()— resolves context paths, forced tools, attached skills, and images - Creates SDK hooks via
create_sdk_hooks()— permission checking, tool approval, result formatting - Builds the full options dict via
build_agent_options()— system prompt, MCP servers, tool permissions, API config - Creates
ClaudeAgentOptionsand callsquery()to start the streaming agent loop - Iterates the async stream, dispatching to three handlers:
| Event Type | Handler | What It Does |
|---|---|---|
StreamEvent |
_handle_stream_event() |
Real-time text/tool streaming deltas → WebSocket |
AssistantMessage |
_handle_assistant_message() |
Extracts text + tool_use blocks, creates Messages, emits via WS |
ResultMessage |
_handle_result_message() |
Captures session ID, cost, token usage |
- On completion: sets status to
completed, persists session, fires analytics - On cancellation: sets status to
stopped - On error: sets status to
error, creates an error message
agent_hooks.py — SDK Hook Factories
Creates the three hook functions the Claude Agent SDK needs for tool execution control.
create_sdk_hooks(session, session_id, sessions, builtin_perms, ...)
Returns a tuple of (can_use_tool, pre_tool_hook, post_tool_hook):
can_use_tool(tool_name, input_data)
- Looks up the effective permission policy for the tool
always_allow→ auto-approvedeny→ auto-rejectask→ triggers HITL approval (except forAskUserQuestionwhich is always allowed)
pre_tool_hook(input_data, tool_use_id)
- Enforces
denypolicy by returning a denial result - For
askpolicy, callsrequest_approval()and blocks until user decides - Tracks tool start time for duration analytics
post_tool_hook(input_data, tool_use_id)
- Calculates elapsed execution time
- Records
tool.executedanalytics (tool name, MCP server, duration, content length) - Normalizes response content from the SDK
- Creates
tool_resultMessage objects and appends to session - Special handling for
Agenttool results — creates sub-agent sessions via_build_sub_agent_session() - Emits everything via WebSocket
_build_sub_agent_session(input_data, raw_response, content, session, ...)
- Parses the result of an
Agenttool call - Creates a child
AgentSessionwith the sub-agent's messages - Stores it in the sessions dict and broadcasts via WebSocket
agent_options.py — ClaudeAgentOptions Builder
Assembles the complete configuration dict passed to the Claude Agent SDK.
build_agent_options(session, builtin_perms, hooks..., fork_session?, selected_browser_ids?) (async)
Builds a kwargs dict containing:
| Key | Source | Description |
|---|---|---|
system_prompt |
compose_system_prompt() |
Global + mode + session + tool context + browser context |
mcp_servers |
build_mcp_servers() |
User MCP tools (with OAuth2 refresh) |
| + browser MCP | stdio subprocess | openswarm-browser-agent (if not fully denied) |
| + invoke MCP | stdio subprocess | openswarm-invoke-agent (if not fully denied) |
allowed_tools |
_compute_tool_permissions() |
Tools with always_allow or ask policy |
disallowed_tools |
_compute_tool_permissions() |
Tools with deny policy |
model |
resolve_model_id() |
Resolved from session model + provider |
api_key or proxy |
settings | Direct Anthropic key or 9Router proxy URL |
session_id |
session | For SDK resume/fork support |
_compute_tool_permissions(session, builtin_perms, mcp_servers, ...)
Maps permission policies to the SDK's mcp__<server>__<tool> naming convention:
- Builtins → direct policy lookup
- Browser/invoke MCP tools → mapped back to their builtin equivalents
- User MCP tools → per-server per-tool permission lookup
Special handling:
VIEW_BUILDER_SKILLis injected when mode is"view-builder"- API key vs. 9Router proxy configuration (with
baremode andcc/model prefix) - Resume vs. fork session behavior
prompt_builder.py — Prompt Composition
Stateless helpers for building the system prompt and user prompt content.
resolve_mode(mode_id, get_all_tool_names_fn)
- Loads mode definition (from the modes package)
- Returns
(allowed_tools, system_prompt, default_folder) - Falls back to all tools if mode not found
compose_system_prompt(default_prompt, mode_prompt, session_prompt, tools_ctx?, browser_ctx?)
- Joins non-empty prompt fragments with
\n\n - Returns
Noneif all fragments are empty
resolve_forced_tools(forced_tools, load_all_tools_fn)
- Builds XML
<forced_tools>block describing user-selected tools - Includes tool descriptions, MCP server names, and connected account emails
resolve_attached_skills(attached_skills)
- Formats skill attachments as
[Using skill: name]\n\ncontent
build_prompt_content(prompt, images?, context_paths?, forced_tools?, attached_skills?, ...)
- Orchestrator that calls all the above
- If images are present: returns a multimodal content list with base64 image blocks
- Otherwise: returns a plain string
prompt_context.py — Context Building
Generates XML context blocks that get injected into the system or user prompt.
build_connected_tools_context(allowed_tools, ...)
- Generates
<connected_mcp_tools>XML listing all MCP servers, their status, connected accounts, and available tools - Also lists installed-but-not-connected tools
build_browser_context(dashboard_id, selected_browser_ids?)
- Generates
<browser_agent_instructions>XML explaining the browser delegation tools - Lists user-selected browser cards with IDs, titles, and current URLs
get_pre_selected_browser_ids(dashboard_id)
- Returns browser card IDs from the dashboard layout
resolve_context_paths(context_paths)
- For each path: reads file contents (up to 512KB) or builds directory tree (depth 4)
- Wraps in
<context_file>or<context_directory>XML tags
build_dir_tree(root, max_depth=4, prefix="")
- Recursive directory listing, skipping dotfiles
mcp_builder.py — MCP Server Config + Tool Policies
Manages MCP server configuration and resolves tool permission policies.
Constants:
FULL_TOOLS— canonical list of ~22 built-in tool names (Read, Edit, Write, Bash, Glob, Grep, AskUserQuestion, WebSearch, WebFetch, NotebookEdit, TodoWrite, EnterPlanMode, ExitPlanMode, EnterWorktree, TaskOutput, TaskStop, CronCreate, CronList, CronDelete, RenderOutput, InvokeAgent, Agent)
build_mcp_servers(allowed_tools) (async)
- Iterates enabled MCP tools, filters by allowed list
- Skips fully-denied tools
- Refreshes OAuth2 tokens for Google tools
- Derives MCP server configs
- Returns
{server_name: config}dict
get_effective_policy(tool_name, builtin_perms)
- Resolves policy for any tool name:
- Builtins → direct lookup in
builtin_perms mcp__openswarm-browser-agent__X→ maps to the builtin browser tool equivalentmcp__openswarm-invoke-agent__X→ maps to the builtin InvokeAgent equivalent- Other MCP tools → per-server per-tool permission lookup
- Builtins → direct lookup in
- Default policy:
"ask"
get_all_tool_names()
- Returns
FULL_TOOLS(minus denied builtins) +mcp:<name>for enabled/connected MCP tools
Helper functions: _get_denied_tool_names(), _get_all_known_tool_names(), _is_fully_denied()
approval.py — Human-in-the-Loop Approval
Shared approval flow used by both the main agent and browser sub-agents.
request_approval(session, tool_name, tool_input, timeout?, track_analytics?) (async)
- Creates an
ApprovalRequestwith a unique ID - Adds it to the session's
pending_approvals - Sets session status to
waiting_approval, emits via WebSocket - Calls
ws_manager.send_approval_request()— creates anasyncio.Futureand waits - User's decision resolves the Future (or timeout triggers auto-deny)
- Records
approval.requestedandapproval.resolvedanalytics (with latency) - Cleans up, restores status to
running - Returns
{"behavior": "allow"|"deny", "message": ..., "updated_input": ...}
agent_mock.py — Session Completion Analytics
fire_session_completed(session, sessions_dict)
Fires a comprehensive session.completed analytics event with:
- Model, provider, mode
- Total cost (USD), token usage (input + output)
- Message count, duration (seconds)
- Final status, tool usage counts
- Session title, first user message
- Sub-agent count and IDs
- Branch count
invoke_agent_mcp_server.py — InvokeAgent MCP Server
A standalone stdio MCP server launched as a subprocess by the Claude Agent SDK.
How it works:
- The SDK starts this as a child process
- It reads JSON-RPC messages from stdin
- For
tools/list: returns theInvokeAgenttool schema - For
tools/call: POSTs tohttp://127.0.0.1:{port}/api/agents/invoke-agent/run - The backend forks the source session, runs the agent loop, and returns the response
- The MCP server formats and returns the result
Environment variables it reads:
OPENSWARM_PORT— backend port (default 8325)OPENSWARM_PARENT_SESSION_ID— parent session for trackingOPENSWARM_DASHBOARD_ID— dashboard context
No internal Python imports — communicates with the backend purely via HTTP. This is necessary because it runs as a separate subprocess.