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.

Files changed (49) 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 +52 -4
  4. fast_agent/agents/llm_decorator.py +6 -0
  5. fast_agent/agents/mcp_agent.py +137 -13
  6. fast_agent/agents/tool_agent.py +33 -19
  7. fast_agent/agents/workflow/router_agent.py +2 -1
  8. fast_agent/cli/__main__.py +35 -0
  9. fast_agent/cli/commands/check_config.py +90 -2
  10. fast_agent/cli/commands/go.py +100 -36
  11. fast_agent/cli/constants.py +13 -1
  12. fast_agent/cli/main.py +1 -0
  13. fast_agent/config.py +41 -12
  14. fast_agent/constants.py +8 -0
  15. fast_agent/context.py +24 -15
  16. fast_agent/core/direct_decorators.py +9 -0
  17. fast_agent/core/fastagent.py +115 -2
  18. fast_agent/core/logging/listeners.py +8 -0
  19. fast_agent/core/validation.py +31 -33
  20. fast_agent/human_input/form_fields.py +4 -1
  21. fast_agent/interfaces.py +12 -1
  22. fast_agent/llm/fastagent_llm.py +76 -0
  23. fast_agent/llm/memory.py +26 -1
  24. fast_agent/llm/model_database.py +2 -2
  25. fast_agent/llm/model_factory.py +4 -1
  26. fast_agent/llm/provider/anthropic/llm_anthropic.py +112 -0
  27. fast_agent/llm/provider/openai/llm_openai.py +184 -18
  28. fast_agent/llm/provider/openai/responses.py +133 -0
  29. fast_agent/mcp/prompt_message_extended.py +2 -2
  30. fast_agent/resources/setup/agent.py +2 -0
  31. fast_agent/resources/setup/fastagent.config.yaml +11 -4
  32. fast_agent/skills/__init__.py +9 -0
  33. fast_agent/skills/registry.py +200 -0
  34. fast_agent/tools/shell_runtime.py +404 -0
  35. fast_agent/ui/console_display.py +925 -73
  36. fast_agent/ui/elicitation_form.py +98 -24
  37. fast_agent/ui/elicitation_style.py +2 -2
  38. fast_agent/ui/enhanced_prompt.py +128 -26
  39. fast_agent/ui/history_display.py +20 -5
  40. fast_agent/ui/interactive_prompt.py +108 -3
  41. fast_agent/ui/markdown_truncator.py +942 -0
  42. fast_agent/ui/mcp_display.py +2 -2
  43. fast_agent/ui/plain_text_truncator.py +68 -0
  44. fast_agent/ui/streaming_buffer.py +449 -0
  45. {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/METADATA +9 -7
  46. {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/RECORD +49 -42
  47. {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/WHEEL +0 -0
  48. {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/entry_points.txt +0 -0
  49. {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]")
@@ -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):
@@ -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
- use_legacy_display: bool = False
452
- """Use the legacy console display instead of the new style display"""
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
- 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