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.

Files changed (39) hide show
  1. fast_agent/__init__.py +2 -0
  2. fast_agent/agents/agent_types.py +5 -0
  3. fast_agent/agents/llm_agent.py +7 -0
  4. fast_agent/agents/llm_decorator.py +6 -0
  5. fast_agent/agents/mcp_agent.py +134 -10
  6. fast_agent/cli/__main__.py +35 -0
  7. fast_agent/cli/commands/check_config.py +85 -0
  8. fast_agent/cli/commands/go.py +100 -36
  9. fast_agent/cli/constants.py +13 -1
  10. fast_agent/cli/main.py +1 -0
  11. fast_agent/config.py +39 -10
  12. fast_agent/constants.py +8 -0
  13. fast_agent/context.py +24 -15
  14. fast_agent/core/direct_decorators.py +9 -0
  15. fast_agent/core/fastagent.py +101 -1
  16. fast_agent/core/logging/listeners.py +8 -0
  17. fast_agent/interfaces.py +8 -0
  18. fast_agent/llm/fastagent_llm.py +45 -0
  19. fast_agent/llm/memory.py +26 -1
  20. fast_agent/llm/provider/anthropic/llm_anthropic.py +112 -0
  21. fast_agent/llm/provider/openai/llm_openai.py +184 -18
  22. fast_agent/llm/provider/openai/responses.py +133 -0
  23. fast_agent/resources/setup/agent.py +2 -0
  24. fast_agent/resources/setup/fastagent.config.yaml +6 -0
  25. fast_agent/skills/__init__.py +9 -0
  26. fast_agent/skills/registry.py +200 -0
  27. fast_agent/tools/shell_runtime.py +404 -0
  28. fast_agent/ui/console_display.py +396 -129
  29. fast_agent/ui/elicitation_form.py +76 -24
  30. fast_agent/ui/elicitation_style.py +2 -2
  31. fast_agent/ui/enhanced_prompt.py +81 -25
  32. fast_agent/ui/history_display.py +20 -5
  33. fast_agent/ui/interactive_prompt.py +108 -3
  34. fast_agent/ui/markdown_truncator.py +1 -1
  35. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.dist-info}/METADATA +8 -7
  36. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.dist-info}/RECORD +39 -35
  37. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.dist-info}/WHEEL +0 -0
  38. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.dist-info}/entry_points.txt +0 -0
  39. {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.dist-info}/licenses/LICENSE +0 -0
@@ -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 typing import Dict, List, Optional
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 = """You are a helpful AI Agent.
23
+ default_instruction = DEFAULT_AGENT_INSTRUCTION
22
24
 
23
- {{serverInstructions}}
24
25
 
25
- The current date is {{currentDate}}."""
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: Optional[str] = None,
32
- server_list: Optional[List[str]] = None,
33
- model: Optional[str] = None,
34
- message: Optional[str] = None,
35
- prompt_file: Optional[str] = None,
36
- url_servers: Optional[Dict[str, Dict[str, str]]] = None,
37
- stdio_servers: Optional[Dict[str, Dict[str, str]]] = None,
38
- agent_name: Optional[str] = "agent",
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: Optional[str] = None,
153
- servers: Optional[str] = None,
154
- urls: Optional[str] = None,
155
- auth: Optional[str] = None,
156
- model: Optional[str] = None,
157
- message: Optional[str] = None,
158
- prompt_file: Optional[str] = None,
159
- stdio_commands: Optional[List[str]] = None,
160
- agent_name: Optional[str] = None,
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: Optional[str] = typer.Option(
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: Optional[str] = typer.Option(
287
- None, "--config-path", "-c", help="Path to config file"
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: Optional[str] = typer.Option(
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: Optional[str] = typer.Option(
339
+ auth: str | None = typer.Option(
296
340
  None, "--auth", help="Bearer token for authorization with URL-based servers"
297
341
  ),
298
- model: Optional[str] = typer.Option(
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: Optional[str] = typer.Option(
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: Optional[str] = typer.Option(
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
- npx: Optional[str] = typer.Option(
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: Optional[str] = typer.Option(
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: Optional[str] = typer.Option(
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
  )
@@ -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 = {"go", "setup", "check", "auth", "bootstrap", "quickstart", "--help", "-h", "--version"}
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
- @field_validator("step_seconds", mode="before")
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 _coerce_step_seconds(cls, value: Any) -> int:
120
+ def _coerce_timeout(cls, value: Any) -> int:
121
+ """Support duration strings like '90s', '2m', '1h'"""
101
122
  if isinstance(value, str):
102
- value = cls._parse_duration(value)
103
- elif isinstance(value, (int, float)):
104
- value = int(value)
105
- else:
106
- raise TypeError("Timeline step duration must be a number of seconds.")
107
- if value <= 0:
108
- raise ValueError("Timeline step duration must be greater than zero.")
109
- return value
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
- handler: logging.Handler
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
- elif settings.type == "file":
152
- log_path = Path(settings.path)
153
- if log_path.parent:
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 transports that handle output elsewhere (e.g., HTTP), suppress console output.
160
- handler = logging.NullHandler()
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
- transport = create_transport(settings=settings, event_filter=event_filter)
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
 
@@ -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 = "You are a helpful agent.",
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.