# 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) 1. Builds the user prompt via `build_prompt_content()` — resolves context paths, forced tools, attached skills, and images 2. Creates SDK hooks via `create_sdk_hooks()` — permission checking, tool approval, result formatting 3. Builds the full options dict via `build_agent_options()` — system prompt, MCP servers, tool permissions, API config 4. Creates `ClaudeAgentOptions` and calls `query()` to start the streaming agent loop 5. 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 | 6. On completion: sets status to `completed`, persists session, fires analytics 7. On cancellation: sets status to `stopped` 8. 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-approve - `deny` → auto-reject - `ask` → triggers HITL approval (except for `AskUserQuestion` which is always allowed) **`pre_tool_hook(input_data, tool_use_id)`** - Enforces `deny` policy by returning a denial result - For `ask` policy, calls `request_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.executed` analytics (tool name, MCP server, duration, content length) - Normalizes response content from the SDK - Creates `tool_result` Message objects and appends to session - Special handling for `Agent` tool 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 `Agent` tool call - Creates a child `AgentSession` with 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____` 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_SKILL` is injected when mode is `"view-builder"` - API key vs. 9Router proxy configuration (with `bare` mode and `cc/` 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 `None` if all fragments are empty **`resolve_forced_tools(forced_tools, load_all_tools_fn)`** - Builds XML `` 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 `` 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 `` 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 `` or `` 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 equivalent - `mcp__openswarm-invoke-agent__X` → maps to the builtin InvokeAgent equivalent - Other MCP tools → per-server per-tool permission lookup - Default policy: `"ask"` **`get_all_tool_names()`** - Returns `FULL_TOOLS` (minus denied builtins) + `mcp:` 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) 1. Creates an `ApprovalRequest` with a unique ID 2. Adds it to the session's `pending_approvals` 3. Sets session status to `waiting_approval`, emits via WebSocket 4. Calls `ws_manager.send_approval_request()` — creates an `asyncio.Future` and waits 5. User's decision resolves the Future (or timeout triggers auto-deny) 6. Records `approval.requested` and `approval.resolved` analytics (with latency) 7. Cleans up, restores status to `running` 8. 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:** 1. The SDK starts this as a child process 2. It reads JSON-RPC messages from stdin 3. For `tools/list`: returns the `InvokeAgent` tool schema 4. For `tools/call`: POSTs to `http://127.0.0.1:{port}/api/agents/invoke-agent/run` 5. The backend forks the source session, runs the agent loop, and returns the response 6. 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 tracking - `OPENSWARM_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.