fast-agent-mcp 0.3.14__py3-none-any.whl → 0.3.16__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.
Potentially problematic release.
This version of fast-agent-mcp might be problematic. Click here for more details.
- fast_agent/__init__.py +2 -0
- fast_agent/agents/agent_types.py +5 -0
- fast_agent/agents/llm_agent.py +52 -4
- fast_agent/agents/llm_decorator.py +6 -0
- fast_agent/agents/mcp_agent.py +137 -13
- fast_agent/agents/tool_agent.py +33 -19
- fast_agent/agents/workflow/router_agent.py +2 -1
- fast_agent/cli/__main__.py +35 -0
- fast_agent/cli/commands/check_config.py +90 -2
- fast_agent/cli/commands/go.py +100 -36
- fast_agent/cli/constants.py +13 -1
- fast_agent/cli/main.py +1 -0
- fast_agent/config.py +41 -12
- fast_agent/constants.py +8 -0
- fast_agent/context.py +24 -15
- fast_agent/core/direct_decorators.py +9 -0
- fast_agent/core/fastagent.py +115 -2
- fast_agent/core/logging/listeners.py +8 -0
- fast_agent/core/validation.py +31 -33
- fast_agent/human_input/form_fields.py +4 -1
- fast_agent/interfaces.py +12 -1
- fast_agent/llm/fastagent_llm.py +76 -0
- fast_agent/llm/memory.py +26 -1
- fast_agent/llm/model_database.py +2 -2
- fast_agent/llm/model_factory.py +4 -1
- fast_agent/llm/provider/anthropic/llm_anthropic.py +112 -0
- fast_agent/llm/provider/openai/llm_openai.py +184 -18
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/mcp/prompt_message_extended.py +2 -2
- fast_agent/resources/setup/agent.py +2 -0
- fast_agent/resources/setup/fastagent.config.yaml +11 -4
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +200 -0
- fast_agent/tools/shell_runtime.py +404 -0
- fast_agent/ui/console_display.py +925 -73
- fast_agent/ui/elicitation_form.py +98 -24
- fast_agent/ui/elicitation_style.py +2 -2
- fast_agent/ui/enhanced_prompt.py +128 -26
- fast_agent/ui/history_display.py +20 -5
- fast_agent/ui/interactive_prompt.py +108 -3
- fast_agent/ui/markdown_truncator.py +942 -0
- fast_agent/ui/mcp_display.py +2 -2
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/METADATA +9 -7
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/RECORD +49 -42
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/licenses/LICENSE +0 -0
|
@@ -13,6 +13,7 @@ from rich.text import Text
|
|
|
13
13
|
|
|
14
14
|
from fast_agent.llm.provider_key_manager import API_KEY_HINT_TEXT, ProviderKeyManager
|
|
15
15
|
from fast_agent.llm.provider_types import Provider
|
|
16
|
+
from fast_agent.skills import SkillRegistry
|
|
16
17
|
from fast_agent.ui.console import console
|
|
17
18
|
|
|
18
19
|
app = typer.Typer(
|
|
@@ -156,6 +157,7 @@ def get_config_summary(config_path: Optional[Path]) -> dict:
|
|
|
156
157
|
"logger": {
|
|
157
158
|
"level": default_settings.logger.level,
|
|
158
159
|
"type": default_settings.logger.type,
|
|
160
|
+
"streaming": default_settings.logger.streaming,
|
|
159
161
|
"progress_display": default_settings.logger.progress_display,
|
|
160
162
|
"show_chat": default_settings.logger.show_chat,
|
|
161
163
|
"show_tools": default_settings.logger.show_tools,
|
|
@@ -168,6 +170,7 @@ def get_config_summary(config_path: Optional[Path]) -> dict:
|
|
|
168
170
|
"step_seconds": default_settings.mcp_timeline.step_seconds,
|
|
169
171
|
},
|
|
170
172
|
"mcp_servers": [],
|
|
173
|
+
"skills_directory": None,
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
if not config_path:
|
|
@@ -198,6 +201,7 @@ def get_config_summary(config_path: Optional[Path]) -> dict:
|
|
|
198
201
|
result["logger"] = {
|
|
199
202
|
"level": logger_config.get("level", default_settings.logger.level),
|
|
200
203
|
"type": logger_config.get("type", default_settings.logger.type),
|
|
204
|
+
"streaming": logger_config.get("streaming", default_settings.logger.streaming),
|
|
201
205
|
"progress_display": logger_config.get(
|
|
202
206
|
"progress_display", default_settings.logger.progress_display
|
|
203
207
|
),
|
|
@@ -221,8 +225,7 @@ def get_config_summary(config_path: Optional[Path]) -> dict:
|
|
|
221
225
|
timeline_override = MCPTimelineSettings(**(config.get("mcp_timeline") or {}))
|
|
222
226
|
except Exception as exc: # pragma: no cover - defensive
|
|
223
227
|
console.print(
|
|
224
|
-
"[yellow]Warning:[/yellow] Invalid mcp_timeline configuration; "
|
|
225
|
-
"using defaults."
|
|
228
|
+
"[yellow]Warning:[/yellow] Invalid mcp_timeline configuration; using defaults."
|
|
226
229
|
)
|
|
227
230
|
console.print(f"[yellow]Details:[/yellow] {exc}")
|
|
228
231
|
else:
|
|
@@ -277,6 +280,13 @@ def get_config_summary(config_path: Optional[Path]) -> dict:
|
|
|
277
280
|
|
|
278
281
|
result["mcp_servers"].append(server_info)
|
|
279
282
|
|
|
283
|
+
# Skills directory override
|
|
284
|
+
skills_cfg = config.get("skills") if isinstance(config, dict) else None
|
|
285
|
+
if isinstance(skills_cfg, dict):
|
|
286
|
+
directory_value = skills_cfg.get("directory")
|
|
287
|
+
if isinstance(directory_value, str) and directory_value.strip():
|
|
288
|
+
result["skills_directory"] = directory_value.strip()
|
|
289
|
+
|
|
280
290
|
except Exception as e:
|
|
281
291
|
# File exists but has parse errors
|
|
282
292
|
result["status"] = "error"
|
|
@@ -387,6 +397,18 @@ def show_check_summary() -> None:
|
|
|
387
397
|
|
|
388
398
|
console.print(env_table)
|
|
389
399
|
|
|
400
|
+
def _relative_path(path: Path) -> str:
|
|
401
|
+
try:
|
|
402
|
+
return str(path.relative_to(cwd))
|
|
403
|
+
except ValueError:
|
|
404
|
+
return str(path)
|
|
405
|
+
|
|
406
|
+
skills_override = config_summary.get("skills_directory")
|
|
407
|
+
override_directory = Path(skills_override).expanduser() if skills_override else None
|
|
408
|
+
skills_registry = SkillRegistry(base_dir=cwd, override_directory=override_directory)
|
|
409
|
+
skills_dir = skills_registry.directory
|
|
410
|
+
skills_manifests, skill_errors = skills_registry.load_manifests_with_errors()
|
|
411
|
+
|
|
390
412
|
# Logger Settings panel with two-column layout
|
|
391
413
|
logger = config_summary.get("logger", {})
|
|
392
414
|
logger_table = Table(show_header=True, box=None)
|
|
@@ -432,6 +454,8 @@ def show_check_summary() -> None:
|
|
|
432
454
|
("Log Level", logger.get("level", "warning (default)")),
|
|
433
455
|
("Log Type", logger.get("type", "file (default)")),
|
|
434
456
|
("MCP-UI", mcp_ui_display),
|
|
457
|
+
("Streaming Mode", f"[green]{logger.get('streaming', 'markdown')}[/green]"),
|
|
458
|
+
("Streaming Display", bool_to_symbol(logger.get("streaming_display", True))),
|
|
435
459
|
("Progress Display", bool_to_symbol(logger.get("progress_display", True))),
|
|
436
460
|
("Show Chat", bool_to_symbol(logger.get("show_chat", True))),
|
|
437
461
|
("Show Tools", bool_to_symbol(logger.get("show_tools", True))),
|
|
@@ -610,6 +634,70 @@ def show_check_summary() -> None:
|
|
|
610
634
|
_print_section_header("MCP Servers", color="blue")
|
|
611
635
|
console.print(servers_table)
|
|
612
636
|
|
|
637
|
+
_print_section_header("Agent Skills", color="blue")
|
|
638
|
+
if skills_dir:
|
|
639
|
+
console.print(f"Directory: [green]{_relative_path(skills_dir)}[/green]")
|
|
640
|
+
|
|
641
|
+
if skills_manifests or skill_errors:
|
|
642
|
+
skills_table = Table(show_header=True, box=None)
|
|
643
|
+
skills_table.add_column("Name", style="cyan", header_style="bold bright_white")
|
|
644
|
+
skills_table.add_column("Description", style="white", header_style="bold bright_white")
|
|
645
|
+
skills_table.add_column("Source", style="dim", header_style="bold bright_white")
|
|
646
|
+
skills_table.add_column("Status", style="green", header_style="bold bright_white")
|
|
647
|
+
|
|
648
|
+
def _truncate(text: str, length: int = 70) -> str:
|
|
649
|
+
if len(text) <= length:
|
|
650
|
+
return text
|
|
651
|
+
return text[: length - 3] + "..."
|
|
652
|
+
|
|
653
|
+
for manifest in skills_manifests:
|
|
654
|
+
try:
|
|
655
|
+
relative_source = manifest.path.parent.relative_to(skills_dir)
|
|
656
|
+
source_display = str(relative_source) if relative_source != Path(".") else "."
|
|
657
|
+
except ValueError:
|
|
658
|
+
source_display = _relative_path(manifest.path.parent)
|
|
659
|
+
|
|
660
|
+
skills_table.add_row(
|
|
661
|
+
manifest.name,
|
|
662
|
+
_truncate(manifest.description or ""),
|
|
663
|
+
source_display,
|
|
664
|
+
"[green]ok[/green]",
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
for error in skill_errors:
|
|
668
|
+
error_path_str = error.get("path", "")
|
|
669
|
+
source_display = "[dim]n/a[/dim]"
|
|
670
|
+
if error_path_str:
|
|
671
|
+
error_path = Path(error_path_str)
|
|
672
|
+
try:
|
|
673
|
+
relative_error = error_path.parent.relative_to(skills_dir)
|
|
674
|
+
source_display = str(relative_error) if relative_error != Path(".") else "."
|
|
675
|
+
except ValueError:
|
|
676
|
+
source_display = _relative_path(error_path.parent)
|
|
677
|
+
message = error.get("error", "Failed to parse skill manifest")
|
|
678
|
+
skills_table.add_row(
|
|
679
|
+
"[red]—[/red]",
|
|
680
|
+
"[red]n/a[/red]",
|
|
681
|
+
source_display,
|
|
682
|
+
f"[red]{_truncate(message, 60)}[/red]",
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
console.print(skills_table)
|
|
686
|
+
else:
|
|
687
|
+
console.print("[yellow]No skills found in the directory[/yellow]")
|
|
688
|
+
else:
|
|
689
|
+
if skills_registry.override_failed and override_directory:
|
|
690
|
+
console.print(
|
|
691
|
+
f"[red]Override directory not found:[/red] {_relative_path(override_directory)}"
|
|
692
|
+
)
|
|
693
|
+
console.print(
|
|
694
|
+
"[yellow]Default folders were not loaded because the override failed[/yellow]"
|
|
695
|
+
)
|
|
696
|
+
else:
|
|
697
|
+
console.print(
|
|
698
|
+
"[dim]Agent Skills not configured. Go to https://fast-agent.ai/agents/skills/[/dim]"
|
|
699
|
+
)
|
|
700
|
+
|
|
613
701
|
# Show help tips
|
|
614
702
|
if config_status == "not_found" or secrets_status == "not_found":
|
|
615
703
|
console.print("\n[bold]Setup Tips:[/bold]")
|
fast_agent/cli/commands/go.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Run an interactive agent directly from the command line."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import logging
|
|
4
5
|
import shlex
|
|
5
6
|
import sys
|
|
6
|
-
from
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
import typer
|
|
9
10
|
|
|
@@ -11,6 +12,7 @@ from fast_agent import FastAgent
|
|
|
11
12
|
from fast_agent.agents.llm_agent import LlmAgent
|
|
12
13
|
from fast_agent.cli.commands.server_helpers import add_servers_to_config, generate_server_name
|
|
13
14
|
from fast_agent.cli.commands.url_parser import generate_server_configs, parse_server_urls
|
|
15
|
+
from fast_agent.constants import DEFAULT_AGENT_INSTRUCTION
|
|
14
16
|
from fast_agent.ui.console_display import ConsoleDisplay
|
|
15
17
|
|
|
16
18
|
app = typer.Typer(
|
|
@@ -18,28 +20,60 @@ app = typer.Typer(
|
|
|
18
20
|
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
19
21
|
)
|
|
20
22
|
|
|
21
|
-
default_instruction =
|
|
23
|
+
default_instruction = DEFAULT_AGENT_INSTRUCTION
|
|
22
24
|
|
|
23
|
-
{{serverInstructions}}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
def _set_asyncio_exception_handler(loop: asyncio.AbstractEventLoop) -> None:
|
|
27
|
+
"""Attach a detailed exception handler to the provided event loop."""
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("fast_agent.asyncio")
|
|
30
|
+
|
|
31
|
+
def _handler(_loop: asyncio.AbstractEventLoop, context: dict) -> None:
|
|
32
|
+
message = context.get("message", "(no message)")
|
|
33
|
+
task = context.get("task")
|
|
34
|
+
future = context.get("future")
|
|
35
|
+
handle = context.get("handle")
|
|
36
|
+
source_traceback = context.get("source_traceback")
|
|
37
|
+
exception = context.get("exception")
|
|
38
|
+
|
|
39
|
+
details = {
|
|
40
|
+
"message": message,
|
|
41
|
+
"task": repr(task) if task else None,
|
|
42
|
+
"future": repr(future) if future else None,
|
|
43
|
+
"handle": repr(handle) if handle else None,
|
|
44
|
+
"source_traceback": [str(frame) for frame in source_traceback]
|
|
45
|
+
if source_traceback
|
|
46
|
+
else None,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
logger.error("Unhandled asyncio error: %s", message)
|
|
50
|
+
logger.error("Asyncio context: %s", details)
|
|
51
|
+
|
|
52
|
+
if exception:
|
|
53
|
+
logger.exception("Asyncio exception", exc_info=exception)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
loop.set_exception_handler(_handler)
|
|
57
|
+
except Exception:
|
|
58
|
+
logger = logging.getLogger("fast_agent.asyncio")
|
|
59
|
+
logger.exception("Failed to set asyncio exception handler")
|
|
26
60
|
|
|
27
61
|
|
|
28
62
|
async def _run_agent(
|
|
29
63
|
name: str = "fast-agent cli",
|
|
30
64
|
instruction: str = default_instruction,
|
|
31
|
-
config_path:
|
|
32
|
-
server_list:
|
|
33
|
-
model:
|
|
34
|
-
message:
|
|
35
|
-
prompt_file:
|
|
36
|
-
url_servers:
|
|
37
|
-
stdio_servers:
|
|
38
|
-
agent_name:
|
|
65
|
+
config_path: str | None = None,
|
|
66
|
+
server_list: list[str] | None = None,
|
|
67
|
+
model: str | None = None,
|
|
68
|
+
message: str | None = None,
|
|
69
|
+
prompt_file: str | None = None,
|
|
70
|
+
url_servers: dict[str, dict[str, str]] | None = None,
|
|
71
|
+
stdio_servers: dict[str, dict[str, str]] | None = None,
|
|
72
|
+
agent_name: str | None = "agent",
|
|
73
|
+
skills_directory: Path | None = None,
|
|
74
|
+
shell_runtime: bool = False,
|
|
39
75
|
) -> None:
|
|
40
76
|
"""Async implementation to run an interactive agent."""
|
|
41
|
-
from pathlib import Path
|
|
42
|
-
|
|
43
77
|
from fast_agent.mcp.prompts.prompt_load import load_prompt
|
|
44
78
|
|
|
45
79
|
# Create the FastAgent instance
|
|
@@ -50,9 +84,15 @@ async def _run_agent(
|
|
|
50
84
|
"ignore_unknown_args": True,
|
|
51
85
|
"parse_cli_args": False, # Don't parse CLI args, we're handling it ourselves
|
|
52
86
|
}
|
|
87
|
+
if skills_directory is not None:
|
|
88
|
+
fast_kwargs["skills_directory"] = skills_directory
|
|
53
89
|
|
|
54
90
|
fast = FastAgent(**fast_kwargs)
|
|
55
91
|
|
|
92
|
+
if shell_runtime:
|
|
93
|
+
await fast.app.initialize()
|
|
94
|
+
setattr(fast.app.context, "shell_runtime", True)
|
|
95
|
+
|
|
56
96
|
# Add all dynamic servers to the configuration
|
|
57
97
|
await add_servers_to_config(fast, url_servers)
|
|
58
98
|
await add_servers_to_config(fast, stdio_servers)
|
|
@@ -149,15 +189,17 @@ async def _run_agent(
|
|
|
149
189
|
def run_async_agent(
|
|
150
190
|
name: str,
|
|
151
191
|
instruction: str,
|
|
152
|
-
config_path:
|
|
153
|
-
servers:
|
|
154
|
-
urls:
|
|
155
|
-
auth:
|
|
156
|
-
model:
|
|
157
|
-
message:
|
|
158
|
-
prompt_file:
|
|
159
|
-
stdio_commands:
|
|
160
|
-
agent_name:
|
|
192
|
+
config_path: str | None = None,
|
|
193
|
+
servers: str | None = None,
|
|
194
|
+
urls: str | None = None,
|
|
195
|
+
auth: str | None = None,
|
|
196
|
+
model: str | None = None,
|
|
197
|
+
message: str | None = None,
|
|
198
|
+
prompt_file: str | None = None,
|
|
199
|
+
stdio_commands: list[str] | None = None,
|
|
200
|
+
agent_name: str | None = None,
|
|
201
|
+
skills_directory: Path | None = None,
|
|
202
|
+
shell_enabled: bool = False,
|
|
161
203
|
):
|
|
162
204
|
"""Run the async agent function with proper loop handling."""
|
|
163
205
|
server_list = servers.split(",") if servers else None
|
|
@@ -240,10 +282,12 @@ def run_async_agent(
|
|
|
240
282
|
# Instead, create a new loop
|
|
241
283
|
loop = asyncio.new_event_loop()
|
|
242
284
|
asyncio.set_event_loop(loop)
|
|
285
|
+
_set_asyncio_exception_handler(loop)
|
|
243
286
|
except RuntimeError:
|
|
244
287
|
# No event loop exists, so we'll create one
|
|
245
288
|
loop = asyncio.new_event_loop()
|
|
246
289
|
asyncio.set_event_loop(loop)
|
|
290
|
+
_set_asyncio_exception_handler(loop)
|
|
247
291
|
|
|
248
292
|
try:
|
|
249
293
|
loop.run_until_complete(
|
|
@@ -258,6 +302,8 @@ def run_async_agent(
|
|
|
258
302
|
url_servers=url_servers,
|
|
259
303
|
stdio_servers=stdio_servers,
|
|
260
304
|
agent_name=agent_name,
|
|
305
|
+
skills_directory=skills_directory,
|
|
306
|
+
shell_runtime=shell_enabled,
|
|
261
307
|
)
|
|
262
308
|
)
|
|
263
309
|
finally:
|
|
@@ -280,39 +326,49 @@ def run_async_agent(
|
|
|
280
326
|
def go(
|
|
281
327
|
ctx: typer.Context,
|
|
282
328
|
name: str = typer.Option("fast-agent", "--name", help="Name for the agent"),
|
|
283
|
-
instruction:
|
|
329
|
+
instruction: str | None = typer.Option(
|
|
284
330
|
None, "--instruction", "-i", help="Path to file or URL containing instruction for the agent"
|
|
285
331
|
),
|
|
286
|
-
config_path:
|
|
287
|
-
|
|
288
|
-
),
|
|
289
|
-
servers: Optional[str] = typer.Option(
|
|
332
|
+
config_path: str | None = typer.Option(None, "--config-path", "-c", help="Path to config file"),
|
|
333
|
+
servers: str | None = typer.Option(
|
|
290
334
|
None, "--servers", help="Comma-separated list of server names to enable from config"
|
|
291
335
|
),
|
|
292
|
-
urls:
|
|
336
|
+
urls: str | None = typer.Option(
|
|
293
337
|
None, "--url", help="Comma-separated list of HTTP/SSE URLs to connect to"
|
|
294
338
|
),
|
|
295
|
-
auth:
|
|
339
|
+
auth: str | None = typer.Option(
|
|
296
340
|
None, "--auth", help="Bearer token for authorization with URL-based servers"
|
|
297
341
|
),
|
|
298
|
-
model:
|
|
342
|
+
model: str | None = typer.Option(
|
|
299
343
|
None, "--model", "--models", help="Override the default model (e.g., haiku, sonnet, gpt-4)"
|
|
300
344
|
),
|
|
301
|
-
message:
|
|
345
|
+
message: str | None = typer.Option(
|
|
302
346
|
None, "--message", "-m", help="Message to send to the agent (skips interactive mode)"
|
|
303
347
|
),
|
|
304
|
-
prompt_file:
|
|
348
|
+
prompt_file: str | None = typer.Option(
|
|
305
349
|
None, "--prompt-file", "-p", help="Path to a prompt file to use (either text or JSON)"
|
|
306
350
|
),
|
|
307
|
-
|
|
351
|
+
skills_dir: Path | None = typer.Option(
|
|
352
|
+
None,
|
|
353
|
+
"--skills-dir",
|
|
354
|
+
"--skills",
|
|
355
|
+
help="Override the default skills directory",
|
|
356
|
+
),
|
|
357
|
+
npx: str | None = typer.Option(
|
|
308
358
|
None, "--npx", help="NPX package and args to run as MCP server (quoted)"
|
|
309
359
|
),
|
|
310
|
-
uvx:
|
|
360
|
+
uvx: str | None = typer.Option(
|
|
311
361
|
None, "--uvx", help="UVX package and args to run as MCP server (quoted)"
|
|
312
362
|
),
|
|
313
|
-
stdio:
|
|
363
|
+
stdio: str | None = typer.Option(
|
|
314
364
|
None, "--stdio", help="Command to run as STDIO MCP server (quoted)"
|
|
315
365
|
),
|
|
366
|
+
shell: bool = typer.Option(
|
|
367
|
+
False,
|
|
368
|
+
"--shell",
|
|
369
|
+
"-x",
|
|
370
|
+
help="Enable a local shell runtime and expose the execute tool (bash or pwsh).",
|
|
371
|
+
),
|
|
316
372
|
) -> None:
|
|
317
373
|
"""
|
|
318
374
|
Run an interactive agent directly from the command line.
|
|
@@ -328,6 +384,7 @@ def go(
|
|
|
328
384
|
fast-agent go --uvx "mcp-server-fetch --verbose"
|
|
329
385
|
fast-agent go --stdio "python my_server.py --debug"
|
|
330
386
|
fast-agent go --stdio "uv run server.py --config=settings.json"
|
|
387
|
+
fast-agent go --skills /path/to/myskills -x
|
|
331
388
|
|
|
332
389
|
This will start an interactive session with the agent, using the specified model
|
|
333
390
|
and instruction. It will use the default configuration from fastagent.config.yaml
|
|
@@ -341,12 +398,15 @@ def go(
|
|
|
341
398
|
--auth Bearer token for authorization with URL-based servers
|
|
342
399
|
--message, -m Send a single message and exit
|
|
343
400
|
--prompt-file, -p Use a prompt file instead of interactive mode
|
|
401
|
+
--skills Override the default skills folder
|
|
402
|
+
--shell, -x Enable local shell runtime
|
|
344
403
|
--npx NPX package and args to run as MCP server (quoted)
|
|
345
404
|
--uvx UVX package and args to run as MCP server (quoted)
|
|
346
405
|
--stdio Command to run as STDIO MCP server (quoted)
|
|
347
406
|
"""
|
|
348
407
|
# Collect all stdio commands from convenience options
|
|
349
408
|
stdio_commands = []
|
|
409
|
+
shell_enabled = shell
|
|
350
410
|
|
|
351
411
|
if npx:
|
|
352
412
|
stdio_commands.append(f"npx {npx}")
|
|
@@ -357,6 +417,8 @@ def go(
|
|
|
357
417
|
if stdio:
|
|
358
418
|
stdio_commands.append(stdio)
|
|
359
419
|
|
|
420
|
+
# When shell is enabled we don't add an MCP stdio server; handled inside the agent
|
|
421
|
+
|
|
360
422
|
# Resolve instruction from file/URL or use default
|
|
361
423
|
resolved_instruction = default_instruction # Default
|
|
362
424
|
agent_name = "agent"
|
|
@@ -396,4 +458,6 @@ def go(
|
|
|
396
458
|
prompt_file=prompt_file,
|
|
397
459
|
stdio_commands=stdio_commands,
|
|
398
460
|
agent_name=agent_name,
|
|
461
|
+
skills_directory=skills_dir,
|
|
462
|
+
shell_enabled=shell_enabled,
|
|
399
463
|
)
|
fast_agent/cli/constants.py
CHANGED
|
@@ -19,7 +19,19 @@ GO_SPECIFIC_OPTIONS = {
|
|
|
19
19
|
"--name",
|
|
20
20
|
"--config-path",
|
|
21
21
|
"-c",
|
|
22
|
+
"--shell",
|
|
23
|
+
"-x",
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
# Known subcommands that should not trigger auto-routing
|
|
25
|
-
KNOWN_SUBCOMMANDS = {
|
|
27
|
+
KNOWN_SUBCOMMANDS = {
|
|
28
|
+
"go",
|
|
29
|
+
"setup",
|
|
30
|
+
"check",
|
|
31
|
+
"auth",
|
|
32
|
+
"bootstrap",
|
|
33
|
+
"quickstart",
|
|
34
|
+
"--help",
|
|
35
|
+
"-h",
|
|
36
|
+
"--version",
|
|
37
|
+
}
|
fast_agent/cli/main.py
CHANGED
|
@@ -62,6 +62,7 @@ def show_welcome() -> None:
|
|
|
62
62
|
table.add_column("Description", header_style="bold bright_white")
|
|
63
63
|
|
|
64
64
|
table.add_row("[bold]go[/bold]", "Start an interactive session")
|
|
65
|
+
table.add_row("go --shell", "Start an interactive session with a local shell tool")
|
|
65
66
|
table.add_row("check", "Show current configuration")
|
|
66
67
|
table.add_row("auth", "Manage OAuth tokens and keyring")
|
|
67
68
|
table.add_row("setup", "Create agent template and configuration")
|
fast_agent/config.py
CHANGED
|
@@ -95,18 +95,41 @@ class MCPTimelineSettings(BaseModel):
|
|
|
95
95
|
raise ValueError("Timeline steps must be greater than zero.")
|
|
96
96
|
return value
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
|
|
99
|
+
class SkillsSettings(BaseModel):
|
|
100
|
+
"""Configuration for the skills directory override."""
|
|
101
|
+
|
|
102
|
+
directory: str | None = None
|
|
103
|
+
|
|
104
|
+
model_config = ConfigDict(extra="ignore")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ShellSettings(BaseModel):
|
|
108
|
+
"""Configuration for shell execution behavior."""
|
|
109
|
+
|
|
110
|
+
timeout_seconds: int = 90
|
|
111
|
+
"""Maximum seconds to wait for command output before terminating (default: 90s)"""
|
|
112
|
+
|
|
113
|
+
warning_interval_seconds: int = 30
|
|
114
|
+
"""Show timeout warnings every N seconds (default: 30s)"""
|
|
115
|
+
|
|
116
|
+
model_config = ConfigDict(extra="ignore")
|
|
117
|
+
|
|
118
|
+
@field_validator("timeout_seconds", mode="before")
|
|
99
119
|
@classmethod
|
|
100
|
-
def
|
|
120
|
+
def _coerce_timeout(cls, value: Any) -> int:
|
|
121
|
+
"""Support duration strings like '90s', '2m', '1h'"""
|
|
101
122
|
if isinstance(value, str):
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
123
|
+
return MCPTimelineSettings._parse_duration(value)
|
|
124
|
+
return int(value)
|
|
125
|
+
|
|
126
|
+
@field_validator("warning_interval_seconds", mode="before")
|
|
127
|
+
@classmethod
|
|
128
|
+
def _coerce_warning_interval(cls, value: Any) -> int:
|
|
129
|
+
"""Support duration strings like '30s', '1m'"""
|
|
130
|
+
if isinstance(value, str):
|
|
131
|
+
return MCPTimelineSettings._parse_duration(value)
|
|
132
|
+
return int(value)
|
|
110
133
|
|
|
111
134
|
|
|
112
135
|
class MCPRootSettings(BaseModel):
|
|
@@ -448,8 +471,8 @@ class LoggerSettings(BaseModel):
|
|
|
448
471
|
"""Truncate display of long tool calls"""
|
|
449
472
|
enable_markup: bool = True
|
|
450
473
|
"""Enable markup in console output. Disable for outputs that may conflict with rich console formatting"""
|
|
451
|
-
|
|
452
|
-
"""
|
|
474
|
+
streaming: Literal["markdown", "plain", "none"] = "markdown"
|
|
475
|
+
"""Streaming renderer for assistant responses"""
|
|
453
476
|
|
|
454
477
|
|
|
455
478
|
def find_fastagent_config_files(start_path: Path) -> Tuple[Optional[Path], Optional[Path]]:
|
|
@@ -591,6 +614,12 @@ class Settings(BaseSettings):
|
|
|
591
614
|
mcp_timeline: MCPTimelineSettings = MCPTimelineSettings()
|
|
592
615
|
"""Display settings for MCP activity timelines."""
|
|
593
616
|
|
|
617
|
+
skills: SkillsSettings = SkillsSettings()
|
|
618
|
+
"""Local skills discovery and selection settings."""
|
|
619
|
+
|
|
620
|
+
shell_execution: ShellSettings = ShellSettings()
|
|
621
|
+
"""Shell execution timeout and warning settings."""
|
|
622
|
+
|
|
594
623
|
@classmethod
|
|
595
624
|
def find_config(cls) -> Path | None:
|
|
596
625
|
"""Find the config file in the current directory or parent directories."""
|
fast_agent/constants.py
CHANGED
|
@@ -11,3 +11,11 @@ FAST_AGENT_REMOVED_METADATA_CHANNEL = "fast-agent-removed-meta"
|
|
|
11
11
|
# should we have MAX_TOOL_CALLS instead to constrain by number of tools rather than turns...?
|
|
12
12
|
DEFAULT_MAX_ITERATIONS = 20
|
|
13
13
|
"""Maximum number of User/Assistant turns to take"""
|
|
14
|
+
|
|
15
|
+
DEFAULT_AGENT_INSTRUCTION = """You are a helpful AI Agent.
|
|
16
|
+
|
|
17
|
+
{{serverInstructions}}
|
|
18
|
+
|
|
19
|
+
{{agentSkills}}
|
|
20
|
+
|
|
21
|
+
The current date is {{currentDate}}."""
|
fast_agent/context.py
CHANGED
|
@@ -26,6 +26,7 @@ from fast_agent.core.logging.events import EventFilter, StreamingExclusionFilter
|
|
|
26
26
|
from fast_agent.core.logging.logger import LoggingConfig, get_logger
|
|
27
27
|
from fast_agent.core.logging.transport import create_transport
|
|
28
28
|
from fast_agent.mcp_server_registry import ServerRegistry
|
|
29
|
+
from fast_agent.skills import SkillRegistry
|
|
29
30
|
|
|
30
31
|
if TYPE_CHECKING:
|
|
31
32
|
from fast_agent.core.executor.workflow_signal import SignalWaitCallback
|
|
@@ -56,6 +57,7 @@ class Context(BaseModel):
|
|
|
56
57
|
# Registries
|
|
57
58
|
server_registry: Optional[ServerRegistry] = None
|
|
58
59
|
task_registry: Optional[ActivityRegistry] = None
|
|
60
|
+
skill_registry: Optional[SkillRegistry] = None
|
|
59
61
|
|
|
60
62
|
tracer: trace.Tracer | None = None
|
|
61
63
|
_connection_manager: "MCPConnectionManager | None" = None
|
|
@@ -145,28 +147,26 @@ async def configure_logger(config: "Settings") -> None:
|
|
|
145
147
|
python_logger.setLevel(settings.level.upper())
|
|
146
148
|
python_logger.propagate = False
|
|
147
149
|
|
|
148
|
-
|
|
150
|
+
transport = None
|
|
149
151
|
if settings.type == "console":
|
|
152
|
+
# Console mode: use the Python logger to emit to stdout and skip additional transport output
|
|
150
153
|
handler = logging.StreamHandler()
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
155
|
-
handler = logging.FileHandler(log_path)
|
|
156
|
-
elif settings.type == "none":
|
|
157
|
-
handler = logging.NullHandler()
|
|
154
|
+
handler.setLevel(settings.level.upper())
|
|
155
|
+
handler.setFormatter(logging.Formatter("%(message)s"))
|
|
156
|
+
python_logger.addHandler(handler)
|
|
158
157
|
else:
|
|
159
|
-
# For
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
handler.setLevel(settings.level.upper())
|
|
163
|
-
handler.setFormatter(logging.Formatter("%(message)s"))
|
|
164
|
-
python_logger.addHandler(handler)
|
|
158
|
+
# For all other modes, rely on transports (file/http/none) and keep the Python logger quiet
|
|
159
|
+
python_logger.addHandler(logging.NullHandler())
|
|
165
160
|
|
|
166
161
|
# Use StreamingExclusionFilter to prevent streaming events from flooding logs
|
|
167
162
|
event_filter: EventFilter = StreamingExclusionFilter(min_level=settings.level)
|
|
168
163
|
logger.info(f"Configuring logger with level: {settings.level}")
|
|
169
|
-
|
|
164
|
+
if settings.type == "console":
|
|
165
|
+
from fast_agent.core.logging.transport import NoOpTransport
|
|
166
|
+
|
|
167
|
+
transport = NoOpTransport(event_filter=event_filter)
|
|
168
|
+
else:
|
|
169
|
+
transport = create_transport(settings=settings, event_filter=event_filter)
|
|
170
170
|
await LoggingConfig.configure(
|
|
171
171
|
event_filter=event_filter,
|
|
172
172
|
transport=transport,
|
|
@@ -206,6 +206,15 @@ async def initialize_context(
|
|
|
206
206
|
context.config = config
|
|
207
207
|
context.server_registry = ServerRegistry(config=config)
|
|
208
208
|
|
|
209
|
+
skills_settings = getattr(config, "skills", None)
|
|
210
|
+
override_directory = None
|
|
211
|
+
if skills_settings and getattr(skills_settings, "directory", None):
|
|
212
|
+
override_directory = Path(skills_settings.directory).expanduser()
|
|
213
|
+
context.skill_registry = SkillRegistry(
|
|
214
|
+
base_dir=Path.cwd(),
|
|
215
|
+
override_directory=override_directory,
|
|
216
|
+
)
|
|
217
|
+
|
|
209
218
|
# Configure logging and telemetry
|
|
210
219
|
await configure_otel(config)
|
|
211
220
|
await configure_logger(config)
|
|
@@ -27,6 +27,7 @@ from fast_agent.agents.workflow.iterative_planner import ITERATIVE_PLAN_SYSTEM_P
|
|
|
27
27
|
from fast_agent.agents.workflow.router_agent import (
|
|
28
28
|
ROUTING_SYSTEM_INSTRUCTION,
|
|
29
29
|
)
|
|
30
|
+
from fast_agent.skills import SkillManifest, SkillRegistry
|
|
30
31
|
from fast_agent.types import RequestParams
|
|
31
32
|
|
|
32
33
|
# Type variables for the decorated function
|
|
@@ -182,6 +183,9 @@ def _decorator_impl(
|
|
|
182
183
|
tools: Optional[Dict[str, List[str]]] = None,
|
|
183
184
|
resources: Optional[Dict[str, List[str]]] = None,
|
|
184
185
|
prompts: Optional[Dict[str, List[str]]] = None,
|
|
186
|
+
skills: SkillManifest | SkillRegistry | Path | str | List[
|
|
187
|
+
SkillManifest | SkillRegistry | Path | str | None
|
|
188
|
+
] | None = None,
|
|
185
189
|
**extra_kwargs,
|
|
186
190
|
) -> Callable[[Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]]:
|
|
187
191
|
"""
|
|
@@ -209,6 +213,7 @@ def _decorator_impl(
|
|
|
209
213
|
tools=tools,
|
|
210
214
|
resources=resources,
|
|
211
215
|
prompts=prompts,
|
|
216
|
+
skills=skills,
|
|
212
217
|
model=model,
|
|
213
218
|
use_history=use_history,
|
|
214
219
|
human_input=human_input,
|
|
@@ -256,6 +261,7 @@ def agent(
|
|
|
256
261
|
tools: Optional[Dict[str, List[str]]] = None,
|
|
257
262
|
resources: Optional[Dict[str, List[str]]] = None,
|
|
258
263
|
prompts: Optional[Dict[str, List[str]]] = None,
|
|
264
|
+
skills: SkillManifest | SkillRegistry | Path | str | None = None,
|
|
259
265
|
model: Optional[str] = None,
|
|
260
266
|
use_history: bool = True,
|
|
261
267
|
request_params: RequestParams | None = None,
|
|
@@ -306,6 +312,7 @@ def agent(
|
|
|
306
312
|
tools=tools,
|
|
307
313
|
resources=resources,
|
|
308
314
|
prompts=prompts,
|
|
315
|
+
skills=skills,
|
|
309
316
|
api_key=api_key,
|
|
310
317
|
)
|
|
311
318
|
|
|
@@ -321,6 +328,7 @@ def custom(
|
|
|
321
328
|
tools: Optional[Dict[str, List[str]]] = None,
|
|
322
329
|
resources: Optional[Dict[str, List[str]]] = None,
|
|
323
330
|
prompts: Optional[Dict[str, List[str]]] = None,
|
|
331
|
+
skills: SkillManifest | SkillRegistry | Path | str | None = None,
|
|
324
332
|
model: Optional[str] = None,
|
|
325
333
|
use_history: bool = True,
|
|
326
334
|
request_params: RequestParams | None = None,
|
|
@@ -368,6 +376,7 @@ def custom(
|
|
|
368
376
|
tools=tools,
|
|
369
377
|
resources=resources,
|
|
370
378
|
prompts=prompts,
|
|
379
|
+
skills=skills,
|
|
371
380
|
)
|
|
372
381
|
|
|
373
382
|
|