fast-agent-mcp 0.3.13__py3-none-any.whl → 0.3.15__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 (44) hide show
  1. fast_agent/agents/llm_agent.py +59 -37
  2. fast_agent/agents/llm_decorator.py +13 -2
  3. fast_agent/agents/mcp_agent.py +21 -5
  4. fast_agent/agents/tool_agent.py +41 -29
  5. fast_agent/agents/workflow/router_agent.py +2 -1
  6. fast_agent/cli/commands/check_config.py +48 -1
  7. fast_agent/config.py +65 -2
  8. fast_agent/constants.py +3 -0
  9. fast_agent/context.py +42 -9
  10. fast_agent/core/fastagent.py +14 -1
  11. fast_agent/core/logging/listeners.py +1 -1
  12. fast_agent/core/validation.py +31 -33
  13. fast_agent/event_progress.py +2 -3
  14. fast_agent/human_input/form_fields.py +4 -1
  15. fast_agent/interfaces.py +12 -2
  16. fast_agent/llm/fastagent_llm.py +31 -0
  17. fast_agent/llm/model_database.py +2 -2
  18. fast_agent/llm/model_factory.py +8 -1
  19. fast_agent/llm/provider_key_manager.py +1 -0
  20. fast_agent/llm/provider_types.py +1 -0
  21. fast_agent/llm/request_params.py +3 -1
  22. fast_agent/mcp/mcp_aggregator.py +313 -40
  23. fast_agent/mcp/mcp_connection_manager.py +39 -9
  24. fast_agent/mcp/prompt_message_extended.py +2 -2
  25. fast_agent/mcp/skybridge.py +45 -0
  26. fast_agent/mcp/sse_tracking.py +287 -0
  27. fast_agent/mcp/transport_tracking.py +37 -3
  28. fast_agent/mcp/types.py +24 -0
  29. fast_agent/resources/examples/workflows/router.py +1 -0
  30. fast_agent/resources/setup/fastagent.config.yaml +7 -1
  31. fast_agent/ui/console_display.py +946 -84
  32. fast_agent/ui/elicitation_form.py +23 -1
  33. fast_agent/ui/enhanced_prompt.py +153 -58
  34. fast_agent/ui/interactive_prompt.py +57 -34
  35. fast_agent/ui/markdown_truncator.py +942 -0
  36. fast_agent/ui/mcp_display.py +110 -29
  37. fast_agent/ui/plain_text_truncator.py +68 -0
  38. fast_agent/ui/rich_progress.py +4 -1
  39. fast_agent/ui/streaming_buffer.py +449 -0
  40. {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/METADATA +4 -3
  41. {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/RECORD +44 -38
  42. {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/WHEEL +0 -0
  43. {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/entry_points.txt +0 -0
  44. {fast_agent_mcp-0.3.13.dist-info → fast_agent_mcp-0.3.15.dist-info}/licenses/LICENSE +0 -0
@@ -39,6 +39,7 @@ class Colours:
39
39
  # Capability token states
40
40
  TOKEN_ERROR = "bright_red"
41
41
  TOKEN_WARNING = "bright_cyan"
42
+ TOKEN_CAUTION = "bright_yellow"
42
43
  TOKEN_DISABLED = "dim"
43
44
  TOKEN_HIGHLIGHTED = "bright_yellow"
44
45
  TOKEN_ENABLED = "bright_green"
@@ -104,6 +105,38 @@ def _format_compact_duration(seconds: float | None) -> str | None:
104
105
  return f"{days}d{hours:02d}h"
105
106
 
106
107
 
108
+ def _format_timeline_label(total_seconds: int) -> str:
109
+ total = max(0, int(total_seconds))
110
+ if total == 0:
111
+ return "0s"
112
+
113
+ days, remainder = divmod(total, 86400)
114
+ if days:
115
+ if remainder == 0:
116
+ return f"{days}d"
117
+ hours = remainder // 3600
118
+ if hours == 0:
119
+ return f"{days}d"
120
+ return f"{days}d{hours}h"
121
+
122
+ hours, remainder = divmod(total, 3600)
123
+ if hours:
124
+ if remainder == 0:
125
+ return f"{hours}h"
126
+ minutes = remainder // 60
127
+ if minutes == 0:
128
+ return f"{hours}h"
129
+ return f"{hours}h{minutes:02d}m"
130
+
131
+ minutes, seconds = divmod(total, 60)
132
+ if minutes:
133
+ if seconds == 0:
134
+ return f"{minutes}m"
135
+ return f"{minutes}m{seconds:02d}s"
136
+
137
+ return f"{seconds}s"
138
+
139
+
107
140
  def _summarise_call_counts(call_counts: dict[str, int]) -> str | None:
108
141
  if not call_counts:
109
142
  return None
@@ -192,7 +225,7 @@ def _format_capability_shorthand(
192
225
  elif instructions_enabled is False:
193
226
  entries.append(("In", "red", False))
194
227
  elif instructions_enabled is None and not template_expected:
195
- entries.append(("In", "blue", False))
228
+ entries.append(("In", "warn", False))
196
229
  elif instructions_enabled is None:
197
230
  entries.append(("In", True, False))
198
231
  elif template_expected:
@@ -200,6 +233,18 @@ def _format_capability_shorthand(
200
233
  else:
201
234
  entries.append(("In", "blue", False))
202
235
 
236
+ skybridge_config = getattr(status, "skybridge", None)
237
+ if not skybridge_config:
238
+ entries.append(("Sk", False, False))
239
+ else:
240
+ has_warnings = bool(getattr(skybridge_config, "warnings", None))
241
+ if has_warnings:
242
+ entries.append(("Sk", "warn", False))
243
+ elif getattr(skybridge_config, "enabled", False):
244
+ entries.append(("Sk", True, False))
245
+ else:
246
+ entries.append(("Sk", False, False))
247
+
203
248
  if status.roots_configured:
204
249
  entries.append(("Ro", True, False))
205
250
  else:
@@ -228,6 +273,8 @@ def _format_capability_shorthand(
228
273
  return Colours.TOKEN_ERROR
229
274
  if supported == "blue":
230
275
  return Colours.TOKEN_WARNING
276
+ if supported == "warn":
277
+ return Colours.TOKEN_CAUTION
231
278
  if not supported:
232
279
  return Colours.TOKEN_DISABLED
233
280
  if highlighted:
@@ -268,10 +315,28 @@ def _format_label(label: str, width: int = 10) -> str:
268
315
  return f"{label:<{width}}" if len(label) < width else label
269
316
 
270
317
 
271
- def _build_inline_timeline(buckets: Iterable[str]) -> str:
318
+ def _build_inline_timeline(
319
+ buckets: Iterable[str],
320
+ *,
321
+ bucket_seconds: int | None = None,
322
+ bucket_count: int | None = None,
323
+ ) -> str:
272
324
  """Build a compact timeline string for inline display."""
273
- timeline = " [dim]10m[/dim] "
274
- for state in buckets:
325
+ bucket_list = list(buckets)
326
+ count = bucket_count or len(bucket_list)
327
+ if count <= 0:
328
+ count = len(bucket_list) or 1
329
+
330
+ seconds = bucket_seconds or 30
331
+ total_window = seconds * count
332
+ timeline = f" [dim]{_format_timeline_label(total_window)}[/dim] "
333
+
334
+ if len(bucket_list) < count:
335
+ bucket_list.extend(["none"] * (count - len(bucket_list)))
336
+ elif len(bucket_list) > count:
337
+ bucket_list = bucket_list[-count:]
338
+
339
+ for state in bucket_list:
275
340
  color = TIMELINE_COLORS.get(state, Colours.NONE)
276
341
  if state in {"idle", "none"}:
277
342
  symbol = SYMBOL_IDLE
@@ -297,6 +362,10 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
297
362
  if snapshot is None:
298
363
  return
299
364
 
365
+ transport_value = getattr(status, "transport", None)
366
+ transport_lower = (transport_value or "").lower()
367
+ is_sse_transport = transport_lower == "sse"
368
+
300
369
  # Show channel types based on what's available
301
370
  entries: list[tuple[str, str, ChannelSnapshot | None]] = []
302
371
 
@@ -311,12 +380,13 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
311
380
  stdio_channel = getattr(snapshot, "stdio", None)
312
381
 
313
382
  if any(channel is not None for channel in http_channels):
314
- # HTTP transport - show the original three channels
383
+ # HTTP or SSE transport - show available channels
315
384
  entries = [
316
385
  ("GET (SSE)", "◀", getattr(snapshot, "get", None)),
317
386
  ("POST (SSE)", "▶", getattr(snapshot, "post_sse", None)),
318
- ("POST (JSON)", "▶", getattr(snapshot, "post_json", None)),
319
387
  ]
388
+ if not is_sse_transport:
389
+ entries.append(("POST (JSON)", "▶", getattr(snapshot, "post_json", None)))
320
390
  elif stdio_channel is not None:
321
391
  # STDIO transport - show single bidirectional channel
322
392
  entries = [
@@ -332,36 +402,31 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
332
402
  # Determine if we're showing stdio or HTTP channels
333
403
  is_stdio = stdio_channel is not None
334
404
 
405
+ default_bucket_seconds = getattr(snapshot, "activity_bucket_seconds", None) or 30
406
+ default_bucket_count = getattr(snapshot, "activity_bucket_count", None) or 20
407
+ timeline_header_label = _format_timeline_label(default_bucket_seconds * default_bucket_count)
408
+
409
+ # Total characters before the metrics section in each row (excluding indent)
410
+ # Structure: "│ " + arrow + " " + label(13) + timeline_label + " " + buckets + " now"
411
+ metrics_prefix_width = 22 + len(timeline_header_label) + default_bucket_count
412
+
335
413
  # Get transport type for display
336
- transport = getattr(status, "transport", None) or "unknown"
414
+ transport = transport_value or "unknown"
337
415
  transport_display = transport.upper() if transport != "unknown" else "Channels"
338
416
 
339
417
  # Header with column labels
340
418
  header = Text(indent)
341
- header.append(f"┌ {transport_display} ", style="dim")
419
+ header_intro = f"┌ {transport_display} "
420
+ header.append(header_intro, style="dim")
342
421
 
343
422
  # Calculate padding needed based on transport display length
344
- # Base structure: "┌ " (2) + transport_display + " " (1) + "─" padding to align with columns
345
- header_prefix_len = 3 + len(transport_display)
423
+ header_prefix_len = len(header_intro)
346
424
 
425
+ dash_count = max(1, metrics_prefix_width - header_prefix_len + 2)
347
426
  if is_stdio:
348
- # Simplified header for stdio: just activity column
349
- # Need to align with "│ ⇄ STDIO 10m ●●●●●●●●●●●●●●●●●●●● now 29"
350
- # That's: "│ " + arrow + " " + label(13) + "10m " + dots(20) + " now" = 47 chars
351
- # Then: " " + activity(8) = 10 chars
352
- # Total content width = 47 + 10 = 57 chars
353
- # So we need 47 - header_prefix_len dashes before "activity"
354
- dash_count = max(1, 47 - header_prefix_len)
355
427
  header.append("─" * dash_count, style="dim")
356
428
  header.append(" activity", style="dim")
357
429
  else:
358
- # Original header for HTTP channels
359
- # Need to align with the req/resp/notif/ping columns
360
- # Structure: "│ " + arrow + " " + label(13) + "10m " + dots(20) + " now" = 47 chars
361
- # Then: " " + req(5) + " " + resp(5) + " " + notif(5) + " " + ping(5) = 25 chars
362
- # Total content width = 47 + 25 = 72 chars
363
- # So we need 47 - header_prefix_len dashes before the column headers
364
- dash_count = max(1, 47 - header_prefix_len)
365
430
  header.append("─" * dash_count, style="dim")
366
431
  header.append(" req resp notif ping", style="dim")
367
432
 
@@ -449,10 +514,24 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
449
514
  line.append(f" {label:<13}", style=label_style)
450
515
 
451
516
  # Always show timeline (dim black dots if no data)
452
- line.append("10m ", style="dim")
453
- if channel and channel.activity_buckets:
517
+ channel_bucket_seconds = (
518
+ getattr(channel, "activity_bucket_seconds", None) or default_bucket_seconds
519
+ )
520
+ bucket_count = (
521
+ len(channel.activity_buckets)
522
+ if channel and channel.activity_buckets
523
+ else getattr(channel, "activity_bucket_count", None)
524
+ )
525
+ if not bucket_count or bucket_count <= 0:
526
+ bucket_count = default_bucket_count
527
+ total_window_seconds = channel_bucket_seconds * bucket_count
528
+ timeline_label = _format_timeline_label(total_window_seconds)
529
+
530
+ line.append(f"{timeline_label} ", style="dim")
531
+ bucket_states = channel.activity_buckets if channel and channel.activity_buckets else None
532
+ if bucket_states:
454
533
  # Show actual activity
455
- for bucket_state in channel.activity_buckets:
534
+ for bucket_state in bucket_states:
456
535
  color = timeline_color_map.get(bucket_state, "dim")
457
536
  if bucket_state in {"idle", "none"}:
458
537
  symbol = SYMBOL_IDLE
@@ -473,7 +552,7 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
473
552
  line.append(symbol, style=f"bold {color}")
474
553
  else:
475
554
  # Show dim dots for no activity
476
- for _ in range(20):
555
+ for _ in range(bucket_count):
477
556
  line.append(SYMBOL_IDLE, style="black dim")
478
557
  line.append(" now", style="dim")
479
558
 
@@ -588,6 +667,8 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
588
667
  symbol = SYMBOL_ERROR
589
668
  elif name == "ping":
590
669
  symbol = SYMBOL_PING
670
+ elif is_stdio and name == "activity":
671
+ symbol = SYMBOL_STDIO_ACTIVITY
591
672
  else:
592
673
  symbol = SYMBOL_RESPONSE
593
674
  footer.append(symbol, style=f"{color}")
@@ -712,7 +793,7 @@ async def render_mcp_status(agent, indent: str = "") -> None:
712
793
  if instr_available and status.instructions_enabled is False:
713
794
  state_segments.append(Text("instructions disabled", style=Colours.TEXT_ERROR))
714
795
  elif instr_available and not template_expected:
715
- state_segments.append(Text("template missing", style=Colours.TEXT_WARNING))
796
+ state_segments.append(Text("instr. not in sysprompt", style=Colours.TEXT_WARNING))
716
797
 
717
798
  if status.spoofing_enabled:
718
799
  state_segments.append(Text("client spoof", style=Colours.TEXT_WARNING))
@@ -0,0 +1,68 @@
1
+ """High performance truncation for plain text streaming displays."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+
8
+ class PlainTextTruncator:
9
+ """Trim plain text content to fit within a target terminal window."""
10
+
11
+ def __init__(self, target_height_ratio: float = 0.7) -> None:
12
+ if not 0 < target_height_ratio <= 1:
13
+ raise ValueError("target_height_ratio must be between 0 and 1")
14
+ self.target_height_ratio = target_height_ratio
15
+
16
+ def truncate(self, text: str, *, terminal_height: int, terminal_width: int) -> str:
17
+ """Return the most recent portion of text that fits the terminal window.
18
+
19
+ Args:
20
+ text: Full text buffer accumulated during streaming.
21
+ terminal_height: Terminal height in rows.
22
+ terminal_width: Terminal width in columns.
23
+
24
+ Returns:
25
+ Tail portion of the text that fits within the target height ratio.
26
+ """
27
+ if not text:
28
+ return text
29
+
30
+ if terminal_height <= 0 or terminal_width <= 0:
31
+ return text
32
+
33
+ target_rows = max(1, int(terminal_height * self.target_height_ratio))
34
+ width = max(1, terminal_width)
35
+
36
+ idx = len(text)
37
+ rows_used = 0
38
+ start_idx = 0
39
+
40
+ while idx > 0 and rows_used < target_rows:
41
+ prev_newline = text.rfind("\n", 0, idx)
42
+ line_start = prev_newline + 1 if prev_newline != -1 else 0
43
+ line = text[line_start:idx]
44
+ expanded = line.expandtabs()
45
+ line_len = len(expanded)
46
+ line_rows = max(1, math.ceil(line_len / width)) if line_len else 1
47
+
48
+ if rows_used + line_rows >= target_rows:
49
+ rows_remaining = target_rows - rows_used
50
+ if rows_remaining <= 0:
51
+ start_idx = idx
52
+ break
53
+
54
+ if line_rows <= rows_remaining:
55
+ start_idx = line_start
56
+ else:
57
+ approx_chars = width * rows_remaining
58
+ keep_chars = min(len(line), approx_chars)
59
+ start_idx = idx - keep_chars
60
+ break
61
+
62
+ rows_used += line_rows
63
+ start_idx = line_start
64
+ if prev_newline == -1:
65
+ break
66
+ idx = prev_newline
67
+
68
+ return text[start_idx:]
@@ -78,6 +78,7 @@ class RichProgressDisplay:
78
78
  ProgressAction.INITIALIZED: "dim green",
79
79
  ProgressAction.CHATTING: "bold blue",
80
80
  ProgressAction.STREAMING: "bold green", # Assistant Colour
81
+ ProgressAction.THINKING: "bold yellow", # Assistant Colour
81
82
  ProgressAction.ROUTING: "bold blue",
82
83
  ProgressAction.PLANNING: "bold blue",
83
84
  ProgressAction.READY: "dim green",
@@ -108,7 +109,9 @@ class RichProgressDisplay:
108
109
 
109
110
  # Ensure no None values in the update
110
111
  # For streaming, use custom description immediately to avoid flashing
111
- if event.action == ProgressAction.STREAMING and event.streaming_tokens:
112
+ if (
113
+ event.action == ProgressAction.STREAMING or event.action == ProgressAction.THINKING
114
+ ) and event.streaming_tokens:
112
115
  # Account for [dim][/dim] tags (11 characters) in padding calculation
113
116
  formatted_tokens = f"▎[dim]◀[/dim] {event.streaming_tokens.strip()}".ljust(17 + 11)
114
117
  description = f"[{self._get_action_style(event.action)}]{formatted_tokens}"