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,402 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ import platform
6
+ import shutil
7
+ import signal
8
+ import subprocess
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from mcp.types import CallToolResult, TextContent, Tool
14
+
15
+ from fast_agent.ui import console
16
+ from fast_agent.ui.progress_display import progress_display
17
+
18
+
19
+ class ShellRuntime:
20
+ """Helper for managing the optional local shell execute tool."""
21
+
22
+ def __init__(
23
+ self,
24
+ activation_reason: str | None,
25
+ logger,
26
+ timeout_seconds: int = 90,
27
+ warning_interval_seconds: int = 30,
28
+ skills_directory: Path | None = None,
29
+ ) -> None:
30
+ self._activation_reason = activation_reason
31
+ self._logger = logger
32
+ self._timeout_seconds = timeout_seconds
33
+ self._warning_interval_seconds = warning_interval_seconds
34
+ self._skills_directory = skills_directory
35
+ self.enabled: bool = activation_reason is not None
36
+ self._tool: Tool | None = None
37
+
38
+ if self.enabled:
39
+ # Detect the shell early so we can include it in the tool description
40
+ runtime_info = self.runtime_info()
41
+ shell_name = runtime_info.get("name", "shell")
42
+
43
+ self._tool = Tool(
44
+ name="execute",
45
+ description=f"Run a shell command directly in {shell_name}.",
46
+ inputSchema={
47
+ "type": "object",
48
+ "properties": {
49
+ "command": {
50
+ "type": "string",
51
+ "description": "Command string only - no shell executable prefix (correct: 'pwd', incorrect: 'bash -c pwd').",
52
+ }
53
+ },
54
+ "required": ["command"],
55
+ "additionalProperties": False,
56
+ },
57
+ )
58
+
59
+ @property
60
+ def tool(self) -> Tool | None:
61
+ return self._tool
62
+
63
+ def announce(self) -> None:
64
+ """Inform the user why the local shell tool is active."""
65
+ if not self.enabled or not self._activation_reason:
66
+ return
67
+
68
+ message = f"Local shell execute tool enabled {self._activation_reason}."
69
+ self._logger.info(message)
70
+
71
+ def working_directory(self) -> Path:
72
+ """Return the working directory used for shell execution."""
73
+ # Skills now show their location relative to cwd in the system prompt
74
+ return Path.cwd()
75
+
76
+ def runtime_info(self) -> dict[str, str | None]:
77
+ """Best-effort detection of the shell runtime used for local execution.
78
+
79
+ Uses modern Python APIs (platform.system(), shutil.which()) to detect
80
+ and prefer modern shells like pwsh (PowerShell 7+) and bash.
81
+ """
82
+ system = platform.system()
83
+
84
+ if system == "Windows":
85
+ # Preference order: pwsh > powershell > cmd
86
+ for shell_name in ["pwsh", "powershell", "cmd"]:
87
+ shell_path = shutil.which(shell_name)
88
+ if shell_path:
89
+ return {"name": shell_name, "path": shell_path}
90
+
91
+ # Fallback to COMSPEC if nothing found in PATH
92
+ comspec = os.environ.get("COMSPEC", "cmd.exe")
93
+ return {"name": Path(comspec).name, "path": comspec}
94
+ else:
95
+ # Unix-like: check SHELL env, then search for common shells
96
+ shell_env = os.environ.get("SHELL")
97
+ if shell_env and Path(shell_env).exists():
98
+ return {"name": Path(shell_env).name, "path": shell_env}
99
+
100
+ # Preference order: bash > zsh > sh
101
+ for shell_name in ["bash", "zsh", "sh"]:
102
+ shell_path = shutil.which(shell_name)
103
+ if shell_path:
104
+ return {"name": shell_name, "path": shell_path}
105
+
106
+ # Fallback to generic sh
107
+ return {"name": "sh", "path": None}
108
+
109
+ def metadata(self, command: str | None) -> dict[str, Any]:
110
+ """Build metadata for display when the shell tool is invoked."""
111
+ info = self.runtime_info()
112
+ working_dir = self.working_directory()
113
+ try:
114
+ working_dir_display = str(working_dir.relative_to(Path.cwd()))
115
+ except ValueError:
116
+ working_dir_display = str(working_dir)
117
+
118
+ return {
119
+ "variant": "shell",
120
+ "command": command,
121
+ "shell_name": info.get("name"),
122
+ "shell_path": info.get("path"),
123
+ "working_dir": str(working_dir),
124
+ "working_dir_display": working_dir_display,
125
+ "timeout_seconds": self._timeout_seconds,
126
+ "warning_interval_seconds": self._warning_interval_seconds,
127
+ "streams_output": True,
128
+ "returns_exit_code": True,
129
+ }
130
+
131
+ async def execute(self, arguments: dict[str, Any] | None = None) -> CallToolResult:
132
+ """Execute a shell command and stream output to the console with timeout detection."""
133
+ command_value = (arguments or {}).get("command") if arguments else None
134
+ if not isinstance(command_value, str) or not command_value.strip():
135
+ return CallToolResult(
136
+ isError=True,
137
+ content=[
138
+ TextContent(
139
+ type="text",
140
+ text="The execute tool requires a 'command' string argument.",
141
+ )
142
+ ],
143
+ )
144
+
145
+ command = command_value.strip()
146
+ self._logger.debug(
147
+ f"Executing command with timeout={self._timeout_seconds}s, warning_interval={self._warning_interval_seconds}s"
148
+ )
149
+
150
+ # Pause progress display during shell execution to avoid overlaying output
151
+ with progress_display.paused():
152
+ try:
153
+ working_dir = self.working_directory()
154
+ runtime_details = self.runtime_info()
155
+ shell_name = (runtime_details.get("name") or "").lower()
156
+ shell_path = runtime_details.get("path")
157
+
158
+ # Detect platform for process group handling
159
+ is_windows = platform.system() == "Windows"
160
+
161
+ # Shared process kwargs
162
+ process_kwargs: dict[str, Any] = {
163
+ "stdout": asyncio.subprocess.PIPE,
164
+ "stderr": asyncio.subprocess.PIPE,
165
+ "cwd": working_dir,
166
+ }
167
+
168
+ if is_windows:
169
+ # Windows: CREATE_NEW_PROCESS_GROUP allows killing process tree
170
+ process_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
171
+ else:
172
+ # Unix: start_new_session creates new process group
173
+ process_kwargs["start_new_session"] = True
174
+
175
+ # Create the subprocess, preferring PowerShell on Windows when available
176
+ if is_windows and shell_path and shell_name in {"pwsh", "powershell"}:
177
+ process = await asyncio.create_subprocess_exec(
178
+ shell_path,
179
+ "-NoLogo",
180
+ "-NoProfile",
181
+ "-Command",
182
+ command,
183
+ **process_kwargs,
184
+ )
185
+ else:
186
+ if shell_path:
187
+ process_kwargs["executable"] = shell_path
188
+ process = await asyncio.create_subprocess_shell(
189
+ command,
190
+ **process_kwargs,
191
+ )
192
+
193
+ output_segments: list[str] = []
194
+ # Track last output time in a mutable container for sharing across coroutines
195
+ last_output_time = [time.time()]
196
+ timeout_occurred = [False]
197
+ watchdog_task = None
198
+
199
+ async def stream_output(
200
+ stream, style: str | None, is_stderr: bool = False
201
+ ) -> None:
202
+ if not stream:
203
+ return
204
+ while True:
205
+ line = await stream.readline()
206
+ if not line:
207
+ break
208
+ text = line.decode(errors="replace")
209
+ output_segments.append(text if not is_stderr else f"[stderr] {text}")
210
+ console.console.print(
211
+ text.rstrip("\n"),
212
+ style=style,
213
+ markup=False,
214
+ )
215
+ # Update last output time whenever we receive a line
216
+ last_output_time[0] = time.time()
217
+
218
+ async def watchdog() -> None:
219
+ """Monitor output timeout and emit warnings."""
220
+ last_warning_time = 0.0
221
+ self._logger.debug(
222
+ f"Watchdog started: timeout={self._timeout_seconds}s, warning_interval={self._warning_interval_seconds}s"
223
+ )
224
+
225
+ while True:
226
+ await asyncio.sleep(1) # Check every second
227
+
228
+ # Check if process has exited
229
+ if process.returncode is not None:
230
+ self._logger.debug("Watchdog: process exited normally")
231
+ break
232
+
233
+ elapsed = time.time() - last_output_time[0]
234
+ remaining = self._timeout_seconds - elapsed
235
+
236
+ # Emit warnings every warning_interval_seconds throughout execution
237
+ time_since_warning = elapsed - last_warning_time
238
+ if time_since_warning >= self._warning_interval_seconds and remaining > 0:
239
+ self._logger.debug(f"Watchdog: warning at {int(remaining)}s remaining")
240
+ console.console.print(
241
+ f"▶ No output detected - terminating in {int(remaining)}s",
242
+ style="black on red",
243
+ )
244
+ last_warning_time = elapsed
245
+
246
+ # Timeout exceeded
247
+ if elapsed >= self._timeout_seconds:
248
+ timeout_occurred[0] = True
249
+ self._logger.debug(
250
+ "Watchdog: timeout exceeded, terminating process group"
251
+ )
252
+ console.console.print(
253
+ "▶ Timeout exceeded - terminating process", style="black on red"
254
+ )
255
+ try:
256
+ if is_windows:
257
+ # Windows: try to signal the entire process group before terminating
258
+ try:
259
+ process.send_signal(signal.CTRL_BREAK_EVENT)
260
+ await asyncio.sleep(2)
261
+ except AttributeError:
262
+ # Older Python/asyncio may not support send_signal on Windows
263
+ self._logger.debug(
264
+ "Watchdog: CTRL_BREAK_EVENT unsupported, skipping"
265
+ )
266
+ except ValueError:
267
+ # Raised when no console is attached; fall back to terminate
268
+ self._logger.debug(
269
+ "Watchdog: no console attached for CTRL_BREAK_EVENT"
270
+ )
271
+ except ProcessLookupError:
272
+ pass # Process already exited
273
+
274
+ if process.returncode is None:
275
+ process.terminate()
276
+ await asyncio.sleep(2)
277
+ if process.returncode is None:
278
+ process.kill()
279
+ else:
280
+ # Unix: kill entire process group for clean cleanup
281
+ os.killpg(process.pid, signal.SIGTERM)
282
+ await asyncio.sleep(2)
283
+ if process.returncode is None:
284
+ os.killpg(process.pid, signal.SIGKILL)
285
+ except (ProcessLookupError, OSError):
286
+ pass # Process already terminated
287
+ except Exception as e:
288
+ self._logger.debug(f"Error terminating process: {e}")
289
+ # Fallback: kill just the main process
290
+ try:
291
+ process.kill()
292
+ except Exception:
293
+ pass
294
+ break
295
+
296
+ stdout_task = asyncio.create_task(stream_output(process.stdout, None))
297
+ stderr_task = asyncio.create_task(stream_output(process.stderr, "red", True))
298
+ watchdog_task = asyncio.create_task(watchdog())
299
+
300
+ # Wait for streams to complete
301
+ await asyncio.gather(stdout_task, stderr_task, return_exceptions=True)
302
+
303
+ # Cancel watchdog if still running
304
+ if watchdog_task and not watchdog_task.done():
305
+ watchdog_task.cancel()
306
+ try:
307
+ await watchdog_task
308
+ except asyncio.CancelledError:
309
+ pass
310
+
311
+ # Wait for process to finish
312
+ try:
313
+ return_code = await asyncio.wait_for(process.wait(), timeout=2.0)
314
+ except asyncio.TimeoutError:
315
+ # Process didn't exit, force kill
316
+ try:
317
+ if is_windows:
318
+ # Windows: force kill main process
319
+ process.kill()
320
+ else:
321
+ # Unix: SIGKILL to process group
322
+ os.killpg(process.pid, signal.SIGKILL)
323
+ return_code = await process.wait()
324
+ except Exception:
325
+ return_code = -1
326
+
327
+ # Build result based on timeout or normal completion
328
+ if timeout_occurred[0]:
329
+ combined_output = "".join(output_segments)
330
+ if combined_output and not combined_output.endswith("\n"):
331
+ combined_output += "\n"
332
+ combined_output += (
333
+ f"(timeout after {self._timeout_seconds}s - process terminated)"
334
+ )
335
+
336
+ result = CallToolResult(
337
+ isError=True,
338
+ content=[
339
+ TextContent(
340
+ type="text",
341
+ text=combined_output,
342
+ )
343
+ ],
344
+ )
345
+ else:
346
+ combined_output = "".join(output_segments)
347
+ # Add explicit exit code message for the LLM
348
+ if combined_output and not combined_output.endswith("\n"):
349
+ combined_output += "\n"
350
+ combined_output += f"process exit code was {return_code}"
351
+
352
+ result = CallToolResult(
353
+ isError=return_code != 0,
354
+ content=[
355
+ TextContent(
356
+ type="text",
357
+ text=combined_output,
358
+ )
359
+ ],
360
+ )
361
+
362
+ # Display bottom separator with exit code
363
+ try:
364
+ from rich.text import Text
365
+ except Exception: # pragma: no cover
366
+ Text = None # type: ignore[assignment]
367
+
368
+ if Text:
369
+ # Build bottom separator matching the style: ─| exit code 0 |─────────
370
+ width = console.console.size.width
371
+ exit_code_style = "red" if return_code != 0 else "dim"
372
+ exit_code_text = f"exit code {return_code}"
373
+
374
+ prefix = Text("─| ")
375
+ prefix.stylize("dim")
376
+ exit_text = Text(exit_code_text, style=exit_code_style)
377
+ suffix = Text(" |")
378
+ suffix.stylize("dim")
379
+
380
+ separator = Text()
381
+ separator.append_text(prefix)
382
+ separator.append_text(exit_text)
383
+ separator.append_text(suffix)
384
+ remaining = width - separator.cell_len
385
+ if remaining > 0:
386
+ separator.append("─" * remaining, style="dim")
387
+
388
+ console.console.print()
389
+ console.console.print(separator)
390
+ else:
391
+ console.console.print(f"exit code {return_code}", style="dim")
392
+
393
+ setattr(result, "_suppress_display", True)
394
+ setattr(result, "exit_code", return_code)
395
+ return result
396
+
397
+ except Exception as exc:
398
+ self._logger.error(f"Execute tool failed: {exc}")
399
+ return CallToolResult(
400
+ isError=True,
401
+ content=[TextContent(type="text", text=f"Command failed to start: {exc}")],
402
+ )
@@ -0,0 +1,59 @@
1
+ """Shared type definitions and helpers for fast-agent.
2
+
3
+ Goals:
4
+ - Provide a stable import path for commonly used public types and helpers
5
+ - Keep dependencies minimal to reduce import-time cycles
6
+ """
7
+
8
+ # Re-export common enums/types
9
+ # Public request parameters used to configure LLM calls
10
+ # Re-export ResourceLink from MCP for convenience
11
+ from mcp.types import ResourceLink
12
+
13
+ from fast_agent.llm.request_params import RequestParams
14
+
15
+ # Content helpers commonly used by users to build messages
16
+ from fast_agent.mcp.helpers.content_helpers import (
17
+ audio_link,
18
+ ensure_multipart_messages,
19
+ image_link,
20
+ normalize_to_extended_list,
21
+ resource_link,
22
+ text_content,
23
+ video_link,
24
+ )
25
+
26
+ # Public message model used across providers and MCP integration
27
+ from fast_agent.mcp.prompt_message_extended import PromptMessageExtended
28
+
29
+ # Conversation analysis utilities
30
+ from .conversation_summary import ConversationSummary
31
+
32
+ # Stop reason enum - imported directly to avoid circular dependency
33
+ from .llm_stop_reason import LlmStopReason
34
+
35
+ # Message search utilities
36
+ from .message_search import extract_first, extract_last, find_matches, search_messages
37
+
38
+ __all__ = [
39
+ # Enums / types
40
+ "LlmStopReason",
41
+ "PromptMessageExtended",
42
+ "RequestParams",
43
+ "ResourceLink",
44
+ # Content helpers
45
+ "text_content",
46
+ "resource_link",
47
+ "image_link",
48
+ "video_link",
49
+ "audio_link",
50
+ "ensure_multipart_messages",
51
+ "normalize_to_extended_list",
52
+ # Analysis utilities
53
+ "ConversationSummary",
54
+ # Search utilities
55
+ "search_messages",
56
+ "find_matches",
57
+ "extract_first",
58
+ "extract_last",
59
+ ]