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,379 @@
1
+ """
2
+ MAKER: Massively decomposed Agentic processes with K-voting Error Reduction.
3
+
4
+ Implementation based on the paper:
5
+ "Solving a Million-Step LLM Task with Zero Errors" (arXiv:2511.09030)
6
+ https://arxiv.org/abs/2511.09030
7
+
8
+ This workflow implements first-to-ahead-by-k voting for statistical error
9
+ correction, enabling high reliability with cost-effective models. The key
10
+ insight is that by sampling multiple responses and requiring a k-vote margin
11
+ for consensus, the probability of error decreases exponentially while cost
12
+ grows only logarithmically with the number of steps.
13
+
14
+ Key concepts from the paper:
15
+ - Maximal Agentic Decomposition (MAD): Break tasks into single-step subtasks
16
+ - First-to-ahead-by-k voting: Winner needs k more votes than runner-up
17
+ - Red-flagging: Discard suspicious outputs (too long, malformed) before voting
18
+ """
19
+
20
+ from collections import defaultdict
21
+ from enum import StrEnum
22
+ from typing import Any, Callable, List, Optional, Tuple, Type
23
+
24
+ from mcp import Tool
25
+ from opentelemetry import trace
26
+ from pydantic import BaseModel, Field
27
+
28
+ from fast_agent.agents.agent_types import AgentConfig, AgentType
29
+ from fast_agent.agents.llm_agent import LlmAgent
30
+ from fast_agent.core.exceptions import AgentConfigError
31
+ from fast_agent.core.logging.logger import get_logger
32
+ from fast_agent.core.prompt import Prompt
33
+ from fast_agent.interfaces import AgentProtocol, ModelT
34
+ from fast_agent.types import PromptMessageExtended, RequestParams
35
+
36
+ logger = get_logger(__name__)
37
+
38
+
39
+ class MatchStrategy(StrEnum):
40
+ """
41
+ Strategies for comparing responses during voting.
42
+
43
+ The choice of strategy affects how responses are grouped for voting:
44
+ - EXACT: Responses must match character-for-character
45
+ - NORMALIZED: Whitespace and case differences are ignored
46
+ - STRUCTURED: JSON responses are parsed and compared structurally
47
+ """
48
+
49
+ EXACT = "exact"
50
+ NORMALIZED = "normalized"
51
+ STRUCTURED = "structured"
52
+
53
+
54
+ class MakerResult(BaseModel):
55
+ """
56
+ Result of a MAKER voting process.
57
+
58
+ Provides transparency into the voting outcome for debugging and analysis.
59
+ """
60
+
61
+ winner: str = Field(description="The winning response text")
62
+ votes: dict[str, int] = Field(
63
+ default_factory=dict, description="Vote counts per unique response"
64
+ )
65
+ total_samples: int = Field(default=0, description="Total samples drawn")
66
+ discarded_samples: int = Field(
67
+ default=0, description="Samples discarded due to red-flags"
68
+ )
69
+ margin: int = Field(default=0, description="Winning margin achieved")
70
+ converged: bool = Field(
71
+ default=False, description="Whether k-margin consensus was achieved"
72
+ )
73
+
74
+
75
+ class MakerAgent(LlmAgent):
76
+ """
77
+ MAKER: Massively decomposed Agentic processes with K-voting Error Reduction.
78
+
79
+ Implements first-to-ahead-by-k voting for statistical error correction.
80
+ Multiple samples are drawn from a worker agent, and the first response
81
+ to achieve a k-vote margin over all alternatives wins.
82
+
83
+ This approach enables:
84
+ - High reliability with cheap/small models
85
+ - Logarithmic cost scaling with task complexity
86
+ - Provable error bounds based on per-step success rate
87
+
88
+ Reference: "Solving a Million-Step LLM Task with Zero Errors"
89
+ https://arxiv.org/abs/2511.09030
90
+ """
91
+
92
+ @property
93
+ def agent_type(self) -> AgentType:
94
+ """Return the type of this agent."""
95
+ return AgentType.MAKER
96
+
97
+ def __init__(
98
+ self,
99
+ config: AgentConfig,
100
+ worker_agent: AgentProtocol,
101
+ k: int = 3,
102
+ max_samples: int = 50,
103
+ match_strategy: MatchStrategy = MatchStrategy.EXACT,
104
+ match_fn: Callable[[str], str] | None = None,
105
+ red_flag_max_length: int | None = None,
106
+ red_flag_validator: Callable[[str], bool] | None = None,
107
+ context: Optional[Any] = None,
108
+ **kwargs,
109
+ ) -> None:
110
+ """
111
+ Initialize the MAKER agent.
112
+
113
+ Args:
114
+ config: Agent configuration
115
+ worker_agent: The agent to sample from for voting
116
+ k: Margin required to declare a winner (first-to-ahead-by-k).
117
+ Higher k = more reliable but more samples needed.
118
+ Paper recommends k >= 3 for high reliability.
119
+ max_samples: Maximum samples before falling back to plurality vote
120
+ match_strategy: How to compare responses for voting
121
+ match_fn: Custom function to normalize responses for comparison.
122
+ If provided, overrides match_strategy.
123
+ red_flag_max_length: Discard responses longer than this (characters).
124
+ Per the paper, overly long responses correlate
125
+ with errors.
126
+ red_flag_validator: Custom validator function. Return False to
127
+ discard the response (red-flag it).
128
+ context: Optional context object
129
+ """
130
+ super().__init__(config, context=context, **kwargs)
131
+
132
+ if not worker_agent:
133
+ raise AgentConfigError("Worker agent must be provided")
134
+ if k < 1:
135
+ raise AgentConfigError("k must be at least 1")
136
+ if max_samples < k:
137
+ raise AgentConfigError("max_samples must be at least k")
138
+
139
+ self.worker_agent = worker_agent
140
+ self.k = k
141
+ self.max_samples = max_samples
142
+ self.match_strategy = match_strategy
143
+ self.match_fn = match_fn
144
+ self.red_flag_max_length = red_flag_max_length
145
+ self.red_flag_validator = red_flag_validator
146
+
147
+ # Result tracking
148
+ self.last_result: MakerResult | None = None
149
+
150
+ def _normalize_response(self, response: str) -> str:
151
+ """
152
+ Normalize response for comparison based on configured strategy.
153
+
154
+ Args:
155
+ response: Raw response text
156
+
157
+ Returns:
158
+ Normalized response for vote counting
159
+ """
160
+ if self.match_fn:
161
+ return self.match_fn(response)
162
+
163
+ match self.match_strategy:
164
+ case MatchStrategy.EXACT:
165
+ return response
166
+ case MatchStrategy.NORMALIZED:
167
+ return " ".join(response.lower().split())
168
+ case MatchStrategy.STRUCTURED:
169
+ import json
170
+
171
+ try:
172
+ parsed = json.loads(response)
173
+ return json.dumps(parsed, sort_keys=True)
174
+ except json.JSONDecodeError:
175
+ return response
176
+
177
+ return response
178
+
179
+ def _is_red_flagged(self, response: str) -> bool:
180
+ """
181
+ Check if response should be discarded (red-flagged).
182
+
183
+ Per the MAKER paper, red-flagging improves effective success rate
184
+ by discarding responses that show signs of confusion:
185
+ - Overly long responses (model went off track)
186
+ - Malformed responses (parsing issues indicate confusion)
187
+
188
+ Args:
189
+ response: Response text to check
190
+
191
+ Returns:
192
+ True if response should be discarded
193
+ """
194
+ if self.red_flag_max_length and len(response) > self.red_flag_max_length:
195
+ logger.debug(
196
+ f"Red-flagged: response length {len(response)} > {self.red_flag_max_length}"
197
+ )
198
+ return True
199
+
200
+ if self.red_flag_validator and not self.red_flag_validator(response):
201
+ logger.debug("Red-flagged: custom validator returned False")
202
+ return True
203
+
204
+ return False
205
+
206
+ def _check_winner(self, votes: dict[str, int]) -> str | None:
207
+ """
208
+ Check if any response has achieved k-margin victory.
209
+
210
+ First-to-ahead-by-k: winner needs k more votes than the runner-up.
211
+
212
+ Args:
213
+ votes: Current vote counts
214
+
215
+ Returns:
216
+ Winning response key if k-margin achieved, None otherwise
217
+ """
218
+ if not votes:
219
+ return None
220
+
221
+ sorted_items = sorted(votes.items(), key=lambda x: x[1], reverse=True)
222
+ leader_key, leader_votes = sorted_items[0]
223
+ runner_up_votes = sorted_items[1][1] if len(sorted_items) > 1 else 0
224
+
225
+ if leader_votes - runner_up_votes >= self.k:
226
+ return leader_key
227
+
228
+ return None
229
+
230
+ async def generate_impl(
231
+ self,
232
+ messages: List[PromptMessageExtended],
233
+ request_params: RequestParams | None = None,
234
+ tools: List[Tool] | None = None,
235
+ ) -> PromptMessageExtended:
236
+ """
237
+ Generate a response using first-to-ahead-by-k voting.
238
+
239
+ Samples from the worker agent until one response achieves a k-vote
240
+ margin over all alternatives, or max_samples is reached.
241
+
242
+ Args:
243
+ messages: Input messages
244
+ request_params: Optional request parameters
245
+ tools: Optional tools (passed to worker)
246
+
247
+ Returns:
248
+ The winning response
249
+ """
250
+ tracer = trace.get_tracer(__name__)
251
+ with tracer.start_as_current_span(f"Maker: '{self._name}' generate"):
252
+ votes: dict[str, int] = defaultdict(int)
253
+ response_map: dict[str, PromptMessageExtended] = {}
254
+ total_samples = 0
255
+ discarded_samples = 0
256
+
257
+ while total_samples < self.max_samples:
258
+ async with self.workflow_telemetry.start_step(
259
+ "maker.sample",
260
+ server_name=self.name,
261
+ arguments={
262
+ "agent": self.worker_agent.name,
263
+ "sample": total_samples + 1,
264
+ "current_votes": dict(votes),
265
+ },
266
+ ) as step:
267
+ response = await self.worker_agent.generate(
268
+ messages, request_params
269
+ )
270
+ response_text = response.last_text() or ""
271
+ total_samples += 1
272
+
273
+ # Red-flag check
274
+ if self._is_red_flagged(response_text):
275
+ discarded_samples += 1
276
+ await step.finish(
277
+ False, text=f"Sample {total_samples} red-flagged, discarded"
278
+ )
279
+ continue
280
+
281
+ # Normalize and record vote
282
+ normalized = self._normalize_response(response_text)
283
+ votes[normalized] += 1
284
+ response_map[normalized] = response
285
+
286
+ await step.finish(
287
+ True,
288
+ text=f"Sample {total_samples}: {votes[normalized]} votes for this response",
289
+ )
290
+
291
+ # Check for k-margin winner
292
+ winner_key = self._check_winner(votes)
293
+ if winner_key:
294
+ sorted_votes = sorted(votes.values(), reverse=True)
295
+ margin = sorted_votes[0] - (
296
+ sorted_votes[1] if len(sorted_votes) > 1 else 0
297
+ )
298
+
299
+ self.last_result = MakerResult(
300
+ winner=winner_key,
301
+ votes=dict(votes),
302
+ total_samples=total_samples,
303
+ discarded_samples=discarded_samples,
304
+ margin=margin,
305
+ converged=True,
306
+ )
307
+
308
+ logger.debug(
309
+ f"MAKER converged: {votes[winner_key]} votes, "
310
+ f"margin {margin}, {total_samples} samples"
311
+ )
312
+ return response_map[winner_key]
313
+
314
+ # Max samples reached - fall back to plurality
315
+ logger.warning(
316
+ f"MAKER: max_samples ({self.max_samples}) reached without "
317
+ f"k-margin ({self.k}) consensus, using plurality"
318
+ )
319
+
320
+ if not votes:
321
+ # All samples were red-flagged
322
+ raise AgentConfigError(
323
+ f"All {total_samples} samples were red-flagged. "
324
+ "Consider relaxing red-flag criteria."
325
+ )
326
+
327
+ winner_key = max(votes, key=lambda x: votes[x])
328
+ sorted_votes = sorted(votes.values(), reverse=True)
329
+ margin = sorted_votes[0] - (
330
+ sorted_votes[1] if len(sorted_votes) > 1 else 0
331
+ )
332
+
333
+ self.last_result = MakerResult(
334
+ winner=winner_key,
335
+ votes=dict(votes),
336
+ total_samples=total_samples,
337
+ discarded_samples=discarded_samples,
338
+ margin=margin,
339
+ converged=False,
340
+ )
341
+
342
+ return response_map[winner_key]
343
+
344
+ async def structured_impl(
345
+ self,
346
+ messages: List[PromptMessageExtended],
347
+ model: Type[ModelT],
348
+ request_params: RequestParams | None = None,
349
+ ) -> Tuple[ModelT | None, PromptMessageExtended]:
350
+ """
351
+ Generate a voted response and parse into structured format.
352
+
353
+ Args:
354
+ messages: Input messages
355
+ model: Pydantic model class for structured output
356
+ request_params: Optional request parameters
357
+
358
+ Returns:
359
+ Tuple of (parsed model or None, raw response)
360
+ """
361
+ response = await self.generate_impl(messages, request_params)
362
+ return await self.worker_agent.structured(
363
+ [Prompt.user(response.all_text())], model, request_params
364
+ )
365
+
366
+ async def initialize(self) -> None:
367
+ """Initialize the agent and its worker agent."""
368
+ await super().initialize()
369
+ if not self.worker_agent.initialized:
370
+ await self.worker_agent.initialize()
371
+ self.initialized = True
372
+
373
+ async def shutdown(self) -> None:
374
+ """Shutdown the agent and its worker agent."""
375
+ await super().shutdown()
376
+ try:
377
+ await self.worker_agent.shutdown()
378
+ except Exception as e:
379
+ logger.warning(f"Error shutting down worker agent: {str(e)}")
@@ -0,0 +1,218 @@
1
+
2
+ from pydantic import BaseModel, ConfigDict, Field
3
+
4
+ from fast_agent.agents.workflow.orchestrator_prompts import (
5
+ PLAN_RESULT_TEMPLATE,
6
+ STEP_RESULT_TEMPLATE,
7
+ TASK_RESULT_TEMPLATE,
8
+ )
9
+
10
+
11
+ class Task(BaseModel):
12
+ """An individual task that needs to be executed"""
13
+
14
+ description: str = Field(description="Description of the task")
15
+
16
+
17
+ class ServerTask(Task):
18
+ """An individual task that can be accomplished by one or more MCP servers"""
19
+
20
+ servers: list[str] = Field(
21
+ description="Names of MCP servers that the LLM has access to for this task",
22
+ default_factory=list,
23
+ )
24
+
25
+
26
+ class AgentTask(Task):
27
+ """An individual task that can be accomplished by an Agent."""
28
+
29
+ agent: str = Field(
30
+ description="Name of Agent from given list of agents that the LLM has access to for this task",
31
+ )
32
+
33
+
34
+ class Step(BaseModel):
35
+ """A step containing independent tasks that can be executed in parallel"""
36
+
37
+ description: str = Field(description="Description of the step")
38
+
39
+ tasks: list[AgentTask] = Field(
40
+ description="Subtasks that can be executed in parallel",
41
+ default_factory=list,
42
+ )
43
+
44
+
45
+ class PlanningStep(Step):
46
+ """Single next step in iterative planning"""
47
+
48
+ is_complete: bool = Field(description="Whether the overall plan objective is complete")
49
+
50
+
51
+ class Plan(BaseModel):
52
+ """Plan generated by the orchestrator planner."""
53
+
54
+ steps: list[Step] = Field(
55
+ description="List of steps to execute sequentially",
56
+ default_factory=list,
57
+ )
58
+ is_complete: bool = Field(description="Whether the overall plan objective is complete")
59
+
60
+
61
+ class TaskWithResult(Task):
62
+ """An individual task with its result"""
63
+
64
+ result: str = Field(description="Result of executing the task", default="Task completed")
65
+
66
+ agent: str = Field(description="Name of the agent that executed this task", default="")
67
+
68
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
69
+
70
+
71
+ class StepResult(BaseModel):
72
+ """Result of executing a step"""
73
+
74
+ step: Step = Field(description="The step that was executed")
75
+ task_results: list[TaskWithResult] = Field(
76
+ description="Results of executing each task", default_factory=list
77
+ )
78
+ result: str = Field(description="Result of executing the step", default="Step completed")
79
+
80
+ def add_task_result(self, task_result: TaskWithResult) -> None:
81
+ """Add a task result to this step"""
82
+ if not isinstance(self.task_results, list):
83
+ self.task_results = []
84
+ self.task_results.append(task_result)
85
+
86
+
87
+ class PlanResult(BaseModel):
88
+ """Results of executing a plan"""
89
+
90
+ objective: str
91
+ """Objective of the plan"""
92
+
93
+ plan: Plan | None = None
94
+ """The plan that was executed"""
95
+
96
+ step_results: list[StepResult]
97
+ """Results of executing each step"""
98
+
99
+ is_complete: bool = False
100
+ """Whether the overall plan objective is complete"""
101
+
102
+ max_iterations_reached: bool = False
103
+ """Whether the plan execution reached the maximum number of iterations without completing"""
104
+
105
+ result: str | None = None
106
+ """Result of executing the plan"""
107
+
108
+ def add_step_result(self, step_result: StepResult) -> None:
109
+ """Add a step result to this plan"""
110
+ if not isinstance(self.step_results, list):
111
+ self.step_results = []
112
+ self.step_results.append(step_result)
113
+
114
+
115
+ def format_task_result_text(task_result: TaskWithResult) -> str:
116
+ """Format a task result as plain text for display"""
117
+ return TASK_RESULT_TEMPLATE.format(
118
+ task_description=task_result.description, task_result=task_result.result
119
+ )
120
+
121
+
122
+ def format_step_result_text(step_result: StepResult) -> str:
123
+ """Format a step result as plain text for display"""
124
+ tasks_str = "\n".join(
125
+ f" - {format_task_result_text(task)}" for task in step_result.task_results
126
+ )
127
+ return STEP_RESULT_TEMPLATE.format(
128
+ step_description=step_result.step.description,
129
+ step_result=step_result.result,
130
+ tasks_str=tasks_str,
131
+ )
132
+
133
+
134
+ def format_plan_result_text(plan_result: PlanResult) -> str:
135
+ """Format the full plan execution state as plain text for display"""
136
+ steps_str = (
137
+ "\n\n".join(
138
+ f"{i + 1}:\n{format_step_result_text(step)}"
139
+ for i, step in enumerate(plan_result.step_results)
140
+ )
141
+ if plan_result.step_results
142
+ else "No steps executed yet"
143
+ )
144
+
145
+ return PLAN_RESULT_TEMPLATE.format(
146
+ plan_objective=plan_result.objective,
147
+ steps_str=steps_str,
148
+ plan_result=plan_result.result if plan_result.is_complete else "In Progress",
149
+ )
150
+
151
+
152
+ def format_task_result_xml(task_result: TaskWithResult) -> str:
153
+ """Format a task result with XML tags for better semantic understanding"""
154
+ from fast_agent.llm.prompt_utils import format_fastagent_tag
155
+
156
+ return format_fastagent_tag(
157
+ "task-result",
158
+ f"\n<fastagent:description>{task_result.description}</fastagent:description>\n"
159
+ f"<fastagent:result>{task_result.result}</fastagent:result>\n",
160
+ {
161
+ "description": task_result.description[:50] + "..."
162
+ if len(task_result.description) > 50
163
+ else task_result.description
164
+ },
165
+ )
166
+
167
+
168
+ def format_step_result_xml(step_result: StepResult) -> str:
169
+ """Format a step result with XML tags for better semantic understanding"""
170
+ from fast_agent.llm.prompt_utils import format_fastagent_tag
171
+
172
+ # Format each task result with XML
173
+ task_results = []
174
+ for task in step_result.task_results:
175
+ task_results.append(format_task_result_xml(task))
176
+
177
+ # Combine task results
178
+ task_results_str = "\n".join(task_results)
179
+
180
+ # Build step result with metadata and tasks
181
+ step_content = (
182
+ f"<fastagent:description>{step_result.step.description}</fastagent:description>\n"
183
+ f"<fastagent:summary>{step_result.result}</fastagent:summary>\n"
184
+ f"<fastagent:task-results>\n{task_results_str}\n</fastagent:task-results>\n"
185
+ )
186
+
187
+ return format_fastagent_tag("step-result", step_content)
188
+
189
+
190
+ def format_plan_result(plan_result: PlanResult) -> str:
191
+ """Format the full plan execution state with XML for better semantic understanding"""
192
+ from fast_agent.llm.prompt_utils import format_fastagent_tag
193
+
194
+ # Format objective
195
+ objective_tag = format_fastagent_tag("objective", plan_result.objective)
196
+
197
+ # Format step results
198
+ step_results = []
199
+ for step in plan_result.step_results:
200
+ step_results.append(format_step_result_xml(step))
201
+
202
+ # Build progress section
203
+ if step_results:
204
+ steps_content = "\n".join(step_results)
205
+ progress_content = (
206
+ f"{objective_tag}\n"
207
+ f"<fastagent:steps>\n{steps_content}\n</fastagent:steps>\n"
208
+ f"<fastagent:status>{plan_result.result if plan_result.is_complete else 'In Progress'}</fastagent:status>\n"
209
+ )
210
+ else:
211
+ # No steps executed yet
212
+ progress_content = (
213
+ f"{objective_tag}\n"
214
+ f"<fastagent:steps>No steps executed yet</fastagent:steps>\n"
215
+ f"<fastagent:status>Not Started</fastagent:status>\n"
216
+ )
217
+
218
+ return format_fastagent_tag("progress", progress_content)