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.
- fast_agent/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- 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
|
+
]
|