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,652 @@
1
+ """
2
+ Iterative Planner Agent - works towards an objective using sub-agents
3
+ """
4
+
5
+ import asyncio
6
+ from typing import Any, Dict, List, Optional, Tuple, Type
7
+
8
+ from mcp import Tool
9
+ from mcp.types import TextContent
10
+
11
+ from fast_agent.agents.agent_types import AgentConfig, AgentType
12
+ from fast_agent.agents.llm_agent import LlmAgent
13
+ from fast_agent.agents.workflow.orchestrator_models import (
14
+ Plan,
15
+ PlanningStep,
16
+ PlanResult,
17
+ Step,
18
+ TaskWithResult,
19
+ format_plan_result,
20
+ format_step_result_text,
21
+ )
22
+ from fast_agent.core.exceptions import AgentConfigError
23
+ from fast_agent.core.logging.logger import get_logger
24
+ from fast_agent.core.prompt import Prompt
25
+ from fast_agent.interfaces import AgentProtocol, ModelT
26
+ from fast_agent.types import PromptMessageExtended, RequestParams
27
+ from fast_agent.workflow_telemetry import (
28
+ NoOpPlanTelemetryProvider,
29
+ PlanEntry,
30
+ PlanEntryStatus,
31
+ PlanTelemetryProvider,
32
+ )
33
+
34
+ logger = get_logger(__name__)
35
+
36
+
37
+ ITERATIVE_PLAN_SYSTEM_PROMPT_TEMPLATE = """
38
+ You are an expert planner, able to Orchestrate complex tasks by breaking them down in to
39
+ manageable steps, and delegating tasks to Agents.
40
+
41
+ You work iteratively - given an Objective, you consider the current state of the plan,
42
+ decide the next step towards the goal. You document those steps and create clear instructions
43
+ for execution by the Agents, being specific about what you need to know to assess task completion.
44
+
45
+ NOTE: A 'Planning Step' has a description, and a list of tasks that can be delegated
46
+ and executed in parallel.
47
+
48
+ Agents have a 'description' describing their primary function, and a set of 'skills' that
49
+ represent Tools they can use in completing their function.
50
+
51
+ The following Agents are available to you:
52
+
53
+ {{agents}}
54
+
55
+ You must specify the Agent name precisely when generating a Planning Step.
56
+
57
+ """
58
+
59
+
60
+ ITERATIVE_PLAN_PROMPT_TEMPLATE2 = """
61
+
62
+ ```
63
+ <fastagent:data>
64
+ <fastagent:progress>
65
+ {plan_result}
66
+ </fastagent:progress>
67
+
68
+ <fastagent:status>
69
+ {plan_status}
70
+ {iterations_info}
71
+ </fastagent:status>
72
+ </fastagent:data>
73
+ ```
74
+
75
+ The overall objective is:
76
+
77
+ ```
78
+ <fastagent:objective>
79
+ {objective}
80
+ </fastagent:objective>
81
+ ```
82
+
83
+ Produce the next step in the plan to complete the Objective.
84
+
85
+ Consider the previous steps and results, and decide what needs to be done next.
86
+
87
+ Set "is_complete" to true when ANY of these conditions are met:
88
+ 1. The objective has been achieved in full or substantively
89
+ 2. The remaining work is minor or trivial compared to what's been accomplished
90
+ 3. The plan has gathered sufficient information to answer the original request
91
+ 4. The plan has no feasible way of completing the objective.
92
+
93
+ Only set is_complete to `true` if there are no outstanding tasks.
94
+
95
+ Be decisive - avoid excessive planning steps that add little value. It's better to complete a plan early than to continue with marginal improvements.
96
+
97
+ Focus on the meeting the core intent of the objective.
98
+
99
+ """
100
+
101
+
102
+ DELEGATED_TASK_TEMPLATE = """
103
+ You are a component in an orchestrated workflow to achieve an objective.
104
+
105
+ The overall objective is:
106
+
107
+ ```
108
+ <fastagent:objective>
109
+ {objective}
110
+ </fastagent:objective>
111
+ ```
112
+
113
+ Previous context in achieving the objective is below:
114
+
115
+ ```
116
+ <fastagent:context>
117
+ {context}
118
+ </fastagent:context>
119
+ ```
120
+
121
+ Your job is to accomplish the "task" specified in `<fastagent:task>`. The overall objective and
122
+ previous context is supplied to inform your approach.
123
+
124
+ ```
125
+ <fastagent:task>
126
+ {task}
127
+ </fastagent:task>
128
+ ```
129
+
130
+ Provide a direct, concise response on completion that makes it simple to assess the status of
131
+ the overall plan.
132
+ """
133
+
134
+
135
+ PLAN_RESULT_TEMPLATE = """
136
+ The below shows the results of running a plan to meet the specified objective.
137
+
138
+ ```
139
+ <fastagent:plan-results>
140
+ {plan_result}
141
+ </fastagent:plan-results>
142
+ ```
143
+
144
+ The plan was stopped because {termination_reason}.
145
+
146
+ Provide a summary of the tasks completed and their outcomes to complete the Objective.
147
+ Use markdown formatting.
148
+
149
+ If the plan was marked as incomplete but the maximum number of iterations was reached,
150
+ make sure to state clearly what was accomplished and what remains to be done.
151
+
152
+
153
+ Complete the plan by providing an appropriate answer for the original objective. Provide a Mermaid diagram
154
+ (in code fences) showing the plan steps and their relationships, if applicable. Do not use parentheses in labels.
155
+ """
156
+
157
+
158
+ class IterativePlanner(LlmAgent):
159
+ """
160
+ An agent that implements the orchestrator workflow pattern.
161
+
162
+ Dynamically creates execution plans and delegates tasks
163
+ to specialized worker agents, synthesizing their results into a cohesive output.
164
+ Supports both full planning and iterative planning modes.
165
+ """
166
+
167
+ @property
168
+ def agent_type(self) -> AgentType:
169
+ """Return the type of this agent."""
170
+ return AgentType.ITERATIVE_PLANNER
171
+
172
+ def __init__(
173
+ self,
174
+ config: AgentConfig,
175
+ agents: List[AgentProtocol],
176
+ plan_iterations: int = -1,
177
+ context: Optional[Any] = None,
178
+ **kwargs,
179
+ ) -> None:
180
+ """
181
+ Initialize an OrchestratorAgent.
182
+
183
+ Args:
184
+ config: Agent configuration or name
185
+ agents: List of specialized worker agents available for task execution
186
+ plan_type: Planning mode ("full" or "iterative")
187
+ context: Optional context object
188
+ **kwargs: Additional keyword arguments to pass to BaseAgent
189
+ """
190
+ if not agents:
191
+ raise AgentConfigError("At least one worker agent must be provided")
192
+
193
+ # Store agents by name for easier lookup
194
+ self.agents: Dict[str, AgentProtocol] = {}
195
+ for agent in agents:
196
+ agent_name = agent.name
197
+ self.agents[agent_name] = agent
198
+
199
+ # Extract plan_type from kwargs before passing to parent
200
+ kwargs.pop("plan_type", "full")
201
+ super().__init__(config, context=context, **kwargs)
202
+
203
+ self.plan_iterations = plan_iterations
204
+ self._plan_telemetry_provider: PlanTelemetryProvider = NoOpPlanTelemetryProvider()
205
+
206
+ @property
207
+ def plan_telemetry(self) -> PlanTelemetryProvider:
208
+ """Telemetry provider for emitting plan updates."""
209
+ return self._plan_telemetry_provider
210
+
211
+ @plan_telemetry.setter
212
+ def plan_telemetry(self, provider: PlanTelemetryProvider | None) -> None:
213
+ if provider is None:
214
+ provider = NoOpPlanTelemetryProvider()
215
+ self._plan_telemetry_provider = provider
216
+
217
+ async def initialize(self) -> None:
218
+ """Initialize the orchestrator agent and worker agents."""
219
+ # Initialize all worker agents first if not already initialized
220
+ for agent_name, agent in self.agents.items():
221
+ if not getattr(agent, "initialized", False):
222
+ logger.debug(f"Initializing agent: {agent_name}")
223
+ await agent.initialize()
224
+
225
+ # Format agent information using agent cards with XML formatting
226
+ agent_descriptions = []
227
+ for agent_name, agent in self.agents.items():
228
+ agent_card = await agent.agent_card()
229
+ # Format as XML for better readability in prompts
230
+ xml_formatted = self._format_agent_card_as_xml(agent_card)
231
+ agent_descriptions.append(xml_formatted)
232
+
233
+ agents_str = "\n".join(agent_descriptions)
234
+
235
+ # Replace {{agents}} placeholder in the system prompt template
236
+ system_prompt = self.config.instruction.replace("{{agents}}", agents_str)
237
+
238
+ # Update the config instruction with the formatted system prompt
239
+ self.instruction = system_prompt
240
+ # Initialize the base agent with the updated system prompt
241
+ await super().initialize()
242
+
243
+ self.initialized = True
244
+
245
+ async def shutdown(self) -> None:
246
+ """Shutdown the orchestrator agent and worker agents."""
247
+ await super().shutdown()
248
+
249
+ # Shutdown all worker agents
250
+ for agent_name, agent in self.agents.items():
251
+ try:
252
+ await agent.shutdown()
253
+ except Exception as e:
254
+ logger.warning(f"Error shutting down agent {agent_name}: {str(e)}")
255
+
256
+ async def generate_impl(
257
+ self,
258
+ messages: List[PromptMessageExtended],
259
+ request_params: RequestParams | None = None,
260
+ tools: List[Tool] | None = None,
261
+ ) -> PromptMessageExtended:
262
+ """
263
+ Execute an orchestrated plan to process the input.
264
+
265
+ Args:
266
+ normalized_messages: Already normalized list of PromptMessageExtended
267
+ request_params: Optional request parameters
268
+
269
+ Returns:
270
+ The final synthesized response from the orchestration
271
+ """
272
+ # Extract user request
273
+ objective = messages[-1].all_text() if messages else ""
274
+ plan_result = await self._execute_plan(objective, request_params)
275
+ # Return the result
276
+ return PromptMessageExtended(
277
+ role="assistant",
278
+ content=[TextContent(type="text", text=plan_result.result or "No result available")],
279
+ )
280
+
281
+ async def structured_impl(
282
+ self,
283
+ messages: List[PromptMessageExtended],
284
+ model: Type[ModelT],
285
+ request_params: Optional[RequestParams] = None,
286
+ ) -> Tuple[ModelT | None, PromptMessageExtended]:
287
+ """
288
+ Execute an orchestration plan and parse the result into a structured format.
289
+
290
+ Args:
291
+ messages: List of messages to process
292
+ model: Pydantic model to parse the response into
293
+ request_params: Optional request parameters
294
+
295
+ Returns:
296
+ The parsed final response, or None if parsing fails
297
+ """
298
+ # Generate orchestration result
299
+ response = await self.generate_impl(messages, request_params)
300
+
301
+ # Try to parse the response into the specified model
302
+ try:
303
+ result_text = response.last_text() or "<no text>"
304
+ prompt_message = PromptMessageExtended(
305
+ role="user", content=[TextContent(type="text", text=result_text)]
306
+ )
307
+ assert self._llm
308
+ return await self._llm.structured([prompt_message], model, request_params)
309
+ except Exception as e:
310
+ logger.warning(f"Failed to parse orchestration result: {str(e)}")
311
+ return None, Prompt.assistant(f"Failed to parse orchestration result: {str(e)}")
312
+
313
+ async def _execute_plan(
314
+ self, objective: str, request_params: RequestParams | None
315
+ ) -> PlanResult:
316
+ """
317
+ Execute a plan to achieve the given objective.
318
+
319
+ Args:
320
+ objective: The objective to achieve
321
+ request_params: Request parameters for execution
322
+
323
+ Returns:
324
+ PlanResult containing execution results and final output
325
+ """
326
+
327
+ objective_met: bool = False
328
+ terminate_plan: str | None = None
329
+ plan_result = PlanResult(objective=objective, step_results=[])
330
+
331
+ # Track plan entries for telemetry
332
+ plan_entries: list[PlanEntry] = []
333
+
334
+ while not objective_met and not terminate_plan:
335
+ next_step: PlanningStep | None = await self._get_next_step(
336
+ objective, plan_result, request_params
337
+ )
338
+
339
+ if None is next_step:
340
+ terminate_plan = "Failed to generate plan, terminating early"
341
+ logger.error("Failed to generate next step, terminating plan early")
342
+ break
343
+
344
+ assert next_step # lets keep the indenting manageable!
345
+
346
+ if next_step.is_complete:
347
+ objective_met = True
348
+ terminate_plan = "Plan completed successfully"
349
+ # Mark all entries as completed
350
+ for entry in plan_entries:
351
+ entry.status = "completed"
352
+ await self.plan_telemetry.update_plan(plan_entries)
353
+ break
354
+
355
+ plan = Plan(steps=[next_step], is_complete=next_step.is_complete)
356
+ invalid_agents = self._validate_agent_names(plan)
357
+ if invalid_agents:
358
+ logger.error(f"Plan contains invalid agent names: {', '.join(invalid_agents)}")
359
+ terminate_plan = (
360
+ f"Invalid agent names found ({', '.join(invalid_agents)}), terminating plan"
361
+ )
362
+ break
363
+
364
+ # Add subtasks as plan entries for telemetry so clients see actionable work
365
+ step_entries = self._build_plan_entries_for_step(next_step)
366
+ plan_entries.extend(step_entries)
367
+ await self.plan_telemetry.update_plan(plan_entries)
368
+
369
+ # Mark subtasks as in progress while they execute
370
+ self._set_plan_entries_status(step_entries, "in_progress")
371
+ await self.plan_telemetry.update_plan(plan_entries)
372
+
373
+ for step in plan.steps: # this will only be one for iterative (change later)
374
+ step_result = await self._execute_step(step, plan_result)
375
+ plan_result.add_step_result(step_result)
376
+
377
+ # Mark subtasks as completed
378
+ self._set_plan_entries_status(step_entries, "completed")
379
+ await self.plan_telemetry.update_plan(plan_entries)
380
+
381
+ # Store plan in result
382
+ plan_result.plan = plan
383
+
384
+ if self.plan_iterations > 0:
385
+ if len(plan_result.step_results) >= self.plan_iterations:
386
+ terminate_plan = f"Reached maximum number of iterations ({self.plan_iterations}), terminating plan"
387
+
388
+ if not terminate_plan:
389
+ terminate_plan = "Unknown termination reason"
390
+ result_prompt = PLAN_RESULT_TEMPLATE.format(
391
+ plan_result=format_plan_result(plan_result), termination_reason=terminate_plan
392
+ )
393
+
394
+ # Generate final synthesis
395
+ final_message = await self._planner_generate_str(result_prompt, request_params)
396
+ plan_result.result = final_message.last_text() or "No final message generated"
397
+ await self.show_assistant_message(final_message)
398
+ return plan_result
399
+
400
+ async def _execute_step(self, step: Step, previous_result: PlanResult) -> Any:
401
+ """
402
+ Execute a single step from the plan.
403
+
404
+ Args:
405
+ step: The step to execute
406
+ previous_result: Results of the plan execution so far
407
+ request_params: Request parameters
408
+
409
+ Returns:
410
+ Result of executing the step
411
+ """
412
+ from fast_agent.agents.workflow.orchestrator_models import StepResult
413
+
414
+ # Initialize step result
415
+ step_result = StepResult(step=step, task_results=[])
416
+
417
+ # Format context for tasks
418
+ context = format_plan_result(previous_result)
419
+
420
+ # Group tasks by agent and execute different agents in parallel
421
+ from collections import defaultdict
422
+
423
+ tasks_by_agent = defaultdict(list)
424
+ for task in step.tasks:
425
+ tasks_by_agent[task.agent].append(task)
426
+
427
+ async def execute_agent_tasks(agent_name: str, agent_tasks: List) -> List[TaskWithResult]:
428
+ """Execute all tasks for a single agent sequentially (preserves history)"""
429
+ agent = self.agents.get(agent_name)
430
+ assert agent is not None
431
+
432
+ results = []
433
+ for task in agent_tasks:
434
+ try:
435
+ task_description = DELEGATED_TASK_TEMPLATE.format(
436
+ objective=previous_result.objective, task=task.description, context=context
437
+ )
438
+ result = await agent.generate(
439
+ [
440
+ PromptMessageExtended(
441
+ role="user",
442
+ content=[TextContent(type="text", text=task_description)],
443
+ )
444
+ ]
445
+ )
446
+
447
+ task_model = task.model_dump()
448
+ results.append(
449
+ TaskWithResult(
450
+ description=task_model["description"],
451
+ agent=task_model["agent"],
452
+ result=result.last_text() or "<missing response>",
453
+ )
454
+ )
455
+ except Exception as e:
456
+ logger.error(f"Error executing task: {str(e)}")
457
+ task_model = task.model_dump()
458
+ results.append(
459
+ TaskWithResult(
460
+ description=task_model["description"],
461
+ agent=task_model["agent"],
462
+ result=f"ERROR: {str(e)}",
463
+ )
464
+ )
465
+ return results
466
+
467
+ # Execute different agents in parallel, tasks within each agent sequentially
468
+ agent_futures = [
469
+ execute_agent_tasks(agent_name, agent_tasks)
470
+ for agent_name, agent_tasks in tasks_by_agent.items()
471
+ ]
472
+
473
+ all_results = await asyncio.gather(*agent_futures)
474
+ task_results = [result for agent_results in all_results for result in agent_results]
475
+
476
+ # Add all task results to step result
477
+ for task_result in task_results:
478
+ step_result.add_task_result(task_result)
479
+
480
+ # Format step result
481
+ step_result.result = format_step_result_text(step_result)
482
+ return step_result
483
+
484
+ async def _get_next_step(
485
+ self, objective: str, plan_result: PlanResult, request_params: RequestParams | None
486
+ ) -> PlanningStep | None:
487
+ """
488
+ Generate just the next step for iterative planning.
489
+
490
+ Args:
491
+ objective: The objective to achieve
492
+ plan_result: Current plan execution state
493
+ request_params: Request parameters
494
+
495
+ Returns:
496
+ Next step to execute, or None if parsing fails
497
+ """
498
+
499
+ # Determine plan status
500
+ if plan_result.is_complete:
501
+ plan_status = "Plan Status: Complete"
502
+ elif plan_result.step_results:
503
+ plan_status = "Plan Status: In Progress"
504
+ else:
505
+ plan_status = "Plan Status: Not Started"
506
+
507
+ # Calculate iteration information
508
+
509
+ if self.plan_iterations > 0:
510
+ max_iterations = self.plan_iterations
511
+ current_iteration = len(plan_result.step_results)
512
+ iterations_remaining = max_iterations - current_iteration
513
+ iterations_info = (
514
+ f"Planning Budget: {iterations_remaining} of {max_iterations} iterations remaining"
515
+ )
516
+ else:
517
+ iterations_info = "Iterating until objective is met."
518
+
519
+ # Format the planning prompt
520
+ prompt = ITERATIVE_PLAN_PROMPT_TEMPLATE2.format(
521
+ objective=objective,
522
+ plan_result=format_plan_result(plan_result),
523
+ plan_status=plan_status,
524
+ iterations_info=iterations_info,
525
+ )
526
+
527
+ request_params = self._merge_request_params(
528
+ request_params, RequestParams(systemPrompt=self.instruction)
529
+ )
530
+
531
+ # Get structured response from LLM
532
+ try:
533
+ plan_msg = PromptMessageExtended(
534
+ role="user", content=[TextContent(type="text", text=prompt)]
535
+ )
536
+ assert self._llm
537
+ self.show_user_message(plan_msg)
538
+ next_step, raw_response = await self._llm.structured(
539
+ [plan_msg], PlanningStep, request_params
540
+ )
541
+ await self.show_assistant_message(raw_response)
542
+ return next_step
543
+ except Exception as e:
544
+ logger.error(f"Failed to parse next step: {str(e)}")
545
+ return None
546
+
547
+ def _validate_agent_names(self, plan: Plan) -> List[str]:
548
+ """
549
+ Validate all agent names in a plan before execution.
550
+
551
+ Args:
552
+ plan: The plan to validate
553
+ """
554
+ invalid_agents = []
555
+
556
+ for step in plan.steps:
557
+ for task in step.tasks:
558
+ if task.agent not in self.agents:
559
+ invalid_agents.append(task.agent)
560
+
561
+ return invalid_agents
562
+
563
+ @staticmethod
564
+ def _format_agent_card_as_xml(agent_card) -> str:
565
+ """
566
+ Format an agent card as XML for display in prompts.
567
+
568
+ This creates a structured XML representation that's more readable than JSON
569
+ and includes all relevant agent information in a hierarchical format.
570
+
571
+ Args:
572
+ agent_card: The AgentCard object
573
+
574
+ Returns:
575
+ XML formatted agent information
576
+ """
577
+ xml_parts = [f'<fastagent:agent name="{agent_card.name}">']
578
+
579
+ # Add description if available
580
+ if agent_card.description:
581
+ xml_parts.append(
582
+ f" <fastagent:description>{agent_card.description}</fastagent:description>"
583
+ )
584
+
585
+ # Add skills if available
586
+ if hasattr(agent_card, "skills") and agent_card.skills:
587
+ xml_parts.append(" <fastagent:skills>")
588
+ for skill in agent_card.skills:
589
+ xml_parts.append(f' <fastagent:skill name="{skill.name}">')
590
+ if hasattr(skill, "description") and skill.description:
591
+ xml_parts.append(
592
+ f" <fastagent:description>{skill.description}</fastagent:description>"
593
+ )
594
+ xml_parts.append(" </fastagent:skill>")
595
+ xml_parts.append(" </fastagent:skills>")
596
+
597
+ xml_parts.append("</fastagent:agent>")
598
+
599
+ return "\n".join(xml_parts)
600
+
601
+ async def _planner_generate_str(
602
+ self, message: str, request_params: RequestParams | None
603
+ ) -> PromptMessageExtended:
604
+ """
605
+ Generate string response from the orchestrator's own LLM.
606
+
607
+ Args:
608
+ message: Message to send to the LLM
609
+ request_params: Request parameters
610
+
611
+ Returns:
612
+ String response from the LLM
613
+ """
614
+ # Create prompt message
615
+ prompt = PromptMessageExtended(
616
+ role="user", content=[TextContent(type="text", text=message)]
617
+ )
618
+ assert self._llm, "LLM must be initialized before generating text"
619
+ return await self._llm.generate([prompt], request_params)
620
+
621
+ @staticmethod
622
+ def _build_plan_entries_for_step(step: PlanningStep | Step) -> list[PlanEntry]:
623
+ """
624
+ Build plan entries for telemetry using individual step tasks when available.
625
+ """
626
+ if step.tasks:
627
+ entries = []
628
+ for task in step.tasks:
629
+ agent_name = getattr(task, "agent", None) or "Unknown agent"
630
+ entries.append(
631
+ PlanEntry(
632
+ content=f"{agent_name}: {task.description}",
633
+ priority="high",
634
+ status="pending",
635
+ )
636
+ )
637
+ return entries
638
+
639
+ # Fallback to a single entry if the planner supplied no explicit tasks
640
+ return [
641
+ PlanEntry(
642
+ content=step.description,
643
+ priority="high",
644
+ status="pending",
645
+ )
646
+ ]
647
+
648
+ @staticmethod
649
+ def _set_plan_entries_status(entries: list[PlanEntry], status: PlanEntryStatus) -> None:
650
+ """Helper to bulk update plan entry statuses."""
651
+ for entry in entries:
652
+ entry.status = status