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,408 @@
1
+ """
2
+ ACPTerminalRuntime - Execute commands via ACP terminal support.
3
+
4
+ This runtime allows FastAgent to execute commands through the ACP client's terminal
5
+ capabilities when available (e.g., in Zed editor). This provides better integration
6
+ compared to local process execution.
7
+ """
8
+
9
+ import asyncio
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from mcp.types import CallToolResult, Tool
13
+
14
+ from fast_agent.constants import DEFAULT_TERMINAL_OUTPUT_BYTE_LIMIT
15
+ from fast_agent.core.logging.logger import get_logger
16
+ from fast_agent.mcp.helpers.content_helpers import text_content
17
+
18
+ if TYPE_CHECKING:
19
+ from acp import AgentSideConnection
20
+
21
+ from fast_agent.mcp.tool_execution_handler import ToolExecutionHandler
22
+ from fast_agent.mcp.tool_permission_handler import ToolPermissionHandler
23
+
24
+ logger = get_logger(__name__)
25
+
26
+
27
+ class ACPTerminalRuntime:
28
+ """
29
+ Provides command execution through ACP terminal support.
30
+
31
+ This runtime implements the "execute" tool by delegating to the ACP client's
32
+ terminal capabilities. The flow is:
33
+ 1. terminal/create - Start command execution
34
+ 2. terminal/wait_for_exit - Wait for completion
35
+ 3. terminal/output - Retrieve output
36
+ 4. terminal/release - Clean up resources
37
+
38
+ The client (e.g., Zed editor) handles displaying the terminal UI to the user.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ connection: "AgentSideConnection",
44
+ session_id: str,
45
+ activation_reason: str,
46
+ logger_instance=None,
47
+ timeout_seconds: int = 90,
48
+ tool_handler: "ToolExecutionHandler | None" = None,
49
+ default_output_byte_limit: int = DEFAULT_TERMINAL_OUTPUT_BYTE_LIMIT,
50
+ permission_handler: "ToolPermissionHandler | None" = None,
51
+ ):
52
+ """
53
+ Initialize the ACP terminal runtime.
54
+
55
+ Args:
56
+ connection: The ACP connection to use for terminal operations
57
+ session_id: The ACP session ID for this runtime
58
+ activation_reason: Human-readable reason for activation
59
+ logger_instance: Optional logger instance
60
+ timeout_seconds: Default timeout for command execution
61
+ tool_handler: Optional tool execution handler for telemetry
62
+ permission_handler: Optional permission handler for tool execution authorization
63
+ """
64
+ self.connection = connection
65
+ self.session_id = session_id
66
+ self.activation_reason = activation_reason
67
+ self.logger = logger_instance or logger
68
+ self.timeout_seconds = timeout_seconds
69
+ self._tool_handler = tool_handler
70
+ self._default_output_byte_limit = (
71
+ default_output_byte_limit or DEFAULT_TERMINAL_OUTPUT_BYTE_LIMIT
72
+ )
73
+ self._permission_handler = permission_handler
74
+
75
+ # Tool definition for LLM
76
+ self._tool = Tool(
77
+ name="execute",
78
+ description="Execute a shell command.",
79
+ inputSchema={
80
+ "type": "object",
81
+ "properties": {
82
+ "command": {
83
+ "type": "string",
84
+ "description": "The shell command to execute. Do not include shell "
85
+ "prefix (bash -c, etc.).",
86
+ },
87
+ "args": {
88
+ "type": "array",
89
+ "items": {"type": "string"},
90
+ "description": "Optional array of command arguments (alternative to including in command string).",
91
+ },
92
+ "env": {
93
+ "type": "object",
94
+ "description": "Optional environment variables as key-value pairs.",
95
+ "additionalProperties": {"type": "string"},
96
+ },
97
+ "cwd": {
98
+ "type": "string",
99
+ "description": "Optional absolute path for working directory.",
100
+ },
101
+ # Do not allow model to handle this for the moment.
102
+ # "outputByteLimit": {
103
+ # "type": "integer",
104
+ # "description": "Maximum bytes of output to retain. (prevents unbounded buffers).",
105
+ # },
106
+ },
107
+ "required": ["command"],
108
+ "additionalProperties": False,
109
+ },
110
+ )
111
+
112
+ self.logger.info(
113
+ "ACPTerminalRuntime initialized",
114
+ session_id=session_id,
115
+ reason=activation_reason,
116
+ timeout=timeout_seconds,
117
+ )
118
+
119
+ @property
120
+ def tool(self) -> Tool:
121
+ """Get the execute tool definition."""
122
+ return self._tool
123
+
124
+ async def execute(
125
+ self, arguments: dict[str, Any], tool_use_id: str | None = None
126
+ ) -> CallToolResult:
127
+ """
128
+ Execute a command using ACP terminal support.
129
+
130
+ Args:
131
+ arguments: Tool arguments containing 'command' key
132
+ tool_use_id: LLM's tool use ID (for matching with stream events)
133
+
134
+ Returns:
135
+ CallToolResult with command output and exit status
136
+ """
137
+ # Validate arguments
138
+ if not isinstance(arguments, dict):
139
+ return CallToolResult(
140
+ content=[text_content("Error: arguments must be a dict")],
141
+ isError=True,
142
+ )
143
+
144
+ command = arguments.get("command")
145
+ if not command or not isinstance(command, str):
146
+ return CallToolResult(
147
+ content=[
148
+ text_content("Error: 'command' argument is required and must be a string")
149
+ ],
150
+ isError=True,
151
+ )
152
+
153
+ self.logger.info(
154
+ "Executing command via ACP terminal",
155
+ session_id=self.session_id,
156
+ command=command[:100], # Log first 100 chars
157
+ )
158
+
159
+ # Check permission before execution
160
+ if self._permission_handler:
161
+ try:
162
+ permission_result = await self._permission_handler.check_permission(
163
+ tool_name="execute",
164
+ server_name="acp_terminal",
165
+ arguments=arguments,
166
+ tool_use_id=tool_use_id,
167
+ )
168
+ if not permission_result.allowed:
169
+ error_msg = permission_result.error_message or (
170
+ "Permission denied for terminal execution"
171
+ )
172
+ self.logger.info(
173
+ "Terminal execution denied by permission handler",
174
+ data={
175
+ "command": command[:100],
176
+ "cancelled": permission_result.is_cancelled,
177
+ },
178
+ )
179
+ return CallToolResult(
180
+ content=[text_content(error_msg)],
181
+ isError=True,
182
+ )
183
+ except Exception as e:
184
+ self.logger.error(f"Error checking terminal permission: {e}", exc_info=True)
185
+ # Fail-safe: deny on permission check error
186
+ return CallToolResult(
187
+ content=[text_content(f"Permission check failed: {e}")],
188
+ isError=True,
189
+ )
190
+
191
+ # Notify tool handler that execution is starting
192
+ tool_call_id = None
193
+ if self._tool_handler:
194
+ try:
195
+ tool_call_id = await self._tool_handler.on_tool_start(
196
+ "execute", "acp_terminal", arguments, tool_use_id
197
+ )
198
+ except Exception as e:
199
+ self.logger.error(f"Error in tool start handler: {e}", exc_info=True)
200
+
201
+ terminal_id = None # Will be set by client in terminal/create response
202
+
203
+ try:
204
+ # Step 1: Create terminal and start command execution
205
+ # NOTE: Client creates and returns the terminal ID, we don't generate it
206
+ self.logger.debug("Creating terminal")
207
+
208
+ # Build create params per ACP spec (sessionId, command, args, env, cwd, outputByteLimit)
209
+ # Extract optional parameters from arguments
210
+ create_params: dict[str, Any] = {
211
+ "sessionId": self.session_id,
212
+ "command": command,
213
+ }
214
+
215
+ # Add optional parameters if provided
216
+ if args := arguments.get("args"):
217
+ create_params["args"] = args
218
+ if env := arguments.get("env"):
219
+ # Transform env from object format (LLM-friendly) to ACP array format
220
+ # Input: {"PATH": "/usr/bin", "HOME": "/home/user"}
221
+ # Output: [{"name": "PATH", "value": "/usr/bin"}, {"name": "HOME", "value": "/home/user"}]
222
+ if isinstance(env, dict):
223
+ create_params["env"] = [
224
+ {"name": name, "value": value} for name, value in env.items()
225
+ ]
226
+ else:
227
+ # If already in array format, pass through
228
+ create_params["env"] = env
229
+ if cwd := arguments.get("cwd"):
230
+ create_params["cwd"] = cwd
231
+ if "outputByteLimit" in arguments and arguments["outputByteLimit"] is not None:
232
+ create_params["outputByteLimit"] = arguments["outputByteLimit"]
233
+ else:
234
+ create_params["outputByteLimit"] = self._default_output_byte_limit
235
+
236
+ create_result = await self.connection._conn.send_request(
237
+ "terminal/create", create_params
238
+ )
239
+ terminal_id = create_result.get("terminalId")
240
+
241
+ if not terminal_id:
242
+ return CallToolResult(
243
+ content=[text_content("Error: Client did not return terminal ID")],
244
+ isError=True,
245
+ )
246
+
247
+ self.logger.debug(f"Terminal created with ID: {terminal_id}")
248
+
249
+ # Step 2: Wait for command to complete (with timeout)
250
+ self.logger.debug(f"Waiting for terminal {terminal_id} to exit")
251
+ try:
252
+ wait_params = {"sessionId": self.session_id, "terminalId": terminal_id}
253
+ wait_result = await asyncio.wait_for(
254
+ self.connection._conn.send_request("terminal/wait_for_exit", wait_params),
255
+ timeout=self.timeout_seconds,
256
+ )
257
+ exit_code = wait_result.get("exitCode", -1)
258
+ signal = wait_result.get("signal")
259
+ except asyncio.TimeoutError:
260
+ self.logger.warning(
261
+ f"Terminal {terminal_id} timed out after {self.timeout_seconds}s"
262
+ )
263
+ # Kill the terminal
264
+ try:
265
+ kill_params = {"sessionId": self.session_id, "terminalId": terminal_id}
266
+ await self.connection._conn.send_request("terminal/kill", kill_params)
267
+ except Exception as kill_error:
268
+ self.logger.error(f"Error killing terminal: {kill_error}")
269
+
270
+ # Still try to get output
271
+ output_params = {"sessionId": self.session_id, "terminalId": terminal_id}
272
+ output_result = await self.connection._conn.send_request(
273
+ "terminal/output", output_params
274
+ )
275
+ output_text = output_result.get("output", "")
276
+
277
+ # Release terminal
278
+ await self._release_terminal(terminal_id)
279
+
280
+ timeout_result = CallToolResult(
281
+ content=[
282
+ text_content(
283
+ f"Command timed out after {self.timeout_seconds}s\n\n"
284
+ f"Output so far:\n{output_text}"
285
+ )
286
+ ],
287
+ isError=True,
288
+ )
289
+
290
+ # Notify tool handler of timeout error
291
+ if self._tool_handler and tool_call_id:
292
+ try:
293
+ await self._tool_handler.on_tool_complete(
294
+ tool_call_id,
295
+ False,
296
+ None,
297
+ f"Command timed out after {self.timeout_seconds}s",
298
+ )
299
+ except Exception as e:
300
+ self.logger.error(f"Error in tool complete handler: {e}", exc_info=True)
301
+
302
+ return timeout_result
303
+
304
+ # Step 3: Get the output
305
+ self.logger.debug(f"Retrieving output from terminal {terminal_id}")
306
+ output_params = {"sessionId": self.session_id, "terminalId": terminal_id}
307
+ output_result = await self.connection._conn.send_request(
308
+ "terminal/output", output_params
309
+ )
310
+ output_text = output_result.get("output", "")
311
+ truncated = output_result.get("truncated", False)
312
+
313
+ # Step 4: Release the terminal
314
+ await self._release_terminal(terminal_id)
315
+
316
+ # Format result
317
+ is_error = exit_code != 0
318
+ result_text = output_text
319
+
320
+ if truncated:
321
+ result_text = f"[Output truncated]\n{result_text}"
322
+
323
+ if signal:
324
+ result_text = f"{result_text}\n\n[Terminated by signal: {signal}]"
325
+
326
+ result_text = f"{result_text}\n\n[Exit code: {exit_code}]"
327
+
328
+ self.logger.info(
329
+ "Terminal execution completed",
330
+ terminal_id=terminal_id,
331
+ exit_code=exit_code,
332
+ output_length=len(output_text),
333
+ truncated=truncated,
334
+ )
335
+
336
+ result = CallToolResult(
337
+ content=[text_content(result_text)],
338
+ isError=is_error,
339
+ )
340
+
341
+ # Notify tool handler of completion
342
+ if self._tool_handler and tool_call_id:
343
+ try:
344
+ await self._tool_handler.on_tool_complete(
345
+ tool_call_id,
346
+ not is_error,
347
+ result.content if not is_error else None,
348
+ result_text if is_error else None,
349
+ )
350
+ except Exception as e:
351
+ self.logger.error(f"Error in tool complete handler: {e}", exc_info=True)
352
+
353
+ return result
354
+
355
+ except Exception as e:
356
+ self.logger.error(
357
+ f"Error executing terminal command: {e}",
358
+ terminal_id=terminal_id,
359
+ exc_info=True,
360
+ )
361
+ # Try to clean up if we have a terminal ID
362
+ if terminal_id:
363
+ try:
364
+ await self._release_terminal(terminal_id)
365
+ except Exception:
366
+ pass # Best effort cleanup
367
+
368
+ # Notify tool handler of error
369
+ if self._tool_handler and tool_call_id:
370
+ try:
371
+ await self._tool_handler.on_tool_complete(tool_call_id, False, None, str(e))
372
+ except Exception as handler_error:
373
+ self.logger.error(
374
+ f"Error in tool complete handler: {handler_error}", exc_info=True
375
+ )
376
+
377
+ return CallToolResult(
378
+ content=[text_content(f"Terminal execution error: {e}")],
379
+ isError=True,
380
+ )
381
+
382
+ async def _release_terminal(self, terminal_id: str) -> None:
383
+ """
384
+ Release a terminal (cleanup).
385
+
386
+ Args:
387
+ terminal_id: The terminal ID to release
388
+ """
389
+ try:
390
+ self.logger.debug(f"Releasing terminal {terminal_id}")
391
+ release_params = {"sessionId": self.session_id, "terminalId": terminal_id}
392
+ await self.connection._conn.send_request("terminal/release", release_params)
393
+ except Exception as e:
394
+ self.logger.error(f"Error releasing terminal {terminal_id}: {e}")
395
+
396
+ def metadata(self) -> dict[str, Any]:
397
+ """
398
+ Get metadata about this runtime for display/logging.
399
+
400
+ Returns:
401
+ Dict with runtime information
402
+ """
403
+ return {
404
+ "type": "acp_terminal",
405
+ "session_id": self.session_id,
406
+ "activation_reason": self.activation_reason,
407
+ "timeout_seconds": self.timeout_seconds,
408
+ }
@@ -0,0 +1,125 @@
1
+ """
2
+ ACP Tool Permission Adapter
3
+
4
+ Bridges ACPToolPermissionManager to the MCP ToolPermissionHandler protocol,
5
+ allowing ACP permission checking to be injected into the MCP aggregator.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from fast_agent.acp.permission_store import PermissionStore
12
+ from fast_agent.acp.tool_permissions import ACPToolPermissionManager
13
+ from fast_agent.mcp.common import create_namespaced_name
14
+ from fast_agent.mcp.tool_permission_handler import ToolPermissionHandler, ToolPermissionResult
15
+
16
+ if TYPE_CHECKING:
17
+ from acp import AgentSideConnection
18
+
19
+ from fast_agent.acp.tool_progress import ACPToolProgressManager
20
+
21
+
22
+ class ACPToolPermissionAdapter(ToolPermissionHandler):
23
+ """
24
+ Adapts ACPToolPermissionManager to implement the ToolPermissionHandler protocol.
25
+
26
+ This adapter translates between the ACP-specific permission types and the
27
+ generic MCP permission handler interface.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ connection: "AgentSideConnection",
33
+ session_id: str,
34
+ store: PermissionStore | None = None,
35
+ cwd: str | Path | None = None,
36
+ tool_handler: "ACPToolProgressManager | None" = None,
37
+ ) -> None:
38
+ """
39
+ Initialize the adapter.
40
+
41
+ Args:
42
+ connection: The ACP connection to send permission requests on
43
+ session_id: The ACP session ID
44
+ store: Optional PermissionStore for persistence
45
+ cwd: Working directory for the store (only used if store not provided)
46
+ tool_handler: Optional tool progress manager for toolCallId lookup
47
+ """
48
+ self._tool_handler = tool_handler
49
+ self._manager = ACPToolPermissionManager(
50
+ connection=connection,
51
+ session_id=session_id,
52
+ store=store,
53
+ cwd=cwd,
54
+ )
55
+
56
+ @property
57
+ def manager(self) -> ACPToolPermissionManager:
58
+ """Access the underlying permission manager."""
59
+ return self._manager
60
+
61
+ async def check_permission(
62
+ self,
63
+ tool_name: str,
64
+ server_name: str,
65
+ arguments: dict[str, Any] | None = None,
66
+ tool_use_id: str | None = None,
67
+ ) -> ToolPermissionResult:
68
+ """
69
+ Check if tool execution is permitted.
70
+
71
+ Delegates to ACPToolPermissionManager and converts the result
72
+ to ToolPermissionResult.
73
+
74
+ Args:
75
+ tool_name: Name of the tool to execute
76
+ server_name: Name of the MCP server providing the tool
77
+ arguments: Tool arguments
78
+ tool_use_id: LLM's tool use ID
79
+
80
+ Returns:
81
+ ToolPermissionResult indicating whether execution is allowed
82
+ """
83
+ # Look up the ACP toolCallId if a streaming notification was already sent
84
+ # This ensures the permission request references the same tool call
85
+ tool_call_id = tool_use_id
86
+ if tool_use_id and self._tool_handler:
87
+ acp_tool_call_id = await self._tool_handler.get_tool_call_id_for_tool_use(tool_use_id)
88
+ if acp_tool_call_id:
89
+ tool_call_id = acp_tool_call_id
90
+
91
+ result = await self._manager.check_permission(
92
+ tool_name=tool_name,
93
+ server_name=server_name,
94
+ arguments=arguments,
95
+ tool_call_id=tool_call_id,
96
+ )
97
+
98
+ namespaced_tool_name = create_namespaced_name(server_name, tool_name)
99
+
100
+ # Convert PermissionResult to ToolPermissionResult
101
+ if result.is_cancelled:
102
+ return ToolPermissionResult.cancelled()
103
+ elif result.allowed:
104
+ return ToolPermissionResult(allowed=True, remember=result.remember)
105
+ else:
106
+ # Distinguish between one-time and persistent rejection for clearer UX
107
+ if result.remember:
108
+ error_message = (
109
+ f"The user has permanently declined permission to use this tool: "
110
+ f"{namespaced_tool_name}"
111
+ )
112
+ else:
113
+ error_message = (
114
+ f"The user has declined permission to use this tool: {namespaced_tool_name}"
115
+ )
116
+
117
+ return ToolPermissionResult(
118
+ allowed=False,
119
+ remember=result.remember,
120
+ error_message=error_message,
121
+ )
122
+
123
+ async def clear_session_cache(self) -> None:
124
+ """Clear the session-level permission cache."""
125
+ await self._manager.clear_session_cache()