fast-agent-mcp 0.3.15__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 +7 -0
- fast_agent/agents/llm_decorator.py +6 -0
- fast_agent/agents/mcp_agent.py +134 -10
- fast_agent/cli/__main__.py +35 -0
- fast_agent/cli/commands/check_config.py +85 -0
- 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 +39 -10
- 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 +101 -1
- fast_agent/core/logging/listeners.py +8 -0
- fast_agent/interfaces.py +8 -0
- fast_agent/llm/fastagent_llm.py +45 -0
- fast_agent/llm/memory.py +26 -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/resources/setup/agent.py +2 -0
- fast_agent/resources/setup/fastagent.config.yaml +6 -0
- 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 +396 -129
- fast_agent/ui/elicitation_form.py +76 -24
- fast_agent/ui/elicitation_style.py +2 -2
- fast_agent/ui/enhanced_prompt.py +81 -25
- fast_agent/ui/history_display.py +20 -5
- fast_agent/ui/interactive_prompt.py +108 -3
- fast_agent/ui/markdown_truncator.py +1 -1
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.dist-info}/METADATA +8 -7
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.dist-info}/RECORD +39 -35
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.dist-info}/licenses/LICENSE +0 -0
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):
|
|
@@ -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
|
|
fast_agent/core/fastagent.py
CHANGED
|
@@ -6,6 +6,7 @@ directly creates Agent instances without proxies.
|
|
|
6
6
|
|
|
7
7
|
import argparse
|
|
8
8
|
import asyncio
|
|
9
|
+
import pathlib
|
|
9
10
|
import sys
|
|
10
11
|
from contextlib import asynccontextmanager
|
|
11
12
|
from importlib.metadata import version as get_version
|
|
@@ -76,12 +77,14 @@ from fast_agent.core.validation import (
|
|
|
76
77
|
validate_workflow_references,
|
|
77
78
|
)
|
|
78
79
|
from fast_agent.mcp.prompts.prompt_load import load_prompt
|
|
80
|
+
from fast_agent.skills import SkillManifest, SkillRegistry
|
|
79
81
|
from fast_agent.ui.usage_display import display_usage_report
|
|
80
82
|
|
|
81
83
|
if TYPE_CHECKING:
|
|
82
84
|
from mcp.client.session import ElicitationFnT
|
|
83
85
|
from pydantic import AnyUrl
|
|
84
86
|
|
|
87
|
+
from fast_agent.constants import DEFAULT_AGENT_INSTRUCTION
|
|
85
88
|
from fast_agent.interfaces import AgentProtocol
|
|
86
89
|
from fast_agent.types import PromptMessageExtended
|
|
87
90
|
|
|
@@ -102,6 +105,7 @@ class FastAgent:
|
|
|
102
105
|
ignore_unknown_args: bool = False,
|
|
103
106
|
parse_cli_args: bool = True,
|
|
104
107
|
quiet: bool = False, # Add quiet parameter
|
|
108
|
+
skills_directory: str | pathlib.Path | None = None,
|
|
105
109
|
**kwargs,
|
|
106
110
|
) -> None:
|
|
107
111
|
"""
|
|
@@ -119,6 +123,10 @@ class FastAgent:
|
|
|
119
123
|
"""
|
|
120
124
|
self.args = argparse.Namespace() # Initialize args always
|
|
121
125
|
self._programmatic_quiet = quiet # Store the programmatic quiet setting
|
|
126
|
+
self._skills_directory_override = (
|
|
127
|
+
Path(skills_directory).expanduser() if skills_directory else None
|
|
128
|
+
)
|
|
129
|
+
self._default_skill_manifests: List[SkillManifest] = []
|
|
122
130
|
|
|
123
131
|
# --- Wrap argument parsing logic ---
|
|
124
132
|
if parse_cli_args:
|
|
@@ -173,6 +181,10 @@ class FastAgent:
|
|
|
173
181
|
default="0.0.0.0",
|
|
174
182
|
help="Host address to bind to when running as a server with SSE transport",
|
|
175
183
|
)
|
|
184
|
+
parser.add_argument(
|
|
185
|
+
"--skills",
|
|
186
|
+
help="Path to skills directory to use instead of default .claude/skills",
|
|
187
|
+
)
|
|
176
188
|
|
|
177
189
|
if ignore_unknown_args:
|
|
178
190
|
known_args, _ = parser.parse_known_args()
|
|
@@ -200,6 +212,14 @@ class FastAgent:
|
|
|
200
212
|
if self._programmatic_quiet:
|
|
201
213
|
self.args.quiet = True
|
|
202
214
|
|
|
215
|
+
# Apply CLI skills directory if not already set programmatically
|
|
216
|
+
if (
|
|
217
|
+
self._skills_directory_override is None
|
|
218
|
+
and hasattr(self.args, "skills")
|
|
219
|
+
and self.args.skills
|
|
220
|
+
):
|
|
221
|
+
self._skills_directory_override = Path(self.args.skills).expanduser()
|
|
222
|
+
|
|
203
223
|
self.name = name
|
|
204
224
|
self.config_path = config_path
|
|
205
225
|
|
|
@@ -271,6 +291,7 @@ class FastAgent:
|
|
|
271
291
|
from collections.abc import Coroutine
|
|
272
292
|
from pathlib import Path
|
|
273
293
|
|
|
294
|
+
from fast_agent.skills import SkillManifest, SkillRegistry
|
|
274
295
|
from fast_agent.types import RequestParams
|
|
275
296
|
|
|
276
297
|
P = ParamSpec("P")
|
|
@@ -281,11 +302,12 @@ class FastAgent:
|
|
|
281
302
|
name: str = "default",
|
|
282
303
|
instruction_or_kwarg: Optional[str | Path | AnyUrl] = None,
|
|
283
304
|
*,
|
|
284
|
-
instruction: str | Path | AnyUrl =
|
|
305
|
+
instruction: str | Path | AnyUrl = DEFAULT_AGENT_INSTRUCTION,
|
|
285
306
|
servers: List[str] = [],
|
|
286
307
|
tools: Optional[Dict[str, List[str]]] = None,
|
|
287
308
|
resources: Optional[Dict[str, List[str]]] = None,
|
|
288
309
|
prompts: Optional[Dict[str, List[str]]] = None,
|
|
310
|
+
skills: Optional[List[SkillManifest | SkillRegistry | Path | str | None]] = None,
|
|
289
311
|
model: Optional[str] = None,
|
|
290
312
|
use_history: bool = True,
|
|
291
313
|
request_params: RequestParams | None = None,
|
|
@@ -430,6 +452,21 @@ class FastAgent:
|
|
|
430
452
|
with tracer.start_as_current_span(self.name):
|
|
431
453
|
try:
|
|
432
454
|
async with self.app.run():
|
|
455
|
+
registry = getattr(self.context, "skill_registry", None)
|
|
456
|
+
if self._skills_directory_override is not None:
|
|
457
|
+
override_registry = SkillRegistry(
|
|
458
|
+
base_dir=Path.cwd(),
|
|
459
|
+
override_directory=self._skills_directory_override,
|
|
460
|
+
)
|
|
461
|
+
self.context.skill_registry = override_registry
|
|
462
|
+
registry = override_registry
|
|
463
|
+
|
|
464
|
+
default_skills: List[SkillManifest] = []
|
|
465
|
+
if registry:
|
|
466
|
+
default_skills = registry.load_manifests()
|
|
467
|
+
|
|
468
|
+
self._apply_skills_to_agent_configs(default_skills)
|
|
469
|
+
|
|
433
470
|
# Apply quiet mode if requested
|
|
434
471
|
if quiet_mode:
|
|
435
472
|
cfg = self.app.context.config
|
|
@@ -621,6 +658,69 @@ class FastAgent:
|
|
|
621
658
|
except Exception:
|
|
622
659
|
pass
|
|
623
660
|
|
|
661
|
+
def _apply_skills_to_agent_configs(self, default_skills: List[SkillManifest]) -> None:
|
|
662
|
+
self._default_skill_manifests = list(default_skills)
|
|
663
|
+
|
|
664
|
+
for agent_data in self.agents.values():
|
|
665
|
+
config_obj = agent_data.get("config")
|
|
666
|
+
if not config_obj:
|
|
667
|
+
continue
|
|
668
|
+
|
|
669
|
+
resolved = self._resolve_skills(config_obj.skills)
|
|
670
|
+
if not resolved:
|
|
671
|
+
resolved = list(default_skills)
|
|
672
|
+
else:
|
|
673
|
+
resolved = self._deduplicate_skills(resolved)
|
|
674
|
+
|
|
675
|
+
config_obj.skill_manifests = resolved
|
|
676
|
+
|
|
677
|
+
def _resolve_skills(
|
|
678
|
+
self,
|
|
679
|
+
entry: SkillManifest
|
|
680
|
+
| SkillRegistry
|
|
681
|
+
| Path
|
|
682
|
+
| str
|
|
683
|
+
| List[SkillManifest | SkillRegistry | Path | str | None]
|
|
684
|
+
| None,
|
|
685
|
+
) -> List[SkillManifest]:
|
|
686
|
+
if entry is None:
|
|
687
|
+
return []
|
|
688
|
+
if isinstance(entry, list):
|
|
689
|
+
manifests: List[SkillManifest] = []
|
|
690
|
+
for item in entry:
|
|
691
|
+
manifests.extend(self._resolve_skills(item))
|
|
692
|
+
return manifests
|
|
693
|
+
if isinstance(entry, SkillManifest):
|
|
694
|
+
return [entry]
|
|
695
|
+
if isinstance(entry, SkillRegistry):
|
|
696
|
+
try:
|
|
697
|
+
return entry.load_manifests()
|
|
698
|
+
except Exception:
|
|
699
|
+
logger.debug(
|
|
700
|
+
"Failed to load skills from registry",
|
|
701
|
+
data={"registry": type(entry).__name__},
|
|
702
|
+
)
|
|
703
|
+
return []
|
|
704
|
+
if isinstance(entry, Path):
|
|
705
|
+
return SkillRegistry.load_directory(entry.expanduser().resolve())
|
|
706
|
+
if isinstance(entry, str):
|
|
707
|
+
return SkillRegistry.load_directory(Path(entry).expanduser().resolve())
|
|
708
|
+
|
|
709
|
+
logger.debug(
|
|
710
|
+
"Unsupported skill entry type",
|
|
711
|
+
data={"type": type(entry).__name__},
|
|
712
|
+
)
|
|
713
|
+
return []
|
|
714
|
+
|
|
715
|
+
@staticmethod
|
|
716
|
+
def _deduplicate_skills(manifests: List[SkillManifest]) -> List[SkillManifest]:
|
|
717
|
+
unique: Dict[str, SkillManifest] = {}
|
|
718
|
+
for manifest in manifests:
|
|
719
|
+
key = manifest.name.lower()
|
|
720
|
+
if key not in unique:
|
|
721
|
+
unique[key] = manifest
|
|
722
|
+
return list(unique.values())
|
|
723
|
+
|
|
624
724
|
def _handle_error(self, e: Exception, error_type: Optional[str] = None) -> None:
|
|
625
725
|
"""
|
|
626
726
|
Handle errors with consistent formatting and messaging.
|