fast-agent-mcp 0.4.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,849 @@
1
+ """
2
+ Agents as Tools Pattern Implementation
3
+ =======================================
4
+
5
+ Overview
6
+ --------
7
+ This module implements the "Agents as Tools" pattern, inspired by OpenAI's Agents SDK
8
+ (https://openai.github.io/openai-agents-python/tools). It allows child agents to be
9
+ exposed as callable tools to a parent agent, enabling hierarchical agent composition
10
+ without the complexity of traditional orchestrator patterns. The current implementation
11
+ goes a step further by spawning **detached per-call clones** of every child so that each
12
+ parallel execution has its own LLM + MCP stack, eliminating name overrides and shared
13
+ state hacks.
14
+
15
+ Rationale
16
+ ---------
17
+ Traditional approaches to multi-agent systems often require:
18
+ 1. Complex orchestration logic with explicit routing rules
19
+ 2. Iterative planning mechanisms that add cognitive overhead
20
+ 3. Tight coupling between parent and child agent implementations
21
+
22
+ The "Agents as Tools" pattern simplifies this by:
23
+ - **Treating agents as first-class tools**: Each child agent becomes a tool that the
24
+ parent LLM can call naturally via function calling
25
+ - **Delegation, not orchestration**: The parent LLM decides which child agents to invoke
26
+ based on its instruction and context, without hardcoded routing logic
27
+ - **Parallel execution**: Multiple child agents can run concurrently when the LLM makes
28
+ parallel tool calls
29
+ - **Clean abstraction**: Child agents expose minimal schemas (text or JSON input),
30
+ making them universally composable
31
+
32
+ Benefits over iterative_planner/orchestrator:
33
+ - Simpler codebase: No custom planning loops or routing tables
34
+ - Better LLM utilization: Modern LLMs excel at function calling
35
+ - Natural composition: Agents nest cleanly without special handling
36
+ - Parallel by default: Leverage asyncio.gather for concurrent execution
37
+
38
+ Algorithm
39
+ ---------
40
+ 1. **Initialization**
41
+ - `AgentsAsToolsAgent` is itself an `McpAgent` (with its own MCP servers + tools) and receives a list of **child agents**.
42
+ - Each child agent is mapped to a synthetic tool name: `agent__{child_name}`.
43
+ - Child tool schemas advertise text/json input capabilities.
44
+
45
+ 2. **Tool Discovery (list_tools)**
46
+ - `list_tools()` starts from the base `McpAgent.list_tools()` (MCP + local tools).
47
+ - Synthetic child tools `agent__ChildName` are added on top when their names do not collide with existing tools.
48
+ - The parent LLM therefore sees a **merged surface**: MCP tools and agent-tools in a single list.
49
+
50
+ 3. **Tool Execution (call_tool)**
51
+ - If the requested tool name resolves to a child agent (either `child_name` or `agent__child_name`):
52
+ - Convert tool arguments (text or JSON) to a child user message.
53
+ - Execute via detached clones created inside `run_tools` (see below).
54
+ - Responses are converted to `CallToolResult` objects (errors propagate as `isError=True`).
55
+ - Otherwise, delegate to the base `McpAgent.call_tool` implementation (MCP tools, shell, human-input, etc.).
56
+
57
+ 4. **Parallel Execution (run_tools)**
58
+ - Collect all tool calls from the parent LLM response.
59
+ - Partition them into **child-agent tools** and **regular MCP/local tools**.
60
+ - Child-agent tools are executed in parallel:
61
+ - For each child tool call, spawn a detached clone with its own LLM + MCP aggregator and suffixed name.
62
+ - Emit `ProgressAction.CHATTING` / `ProgressAction.FINISHED` events for each instance and keep parent status untouched.
63
+ - Merge each clone's usage back into the template child after shutdown.
64
+ - Remaining MCP/local tools are delegated to `McpAgent.run_tools()`.
65
+ - Child and MCP results (and their error text from `FAST_AGENT_ERROR_CHANNEL`) are merged into a single `PromptMessageExtended` that is returned to the parent LLM.
66
+
67
+ Progress Panel Behavior
68
+ -----------------------
69
+ To provide clear visibility into parallel executions, the progress panel (left status
70
+ table) undergoes dynamic updates:
71
+
72
+ **Before parallel execution:**
73
+ ```
74
+ ▎▶ Chatting ▎ PM-1-DayStatusSummarizer gpt-5 turn 1
75
+ ```
76
+
77
+ **During parallel execution (2+ instances):**
78
+ - Parent line stays in whatever lifecycle state it already had; no forced "Ready" flips.
79
+ - New lines appear for each detached instance with suffixed names:
80
+ ```
81
+ ▎▶ Chatting ▎ PM-1-DayStatusSummarizer[1] gpt-5 turn 2
82
+ ▎▶ Calling tool ▎ PM-1-DayStatusSummarizer[2] tg-ro (list_messages)
83
+ ```
84
+
85
+ **Key implementation details:**
86
+ - Each clone advertises its own `agent_name` (e.g., `OriginalName[instance_number]`).
87
+ - MCP progress events originate from the clone's aggregator, so tool activity always shows under the suffixed name.
88
+ - Parent status lines remain visible for context while children run.
89
+
90
+ **As each instance completes:**
91
+ - We emit `ProgressAction.FINISHED` with elapsed time, keeping the line in the panel for auditability.
92
+ - Other instances continue showing their independent progress until they also finish.
93
+
94
+ **After all parallel executions complete:**
95
+ - Finished instance lines remain until the parent agent moves on, giving a full record of what ran.
96
+ - Parent and child template names stay untouched because clones carry the suffixed identity.
97
+
98
+ - **Instance line visibility**: We now leave finished instance lines visible (marked `FINISHED`)
99
+ instead of hiding them immediately, preserving a full audit trail of parallel runs.
100
+ - **Chat log separation**: Each parallel instance gets its own tool request/result headers
101
+ with instance numbers [1], [2], etc. for traceability.
102
+
103
+ Stats and Usage Semantics
104
+ -------------------------
105
+ - Each detached clone accrues usage on its own `UsageAccumulator`; after shutdown we
106
+ call `child.merge_usage_from(clone)` so template agents retain consolidated totals.
107
+ - Runtime events (logs, MCP progress, chat headers) use the suffixed clone names,
108
+ ensuring per-instance traceability even though usage rolls up to the template.
109
+ - The CLI *Usage Summary* table still reports one row per template agent
110
+ (for example, `PM-1-DayStatusSummarizer`), not per `[i]` instance; clones are
111
+ runtime-only and do not appear as separate agents in that table.
112
+
113
+ **Chat log display:**
114
+ Tool headers show instance numbers for clarity:
115
+ ```
116
+ ▎▶ orchestrator [tool request - agent__PM-1-DayStatusSummarizer[1]]
117
+ ▎◀ orchestrator [tool result - agent__PM-1-DayStatusSummarizer[1]]
118
+ ▎▶ orchestrator [tool request - agent__PM-1-DayStatusSummarizer[2]]
119
+ ▎◀ orchestrator [tool result - agent__PM-1-DayStatusSummarizer[2]]
120
+ ```
121
+
122
+ Bottom status bar shows all instances:
123
+ ```
124
+ | agent__PM-1-DayStatusSummarizer[1] · running | agent__PM-1-DayStatusSummarizer[2] · running |
125
+ ```
126
+
127
+ Implementation Notes
128
+ --------------------
129
+ - **Instance naming**: `run_tools` computes `instance_name = f"{child.name}[i]"` inside the
130
+ per-call wrapper and passes it into `spawn_detached_instance`, so the template child object
131
+ keeps its original name while each detached clone owns the suffixed identity.
132
+ - **Progress event routing**: Because each clone's `MCPAggregator` is constructed with the
133
+ suffixed `agent_name`, all MCP/tool progress events naturally use
134
+ `PM-1-DayStatusSummarizer[i]` without mutating base agent fields or using `ContextVar` hacks.
135
+ - **Display suppression with reference counting**: Multiple parallel instances of the same
136
+ child agent share a single agent object. Use reference counting to track active instances:
137
+ - `_display_suppression_count[child_id]`: Count of active parallel instances
138
+ - `_original_display_configs[child_id]`: Stored original config
139
+ - Only modify display config when first instance starts (count 0→1)
140
+ - Only restore display config when last instance completes (count 1→0)
141
+ - Prevents race condition where early-finishing instances restore config while others run
142
+ - **Child agent(s)**
143
+ - Existing agents (typically `McpAgent`-based) with their own MCP servers, skills, tools, etc.
144
+ - Serve as **templates**; `run_tools` now clones them before every tool call via
145
+ `spawn_detached_instance`, so runtime work happens inside short-lived replicas.
146
+
147
+ - **Detached instances**
148
+ - Each tool call gets an actual cloned agent with suffixed name `Child[i]`.
149
+ - Clones own their MCP aggregator/LLM stacks and merge usage back into the template after shutdown.
150
+ - **Chat log separation**: Each parallel instance gets its own tool request/result headers
151
+ with instance numbers [1], [2], etc. for traceability
152
+
153
+ Usage Example
154
+ -------------
155
+ ```python
156
+ from fast_agent import FastAgent
157
+
158
+ fast = FastAgent("parent")
159
+
160
+ # Define child agents
161
+ @fast.agent(name="researcher", instruction="Research topics")
162
+ async def researcher(): pass
163
+
164
+ @fast.agent(name="writer", instruction="Write content")
165
+ async def writer(): pass
166
+
167
+ # Define parent with agents-as-tools
168
+ @fast.agent(
169
+ name="coordinator",
170
+ instruction="Coordinate research and writing",
171
+ agents=["researcher", "writer"], # Exposes children as tools
172
+ )
173
+ async def coordinator(): pass
174
+ ```
175
+
176
+ The parent LLM can now naturally call researcher and writer as tools.
177
+
178
+ References
179
+ ----------
180
+ - Design doc: ``agetns_as_tools_plan_scratch.md`` (repo root).
181
+ - Docs: [`evalstate/fast-agent-docs`](https://github.com/evalstate/fast-agent-docs) (Agents-as-Tools section).
182
+ - OpenAI Agents SDK: <https://openai.github.io/openai-agents-python/tools>
183
+ - GitHub Issue: [#458](https://github.com/evalstate/fast-agent/issues/458)
184
+ """
185
+
186
+ from __future__ import annotations
187
+
188
+ import asyncio
189
+ import json
190
+ from contextlib import contextmanager, nullcontext
191
+ from copy import copy
192
+ from dataclasses import dataclass
193
+ from enum import Enum
194
+ from typing import TYPE_CHECKING, Any
195
+
196
+ from mcp import ListToolsResult, Tool
197
+ from mcp.types import CallToolResult
198
+
199
+ from fast_agent.agents.mcp_agent import McpAgent
200
+ from fast_agent.constants import FAST_AGENT_ERROR_CHANNEL
201
+ from fast_agent.core.logging.logger import get_logger
202
+ from fast_agent.core.prompt import Prompt
203
+ from fast_agent.mcp.helpers.content_helpers import get_text, text_content
204
+ from fast_agent.types import PromptMessageExtended
205
+
206
+ if TYPE_CHECKING:
207
+ from fast_agent.agents.agent_types import AgentConfig
208
+ from fast_agent.agents.llm_agent import LlmAgent
209
+
210
+ logger = get_logger(__name__)
211
+
212
+
213
+ class HistoryMode(str, Enum):
214
+ """History handling for detached child instances."""
215
+
216
+ SCRATCH = "scratch"
217
+ FORK = "fork"
218
+ FORK_AND_MERGE = "fork_and_merge"
219
+
220
+ @classmethod
221
+ def from_input(cls, value: Any | None) -> HistoryMode:
222
+ if value is None:
223
+ return cls.FORK
224
+ if isinstance(value, cls):
225
+ return value
226
+ try:
227
+ return cls(str(value))
228
+ except Exception:
229
+ return cls.FORK
230
+
231
+
232
+ @dataclass(kw_only=True)
233
+ class AgentsAsToolsOptions:
234
+ """Configuration knobs for the Agents-as-Tools wrapper.
235
+
236
+ Defaults:
237
+ - history_mode: fork child history (no merge back)
238
+ - max_parallel: None (no cap; caller may set an explicit limit)
239
+ - child_timeout_sec: None (no per-child timeout)
240
+ - max_display_instances: 20 (show first N lines, collapse the rest)
241
+ """
242
+
243
+ history_mode: HistoryMode = HistoryMode.FORK
244
+ max_parallel: int | None = None
245
+ child_timeout_sec: int | None = None
246
+ max_display_instances: int = 20
247
+
248
+ def __post_init__(self) -> None:
249
+ self.history_mode = HistoryMode.from_input(self.history_mode)
250
+ if self.max_parallel is not None and self.max_parallel <= 0:
251
+ raise ValueError("max_parallel must be > 0 when set")
252
+ if self.max_display_instances is not None and self.max_display_instances <= 0:
253
+ raise ValueError("max_display_instances must be > 0")
254
+ if self.child_timeout_sec is not None and self.child_timeout_sec <= 0:
255
+ raise ValueError("child_timeout_sec must be > 0 when set")
256
+
257
+
258
+ class AgentsAsToolsAgent(McpAgent):
259
+ """MCP-enabled agent that exposes child agents as additional tools.
260
+
261
+ This hybrid agent:
262
+
263
+ - Inherits all MCP behavior from :class:`McpAgent` (servers, MCP tool discovery, local tools).
264
+ - Exposes each child agent as an additional synthetic tool (`agent__ChildName`).
265
+ - Merges **MCP tools** and **agent-tools** into a single `list_tools()` surface.
266
+ - Routes `call_tool()` to child agents when the name matches a child, otherwise delegates
267
+ to the base `McpAgent.call_tool` implementation.
268
+ - Overrides `run_tools()` to fan out child-agent tools in parallel using detached clones,
269
+ while delegating any remaining MCP/local tools to the base `McpAgent.run_tools` and
270
+ merging all results into a single tool-loop response.
271
+ """
272
+
273
+ def __init__(
274
+ self,
275
+ config: AgentConfig,
276
+ agents: list[LlmAgent],
277
+ options: AgentsAsToolsOptions | None = None,
278
+ context: Any | None = None,
279
+ **kwargs: Any,
280
+ ) -> None:
281
+ """Initialize AgentsAsToolsAgent.
282
+
283
+ Args:
284
+ config: Agent configuration for this parent agent (including MCP servers/tools)
285
+ agents: List of child agents to expose as tools
286
+ context: Optional context for agent execution
287
+ **kwargs: Additional arguments passed through to :class:`McpAgent` and its bases
288
+ """
289
+ super().__init__(config=config, context=context, **kwargs)
290
+ self._options = options or AgentsAsToolsOptions()
291
+ self._child_agents: dict[str, LlmAgent] = {}
292
+ self._history_merge_lock = asyncio.Lock()
293
+ self._display_suppression_count: dict[int, int] = {}
294
+ self._original_display_configs: dict[int, Any] = {}
295
+
296
+ for child in agents:
297
+ tool_name = self._make_tool_name(child.name)
298
+ if tool_name in self._child_agents:
299
+ logger.warning(
300
+ f"Duplicate tool name '{tool_name}' for child agent '{child.name}', overwriting"
301
+ )
302
+ self._child_agents[tool_name] = child
303
+
304
+ def _make_tool_name(self, child_name: str) -> str:
305
+ """Generate a tool name for a child agent.
306
+
307
+ Args:
308
+ child_name: Name of the child agent
309
+
310
+ Returns:
311
+ Prefixed tool name to avoid collisions with MCP tools
312
+ """
313
+ return f"agent__{child_name}"
314
+
315
+ async def initialize(self) -> None:
316
+ """Initialize this agent and all child agents."""
317
+ await super().initialize()
318
+ for agent in self._child_agents.values():
319
+ if not getattr(agent, "initialized", False):
320
+ await agent.initialize()
321
+
322
+ async def shutdown(self) -> None:
323
+ """Shutdown this agent and all child agents."""
324
+ await super().shutdown()
325
+ for agent in self._child_agents.values():
326
+ try:
327
+ await agent.shutdown()
328
+ except Exception as e:
329
+ logger.warning(f"Error shutting down child agent {agent.name}: {e}")
330
+
331
+ async def list_tools(self) -> ListToolsResult:
332
+ """List MCP tools plus child agents exposed as tools."""
333
+
334
+ base = await super().list_tools()
335
+ tools = list(base.tools)
336
+ existing_names = {tool.name for tool in tools}
337
+
338
+ for tool_name, agent in self._child_agents.items():
339
+ if tool_name in existing_names:
340
+ continue
341
+
342
+ input_schema: dict[str, Any] = {
343
+ "type": "object",
344
+ "properties": {
345
+ "text": {"type": "string", "description": "Plain text input"},
346
+ "json": {"type": "object", "description": "Arbitrary JSON payload"},
347
+ },
348
+ "additionalProperties": True,
349
+ }
350
+ tools.append(
351
+ Tool(
352
+ name=tool_name,
353
+ description=agent.instruction,
354
+ inputSchema=input_schema,
355
+ )
356
+ )
357
+ existing_names.add(tool_name)
358
+
359
+ return ListToolsResult(tools=tools)
360
+
361
+ @contextmanager
362
+ def _child_display_suppressed(self, child: LlmAgent):
363
+ """Context manager to hide child chat while keeping tool logs visible."""
364
+ child_id = id(child)
365
+ count = self._display_suppression_count.get(child_id, 0)
366
+ if count == 0:
367
+ if (
368
+ hasattr(child, "display")
369
+ and child.display
370
+ and getattr(child.display, "config", None)
371
+ ):
372
+ self._original_display_configs[child_id] = child.display.config
373
+ temp_config = copy(child.display.config)
374
+ if hasattr(temp_config, "logger"):
375
+ temp_logger = copy(temp_config.logger)
376
+ temp_logger.show_chat = False
377
+ temp_logger.show_tools = True
378
+ temp_config.logger = temp_logger
379
+ child.display.config = temp_config
380
+ self._display_suppression_count[child_id] = count + 1
381
+ try:
382
+ yield
383
+ finally:
384
+ self._display_suppression_count[child_id] -= 1
385
+ if self._display_suppression_count[child_id] <= 0:
386
+ del self._display_suppression_count[child_id]
387
+ original_config = self._original_display_configs.pop(child_id, None)
388
+ if original_config is not None and hasattr(child, "display") and child.display:
389
+ child.display.config = original_config
390
+
391
+ async def _merge_child_history(
392
+ self, target: LlmAgent, clone: LlmAgent, start_index: int
393
+ ) -> None:
394
+ """Append clone history from start_index into target with a global merge lock."""
395
+ async with self._history_merge_lock:
396
+ new_messages = clone.message_history[start_index:]
397
+ target.append_history(new_messages)
398
+
399
+ async def _invoke_child_agent(
400
+ self,
401
+ child: LlmAgent,
402
+ arguments: dict[str, Any] | None = None,
403
+ *,
404
+ suppress_display: bool = True,
405
+ ) -> CallToolResult:
406
+ """Shared helper to execute a child agent with standard serialization and display rules."""
407
+
408
+ args = arguments or {}
409
+ # Serialize arguments to text input
410
+ if isinstance(args.get("text"), str):
411
+ input_text = args["text"]
412
+ elif "json" in args:
413
+ input_text = (
414
+ json.dumps(args["json"], ensure_ascii=False)
415
+ if isinstance(args["json"], dict)
416
+ else str(args["json"])
417
+ )
418
+ else:
419
+ input_text = json.dumps(args, ensure_ascii=False) if args else ""
420
+
421
+ child_request = Prompt.user(input_text)
422
+
423
+ try:
424
+ with self._child_display_suppressed(child) if suppress_display else nullcontext():
425
+ response: PromptMessageExtended = await child.generate([child_request], None)
426
+ content_blocks = list(response.content or [])
427
+
428
+ error_blocks = None
429
+ if response.channels and FAST_AGENT_ERROR_CHANNEL in response.channels:
430
+ error_blocks = response.channels.get(FAST_AGENT_ERROR_CHANNEL) or []
431
+ if error_blocks:
432
+ content_blocks.extend(error_blocks)
433
+
434
+ return CallToolResult(
435
+ content=content_blocks,
436
+ isError=bool(error_blocks),
437
+ )
438
+ except Exception as exc:
439
+ import traceback
440
+
441
+ logger.error(
442
+ "Child agent tool call failed",
443
+ data={
444
+ "agent_name": child.name,
445
+ "error": str(exc),
446
+ "error_type": type(exc).__name__,
447
+ "traceback": traceback.format_exc(),
448
+ },
449
+ )
450
+ return CallToolResult(content=[text_content(f"Error: {exc}")], isError=True)
451
+
452
+ def _resolve_child_agent(self, name: str) -> LlmAgent | None:
453
+ return self._child_agents.get(name) or self._child_agents.get(self._make_tool_name(name))
454
+
455
+ async def call_tool(
456
+ self,
457
+ name: str,
458
+ arguments: dict[str, Any] | None = None,
459
+ tool_use_id: str | None = None,
460
+ ) -> CallToolResult:
461
+ """Route tool execution to child agents first, then MCP/local tools.
462
+
463
+ The signature matches :meth:`McpAgent.call_tool` so that upstream tooling
464
+ can safely pass the LLM's ``tool_use_id`` as a positional argument.
465
+ """
466
+
467
+ child = self._resolve_child_agent(name)
468
+ if child is not None:
469
+ # Child agents don't currently use tool_use_id, they operate via
470
+ # a plain PromptMessageExtended tool call.
471
+ return await self._invoke_child_agent(child, arguments)
472
+
473
+ return await super().call_tool(name, arguments, tool_use_id)
474
+
475
+ def _show_parallel_tool_calls(self, descriptors: list[dict[str, Any]]) -> None:
476
+ """Display tool call headers for parallel agent execution.
477
+
478
+ Args:
479
+ descriptors: List of tool call descriptors with metadata
480
+ """
481
+ if not descriptors:
482
+ return
483
+
484
+ status_labels = {
485
+ "pending": "running",
486
+ "error": "error",
487
+ "missing": "missing",
488
+ }
489
+
490
+ total = len(descriptors)
491
+ limit = self._options.max_display_instances or total
492
+
493
+ # Show detailed call information for each agent
494
+ for i, desc in enumerate(descriptors[:limit], 1):
495
+ tool_name = desc.get("tool", "(unknown)")
496
+ corr_id = desc.get("id")
497
+ args = desc.get("args", {})
498
+ status = desc.get("status", "pending")
499
+
500
+ if status == "error":
501
+ continue # Skip display for error tools, will show in results
502
+
503
+ # Always add individual instance number for clarity
504
+ suffix = f"[{i}]"
505
+ if corr_id:
506
+ suffix = f"[{i}|{corr_id}]"
507
+ display_tool_name = f"{tool_name}{suffix}"
508
+
509
+ # Build bottom item for THIS instance only (not all instances)
510
+ status_label = status_labels.get(status, "pending")
511
+ bottom_item = f"{display_tool_name} · {status_label}"
512
+
513
+ # Show individual tool call with arguments
514
+ self.display.show_tool_call(
515
+ name=self.name,
516
+ tool_name=display_tool_name,
517
+ tool_args=args,
518
+ bottom_items=[bottom_item], # Only this instance's label
519
+ max_item_length=28,
520
+ metadata={"correlation_id": corr_id, "instance_name": display_tool_name},
521
+ type_label="subagent",
522
+ )
523
+ if total > limit:
524
+ collapsed = total - limit
525
+ label = f"[{limit + 1}..{total}]"
526
+ self.display.show_tool_call(
527
+ name=self.name,
528
+ tool_name=label,
529
+ tool_args={"collapsed": collapsed},
530
+ bottom_items=[f"{label} · {collapsed} more"],
531
+ max_item_length=28,
532
+ type_label="subagent",
533
+ )
534
+
535
+ def _show_parallel_tool_results(self, records: list[dict[str, Any]]) -> None:
536
+ """Display tool result panels for parallel agent execution.
537
+
538
+ Args:
539
+ records: List of result records with descriptor and result data
540
+ """
541
+ if not records:
542
+ return
543
+
544
+ total = len(records)
545
+ limit = self._options.max_display_instances or total
546
+
547
+ # Show detailed result for each agent
548
+ for i, record in enumerate(records[:limit], 1):
549
+ descriptor = record.get("descriptor", {})
550
+ result = record.get("result")
551
+ tool_name = descriptor.get("tool", "(unknown)")
552
+ corr_id = descriptor.get("id")
553
+
554
+ if result:
555
+ # Always add individual instance number for clarity
556
+ suffix = f"[{i}]"
557
+ if corr_id:
558
+ suffix = f"[{i}|{corr_id}]"
559
+ display_tool_name = f"{tool_name}{suffix}"
560
+
561
+ # Show individual tool result with full content
562
+ self.display.show_tool_result(
563
+ name=self.name,
564
+ tool_name=display_tool_name,
565
+ type_label="subagent response",
566
+ result=result,
567
+ )
568
+ if total > limit:
569
+ collapsed = total - limit
570
+ label = f"[{limit + 1}..{total}]"
571
+ self.display.show_tool_result(
572
+ name=self.name,
573
+ tool_name=label,
574
+ type_label="subagent response",
575
+ result=CallToolResult(
576
+ content=[text_content(f"{collapsed} more results (collapsed)")],
577
+ isError=False,
578
+ ),
579
+ )
580
+
581
+ async def run_tools(self, request: PromptMessageExtended) -> PromptMessageExtended:
582
+ """Handle mixed MCP + agent-tool batches."""
583
+
584
+ if not request.tool_calls:
585
+ logger.warning("No tool calls found in request", data=request)
586
+ return PromptMessageExtended(role="user", tool_results={})
587
+
588
+ child_ids: list[str] = []
589
+ for correlation_id, tool_request in request.tool_calls.items():
590
+ if self._resolve_child_agent(tool_request.params.name):
591
+ child_ids.append(correlation_id)
592
+
593
+ if not child_ids:
594
+ return await super().run_tools(request)
595
+
596
+ child_results, child_error = await self._run_child_tools(request, set(child_ids))
597
+
598
+ if len(child_ids) == len(request.tool_calls):
599
+ return self._finalize_tool_results(child_results, tool_loop_error=child_error)
600
+
601
+ # Execute remaining MCP/local tools via base implementation
602
+ remaining_ids = [cid for cid in request.tool_calls.keys() if cid not in child_ids]
603
+ mcp_request = PromptMessageExtended(
604
+ role=request.role,
605
+ content=request.content,
606
+ tool_calls={cid: request.tool_calls[cid] for cid in remaining_ids},
607
+ )
608
+ mcp_message = await super().run_tools(mcp_request)
609
+ mcp_results = mcp_message.tool_results or {}
610
+ mcp_error = self._extract_error_text(mcp_message)
611
+
612
+ combined_results = {}
613
+ combined_results.update(child_results)
614
+ combined_results.update(mcp_results)
615
+
616
+ tool_loop_error = child_error or mcp_error
617
+ return self._finalize_tool_results(combined_results, tool_loop_error=tool_loop_error)
618
+
619
+ async def _run_child_tools(
620
+ self,
621
+ request: PromptMessageExtended,
622
+ target_ids: set[str],
623
+ ) -> tuple[dict[str, CallToolResult], str | None]:
624
+ """Run only the child-agent tool calls from the request."""
625
+
626
+ if not target_ids:
627
+ return {}, None
628
+
629
+ tool_results: dict[str, CallToolResult] = {}
630
+ tool_loop_error: str | None = None
631
+
632
+ try:
633
+ listed = await self.list_tools()
634
+ available_tools = {t.name for t in listed.tools}
635
+ except Exception as exc:
636
+ logger.warning(f"Failed to list tools before execution: {exc}")
637
+ available_tools = set(self._child_agents.keys())
638
+
639
+ call_descriptors: list[dict[str, Any]] = []
640
+ descriptor_by_id: dict[str, dict[str, Any]] = {}
641
+ tasks: list[asyncio.Task] = []
642
+ id_list: list[str] = []
643
+
644
+ for correlation_id, tool_request in request.tool_calls.items():
645
+ if correlation_id not in target_ids:
646
+ continue
647
+
648
+ tool_name = tool_request.params.name
649
+ tool_args = tool_request.params.arguments or {}
650
+
651
+ descriptor = {
652
+ "id": correlation_id,
653
+ "tool": tool_name,
654
+ "args": tool_args,
655
+ }
656
+ call_descriptors.append(descriptor)
657
+ descriptor_by_id[correlation_id] = descriptor
658
+
659
+ if (
660
+ tool_name not in available_tools
661
+ and self._make_tool_name(tool_name) not in available_tools
662
+ ):
663
+ error_message = f"Tool '{tool_name}' is not available"
664
+ tool_results[correlation_id] = CallToolResult(
665
+ content=[text_content(error_message)], isError=True
666
+ )
667
+ tool_loop_error = tool_loop_error or error_message
668
+ descriptor["status"] = "error"
669
+ continue
670
+
671
+ descriptor["status"] = "pending"
672
+ id_list.append(correlation_id)
673
+
674
+ max_parallel = self._options.max_parallel
675
+ if max_parallel and len(id_list) > max_parallel:
676
+ skipped_ids = id_list[max_parallel:]
677
+ id_list = id_list[:max_parallel]
678
+ skip_msg = f"Skipped {len(skipped_ids)} agent-tool calls (max_parallel={max_parallel})"
679
+ tool_loop_error = tool_loop_error or skip_msg
680
+ for cid in skipped_ids:
681
+ tool_results[cid] = CallToolResult(
682
+ content=[text_content(skip_msg)],
683
+ isError=True,
684
+ )
685
+ descriptor_by_id[cid]["status"] = "error"
686
+ descriptor_by_id[cid]["error_message"] = skip_msg
687
+
688
+ from fast_agent.event_progress import ProgressAction, ProgressEvent
689
+ from fast_agent.ui.progress_display import (
690
+ progress_display as outer_progress_display,
691
+ )
692
+
693
+ async def call_with_instance_name(
694
+ tool_name: str, tool_args: dict[str, Any], instance: int, correlation_id: str
695
+ ) -> CallToolResult:
696
+ child = self._resolve_child_agent(tool_name)
697
+ if not child:
698
+ error_msg = f"Unknown agent-tool: {tool_name}"
699
+ return CallToolResult(content=[text_content(error_msg)], isError=True)
700
+
701
+ base_name = getattr(child, "_name", child.name)
702
+ instance_name = f"{base_name}[{instance}]"
703
+
704
+ try:
705
+ clone = await child.spawn_detached_instance(name=instance_name)
706
+ except Exception as exc:
707
+ logger.error(
708
+ "Failed to spawn dedicated child instance",
709
+ data={
710
+ "tool_name": tool_name,
711
+ "agent_name": base_name,
712
+ "error": str(exc),
713
+ },
714
+ )
715
+ return CallToolResult(content=[text_content(f"Spawn failed: {exc}")], isError=True)
716
+
717
+ # Prepare history according to mode
718
+ history_mode = self._options.history_mode
719
+ base_history = child.message_history
720
+ fork_index = len(base_history)
721
+ try:
722
+ if history_mode == HistoryMode.SCRATCH:
723
+ clone.load_message_history([])
724
+ fork_index = 0
725
+ else:
726
+ clone.load_message_history(base_history)
727
+ except Exception as hist_exc:
728
+ logger.warning(
729
+ "Failed to load history into clone",
730
+ data={"instance_name": instance_name, "error": str(hist_exc)},
731
+ )
732
+
733
+ progress_started = False
734
+ try:
735
+ outer_progress_display.update(
736
+ ProgressEvent(
737
+ action=ProgressAction.CHATTING,
738
+ target=instance_name,
739
+ details="",
740
+ agent_name=instance_name,
741
+ correlation_id=correlation_id,
742
+ instance_name=instance_name,
743
+ tool_name=tool_name,
744
+ )
745
+ )
746
+ progress_started = True
747
+ call_coro = self._invoke_child_agent(clone, tool_args)
748
+ timeout = self._options.child_timeout_sec
749
+ if timeout:
750
+ return await asyncio.wait_for(call_coro, timeout=timeout)
751
+ return await call_coro
752
+ finally:
753
+ try:
754
+ await clone.shutdown()
755
+ except Exception as shutdown_exc:
756
+ logger.warning(
757
+ "Error shutting down dedicated child instance",
758
+ data={
759
+ "instance_name": instance_name,
760
+ "error": str(shutdown_exc),
761
+ },
762
+ )
763
+ try:
764
+ child.merge_usage_from(clone)
765
+ except Exception as merge_exc:
766
+ logger.warning(
767
+ "Failed to merge usage from child instance",
768
+ data={
769
+ "instance_name": instance_name,
770
+ "error": str(merge_exc),
771
+ },
772
+ )
773
+ if history_mode == HistoryMode.FORK_AND_MERGE:
774
+ try:
775
+ await self._merge_child_history(
776
+ target=child, clone=clone, start_index=fork_index
777
+ )
778
+ except Exception as merge_hist_exc:
779
+ logger.warning(
780
+ "Failed to merge child history",
781
+ data={
782
+ "instance_name": instance_name,
783
+ "error": str(merge_hist_exc),
784
+ },
785
+ )
786
+ if progress_started and instance_name:
787
+ outer_progress_display.update(
788
+ ProgressEvent(
789
+ action=ProgressAction.FINISHED,
790
+ target=instance_name,
791
+ details="Completed",
792
+ agent_name=instance_name,
793
+ correlation_id=correlation_id,
794
+ instance_name=instance_name,
795
+ tool_name=tool_name,
796
+ )
797
+ )
798
+
799
+ for i, cid in enumerate(id_list, 1):
800
+ tool_name = descriptor_by_id[cid]["tool"]
801
+ tool_args = descriptor_by_id[cid]["args"]
802
+ tasks.append(asyncio.create_task(call_with_instance_name(tool_name, tool_args, i, cid)))
803
+
804
+ self._show_parallel_tool_calls(call_descriptors)
805
+
806
+ if tasks:
807
+ results = await asyncio.gather(*tasks, return_exceptions=True)
808
+ for i, result in enumerate(results):
809
+ correlation_id = id_list[i]
810
+ if isinstance(result, Exception):
811
+ msg = f"Tool execution failed: {result}"
812
+ tool_results[correlation_id] = CallToolResult(
813
+ content=[text_content(msg)], isError=True
814
+ )
815
+ tool_loop_error = tool_loop_error or msg
816
+ descriptor_by_id[correlation_id]["status"] = "error"
817
+ descriptor_by_id[correlation_id]["error_message"] = msg
818
+ else:
819
+ tool_results[correlation_id] = result
820
+ descriptor_by_id[correlation_id]["status"] = (
821
+ "error" if result.isError else "done"
822
+ )
823
+
824
+ ordered_records: list[dict[str, Any]] = []
825
+ for cid in id_list:
826
+ result = tool_results.get(cid)
827
+ if result is None:
828
+ continue
829
+ descriptor = descriptor_by_id.get(cid, {})
830
+ ordered_records.append({"descriptor": descriptor, "result": result})
831
+
832
+ self._show_parallel_tool_results(ordered_records)
833
+
834
+ return tool_results, tool_loop_error
835
+
836
+ def _extract_error_text(self, message: PromptMessageExtended) -> str | None:
837
+ if not message.channels:
838
+ return None
839
+
840
+ error_blocks = message.channels.get(FAST_AGENT_ERROR_CHANNEL)
841
+ if not error_blocks:
842
+ return None
843
+
844
+ for block in error_blocks:
845
+ text = get_text(block)
846
+ if text:
847
+ return text
848
+
849
+ return None