fast-agent-mcp 0.3.14__py3-none-any.whl → 0.3.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fast-agent-mcp might be problematic. Click here for more details.
- fast_agent/__init__.py +2 -0
- fast_agent/agents/agent_types.py +5 -0
- fast_agent/agents/llm_agent.py +52 -4
- fast_agent/agents/llm_decorator.py +6 -0
- fast_agent/agents/mcp_agent.py +137 -13
- fast_agent/agents/tool_agent.py +33 -19
- fast_agent/agents/workflow/router_agent.py +2 -1
- fast_agent/cli/__main__.py +35 -0
- fast_agent/cli/commands/check_config.py +90 -2
- fast_agent/cli/commands/go.py +100 -36
- fast_agent/cli/constants.py +13 -1
- fast_agent/cli/main.py +1 -0
- fast_agent/config.py +41 -12
- fast_agent/constants.py +8 -0
- fast_agent/context.py +24 -15
- fast_agent/core/direct_decorators.py +9 -0
- fast_agent/core/fastagent.py +115 -2
- fast_agent/core/logging/listeners.py +8 -0
- fast_agent/core/validation.py +31 -33
- fast_agent/human_input/form_fields.py +4 -1
- fast_agent/interfaces.py +12 -1
- fast_agent/llm/fastagent_llm.py +76 -0
- fast_agent/llm/memory.py +26 -1
- fast_agent/llm/model_database.py +2 -2
- fast_agent/llm/model_factory.py +4 -1
- fast_agent/llm/provider/anthropic/llm_anthropic.py +112 -0
- fast_agent/llm/provider/openai/llm_openai.py +184 -18
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/mcp/prompt_message_extended.py +2 -2
- fast_agent/resources/setup/agent.py +2 -0
- fast_agent/resources/setup/fastagent.config.yaml +11 -4
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +200 -0
- fast_agent/tools/shell_runtime.py +404 -0
- fast_agent/ui/console_display.py +925 -73
- fast_agent/ui/elicitation_form.py +98 -24
- fast_agent/ui/elicitation_style.py +2 -2
- fast_agent/ui/enhanced_prompt.py +128 -26
- fast_agent/ui/history_display.py +20 -5
- fast_agent/ui/interactive_prompt.py +108 -3
- fast_agent/ui/markdown_truncator.py +942 -0
- fast_agent/ui/mcp_display.py +2 -2
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/METADATA +9 -7
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/RECORD +49 -42
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.14.dist-info → fast_agent_mcp-0.3.16.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""Simplified, robust elicitation form dialog."""
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from datetime import date, datetime
|
|
4
5
|
from typing import Any, Dict, Optional
|
|
5
6
|
|
|
6
7
|
from mcp.types import ElicitRequestedSchema
|
|
7
8
|
from prompt_toolkit import Application
|
|
8
9
|
from prompt_toolkit.buffer import Buffer
|
|
10
|
+
from prompt_toolkit.filters import Condition
|
|
9
11
|
from prompt_toolkit.formatted_text import FormattedText
|
|
10
12
|
from prompt_toolkit.key_binding import KeyBindings
|
|
11
13
|
from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
|
|
@@ -24,6 +26,8 @@ from pydantic import ValidationError as PydanticValidationError
|
|
|
24
26
|
|
|
25
27
|
from fast_agent.ui.elicitation_style import ELICITATION_STYLE
|
|
26
28
|
|
|
29
|
+
text_navigation_mode = False
|
|
30
|
+
|
|
27
31
|
|
|
28
32
|
class SimpleNumberValidator(Validator):
|
|
29
33
|
"""Simple number validator with real-time feedback."""
|
|
@@ -63,9 +67,15 @@ class SimpleNumberValidator(Validator):
|
|
|
63
67
|
class SimpleStringValidator(Validator):
|
|
64
68
|
"""Simple string validator with real-time feedback."""
|
|
65
69
|
|
|
66
|
-
def __init__(
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
min_length: Optional[int] = None,
|
|
73
|
+
max_length: Optional[int] = None,
|
|
74
|
+
pattern: Optional[str] = None,
|
|
75
|
+
):
|
|
67
76
|
self.min_length = min_length
|
|
68
77
|
self.max_length = max_length
|
|
78
|
+
self.pattern = re.compile(pattern, re.DOTALL) if pattern else None
|
|
69
79
|
|
|
70
80
|
def validate(self, document):
|
|
71
81
|
text = document.text
|
|
@@ -83,6 +93,12 @@ class SimpleStringValidator(Validator):
|
|
|
83
93
|
cursor_position=self.max_length,
|
|
84
94
|
)
|
|
85
95
|
|
|
96
|
+
if self.pattern is not None and self.pattern.fullmatch(text) is None:
|
|
97
|
+
# TODO: Wrap or truncate line if too long
|
|
98
|
+
raise ValidationError(
|
|
99
|
+
message=f"Must match pattern '{self.pattern.pattern}'", cursor_position=len(text)
|
|
100
|
+
)
|
|
101
|
+
|
|
86
102
|
|
|
87
103
|
class FormatValidator(Validator):
|
|
88
104
|
"""Format-specific validator using Pydantic validators."""
|
|
@@ -301,6 +317,10 @@ class ElicitationForm:
|
|
|
301
317
|
]
|
|
302
318
|
)
|
|
303
319
|
|
|
320
|
+
# Use field navigation mode as default
|
|
321
|
+
global text_navigation_mode
|
|
322
|
+
text_navigation_mode = False
|
|
323
|
+
|
|
304
324
|
# Key bindings
|
|
305
325
|
kb = KeyBindings()
|
|
306
326
|
|
|
@@ -312,31 +332,49 @@ class ElicitationForm:
|
|
|
312
332
|
def focus_previous_with_refresh(event):
|
|
313
333
|
focus_previous(event)
|
|
314
334
|
|
|
335
|
+
# Toggle between text navigation mode and field navigation mode
|
|
336
|
+
@kb.add("c-t")
|
|
337
|
+
def toggle_text_navigation_mode(event):
|
|
338
|
+
global text_navigation_mode
|
|
339
|
+
text_navigation_mode = not text_navigation_mode
|
|
340
|
+
event.app.invalidate() # Force redraw the app to update toolbar
|
|
341
|
+
|
|
315
342
|
# Arrow key navigation - let radio lists handle up/down first
|
|
316
|
-
@kb.add("down")
|
|
343
|
+
@kb.add("down", filter=Condition(lambda: not text_navigation_mode))
|
|
317
344
|
def focus_next_arrow(event):
|
|
318
345
|
focus_next(event)
|
|
319
346
|
|
|
320
|
-
@kb.add("up")
|
|
347
|
+
@kb.add("up", filter=Condition(lambda: not text_navigation_mode))
|
|
321
348
|
def focus_previous_arrow(event):
|
|
322
349
|
focus_previous(event)
|
|
323
350
|
|
|
324
|
-
@kb.add("right", eager=True)
|
|
351
|
+
@kb.add("right", eager=True, filter=Condition(lambda: not text_navigation_mode))
|
|
325
352
|
def focus_next_right(event):
|
|
326
353
|
focus_next(event)
|
|
327
354
|
|
|
328
|
-
@kb.add("left", eager=True)
|
|
355
|
+
@kb.add("left", eager=True, filter=Condition(lambda: not text_navigation_mode))
|
|
329
356
|
def focus_previous_left(event):
|
|
330
357
|
focus_previous(event)
|
|
331
358
|
|
|
332
|
-
# Enter
|
|
333
|
-
@kb.add("c-m")
|
|
334
|
-
def
|
|
359
|
+
# Enter submits in field navigation mode
|
|
360
|
+
@kb.add("c-m", filter=Condition(lambda: not text_navigation_mode))
|
|
361
|
+
def submit_enter(event):
|
|
335
362
|
self._accept()
|
|
336
363
|
|
|
337
|
-
# Ctrl+J inserts newlines
|
|
338
|
-
@kb.add("c-j")
|
|
339
|
-
def
|
|
364
|
+
# Ctrl+J inserts newlines in field navigation mode
|
|
365
|
+
@kb.add("c-j", filter=Condition(lambda: not text_navigation_mode))
|
|
366
|
+
def insert_newline_cj(event):
|
|
367
|
+
# Insert a newline at the cursor position
|
|
368
|
+
event.current_buffer.insert_text("\n")
|
|
369
|
+
# Mark this field as multiline when user adds a newline
|
|
370
|
+
for field_name, widget in self.field_widgets.items():
|
|
371
|
+
if isinstance(widget, Buffer) and widget == event.current_buffer:
|
|
372
|
+
self.multiline_fields.add(field_name)
|
|
373
|
+
break
|
|
374
|
+
|
|
375
|
+
# Enter inserts new lines in text navigation mode
|
|
376
|
+
@kb.add("c-m", filter=Condition(lambda: text_navigation_mode))
|
|
377
|
+
def insert_newline_enter(event):
|
|
340
378
|
# Insert a newline at the cursor position
|
|
341
379
|
event.current_buffer.insert_text("\n")
|
|
342
380
|
# Mark this field as multiline when user adds a newline
|
|
@@ -345,6 +383,11 @@ class ElicitationForm:
|
|
|
345
383
|
self.multiline_fields.add(field_name)
|
|
346
384
|
break
|
|
347
385
|
|
|
386
|
+
# deactivate ctrl+j in text navigation mode
|
|
387
|
+
@kb.add("c-j", filter=Condition(lambda: text_navigation_mode))
|
|
388
|
+
def _(event):
|
|
389
|
+
pass
|
|
390
|
+
|
|
348
391
|
# ESC should ALWAYS cancel immediately, no matter what
|
|
349
392
|
@kb.add("escape", eager=True, is_global=True)
|
|
350
393
|
def cancel(event):
|
|
@@ -356,18 +399,40 @@ class ElicitationForm:
|
|
|
356
399
|
if hasattr(self, "_toolbar_hidden") and self._toolbar_hidden:
|
|
357
400
|
return FormattedText([])
|
|
358
401
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
402
|
+
mode_label = "TEXT MODE" if text_navigation_mode else "FIELD MODE"
|
|
403
|
+
mode_color = "ansired" if text_navigation_mode else "ansigreen"
|
|
404
|
+
|
|
405
|
+
arrow_up = "↑"
|
|
406
|
+
arrow_down = "↓"
|
|
407
|
+
arrow_left = "←"
|
|
408
|
+
arrow_right = "→"
|
|
409
|
+
|
|
410
|
+
if text_navigation_mode:
|
|
411
|
+
actions_line = (
|
|
412
|
+
" <ESC> cancel. <Cancel All> Auto-Cancel further elicitations from this Server."
|
|
413
|
+
)
|
|
414
|
+
navigation_tail = (
|
|
415
|
+
" | <CTRL+T> toggle text mode. <TAB> navigate. <ENTER> insert new line."
|
|
416
|
+
)
|
|
417
|
+
else:
|
|
418
|
+
actions_line = (
|
|
419
|
+
" <ENTER> submit. <ESC> cancel. <Cancel All> Auto-Cancel further elicitations "
|
|
420
|
+
"from this Server."
|
|
421
|
+
)
|
|
422
|
+
navigation_tail = (
|
|
423
|
+
" | <CTRL+T> toggle text mode. "
|
|
424
|
+
f"<TAB>/{arrow_up}{arrow_down}{arrow_right}{arrow_left} navigate. "
|
|
425
|
+
"<Ctrl+J> insert new line."
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
formatted_segments = [
|
|
429
|
+
("class:bottom-toolbar.text", actions_line),
|
|
430
|
+
("", "\n"),
|
|
431
|
+
("class:bottom-toolbar.text", " | "),
|
|
432
|
+
(f"fg:{mode_color} bg:ansiblack", f" {mode_label} "),
|
|
433
|
+
("class:bottom-toolbar.text", navigation_tail),
|
|
434
|
+
]
|
|
435
|
+
return FormattedText(formatted_segments)
|
|
371
436
|
|
|
372
437
|
# Store toolbar function reference for later control
|
|
373
438
|
self._get_toolbar = get_toolbar
|
|
@@ -375,7 +440,7 @@ class ElicitationForm:
|
|
|
375
440
|
|
|
376
441
|
# Create toolbar window that we can reference later
|
|
377
442
|
self._toolbar_window = Window(
|
|
378
|
-
FormattedTextControl(get_toolbar), height=
|
|
443
|
+
FormattedTextControl(get_toolbar), height=2, style="class:bottom-toolbar"
|
|
379
444
|
)
|
|
380
445
|
|
|
381
446
|
# Add toolbar to the layout
|
|
@@ -429,6 +494,8 @@ class ElicitationForm:
|
|
|
429
494
|
constraints["minLength"] = field_def["minLength"]
|
|
430
495
|
if field_def.get("maxLength") is not None:
|
|
431
496
|
constraints["maxLength"] = field_def["maxLength"]
|
|
497
|
+
if field_def.get("pattern") is not None:
|
|
498
|
+
constraints["pattern"] = field_def["pattern"]
|
|
432
499
|
|
|
433
500
|
# Check anyOf constraints (for Optional fields)
|
|
434
501
|
if "anyOf" in field_def:
|
|
@@ -438,6 +505,8 @@ class ElicitationForm:
|
|
|
438
505
|
constraints["minLength"] = variant["minLength"]
|
|
439
506
|
if variant.get("maxLength") is not None:
|
|
440
507
|
constraints["maxLength"] = variant["maxLength"]
|
|
508
|
+
if variant.get("pattern") is not None:
|
|
509
|
+
constraints["pattern"] = variant["pattern"]
|
|
441
510
|
break
|
|
442
511
|
|
|
443
512
|
return constraints
|
|
@@ -468,6 +537,10 @@ class ElicitationForm:
|
|
|
468
537
|
if constraints.get("maxLength"):
|
|
469
538
|
hints.append(f"max {constraints['maxLength']} chars")
|
|
470
539
|
|
|
540
|
+
if constraints.get("pattern"):
|
|
541
|
+
# TODO: Wrap or truncate line if too long
|
|
542
|
+
format_hint = f"Pattern: {constraints['pattern']}"
|
|
543
|
+
|
|
471
544
|
# Handle format hints separately (these go on next line)
|
|
472
545
|
format_type = field_def.get("format")
|
|
473
546
|
if format_type:
|
|
@@ -545,6 +618,7 @@ class ElicitationForm:
|
|
|
545
618
|
validator = SimpleStringValidator(
|
|
546
619
|
min_length=constraints.get("minLength"),
|
|
547
620
|
max_length=constraints.get("maxLength"),
|
|
621
|
+
pattern=constraints.get("pattern"),
|
|
548
622
|
)
|
|
549
623
|
else:
|
|
550
624
|
constraints = {}
|
|
@@ -53,7 +53,7 @@ ELICITATION_STYLE = Style.from_dict(
|
|
|
53
53
|
"completion-menu.meta.completion": "bg:ansiblack fg:ansiblue",
|
|
54
54
|
"completion-menu.meta.completion.current": "bg:ansibrightblack fg:ansiblue",
|
|
55
55
|
# Toolbar - matching enhanced_prompt.py exactly
|
|
56
|
-
"bottom-toolbar": "fg
|
|
57
|
-
"bottom-toolbar.text": "fg
|
|
56
|
+
"bottom-toolbar": "fg:#ansiblack bg:#ansigray",
|
|
57
|
+
"bottom-toolbar.text": "fg:#ansiblack bg:#ansigray",
|
|
58
58
|
}
|
|
59
59
|
)
|
fast_agent/ui/enhanced_prompt.py
CHANGED
|
@@ -121,6 +121,14 @@ async def _display_agent_info_helper(agent_name: str, agent_provider: "AgentApp
|
|
|
121
121
|
prompts_dict = await agent.list_prompts()
|
|
122
122
|
prompt_count = sum(len(prompts) for prompts in prompts_dict.values()) if prompts_dict else 0
|
|
123
123
|
|
|
124
|
+
skill_count = 0
|
|
125
|
+
skill_manifests = getattr(agent, "_skill_manifests", None)
|
|
126
|
+
if skill_manifests:
|
|
127
|
+
try:
|
|
128
|
+
skill_count = len(list(skill_manifests))
|
|
129
|
+
except TypeError:
|
|
130
|
+
skill_count = 0
|
|
131
|
+
|
|
124
132
|
# Handle different agent types
|
|
125
133
|
if agent.agent_type == AgentType.PARALLEL:
|
|
126
134
|
# Count child agents for parallel agents
|
|
@@ -149,36 +157,38 @@ async def _display_agent_info_helper(agent_name: str, agent_provider: "AgentApp
|
|
|
149
157
|
f"[dim]Agent [/dim][blue]{agent_name}[/blue][dim]:[/dim] {child_count:,}[dim] {child_word}[/dim]"
|
|
150
158
|
)
|
|
151
159
|
else:
|
|
152
|
-
|
|
153
|
-
if server_count > 0:
|
|
154
|
-
# Build display parts in order: tools, prompts, resources (omit if count is 0)
|
|
155
|
-
display_parts = []
|
|
160
|
+
content_parts = []
|
|
156
161
|
|
|
162
|
+
if server_count > 0:
|
|
163
|
+
sub_parts = []
|
|
157
164
|
if tool_count > 0:
|
|
158
165
|
tool_word = "tool" if tool_count == 1 else "tools"
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
sub_parts.append(f"{tool_count:,}[dim] {tool_word}[/dim]")
|
|
161
167
|
if prompt_count > 0:
|
|
162
168
|
prompt_word = "prompt" if prompt_count == 1 else "prompts"
|
|
163
|
-
|
|
164
|
-
|
|
169
|
+
sub_parts.append(f"{prompt_count:,}[dim] {prompt_word}[/dim]")
|
|
165
170
|
if resource_count > 0:
|
|
166
171
|
resource_word = "resource" if resource_count == 1 else "resources"
|
|
167
|
-
|
|
172
|
+
sub_parts.append(f"{resource_count:,}[dim] {resource_word}[/dim]")
|
|
168
173
|
|
|
169
|
-
# Always show server count
|
|
170
174
|
server_word = "Server" if server_count == 1 else "Servers"
|
|
171
175
|
server_text = f"{server_count:,}[dim] MCP {server_word}[/dim]"
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
+ "[dim]
|
|
177
|
-
+ "[dim] available[/dim]"
|
|
176
|
+
if sub_parts:
|
|
177
|
+
server_text = (
|
|
178
|
+
f"{server_text}[dim] ([/dim]"
|
|
179
|
+
+ "[dim], [/dim]".join(sub_parts)
|
|
180
|
+
+ "[dim])[/dim]"
|
|
178
181
|
)
|
|
179
|
-
|
|
180
|
-
content = f"{server_text}[dim] available[/dim]"
|
|
182
|
+
content_parts.append(server_text)
|
|
181
183
|
|
|
184
|
+
if skill_count > 0:
|
|
185
|
+
skill_word = "skill" if skill_count == 1 else "skills"
|
|
186
|
+
content_parts.append(
|
|
187
|
+
f"{skill_count:,}[dim] {skill_word}[/dim][dim] available[/dim]"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if content_parts:
|
|
191
|
+
content = "[dim]. [/dim]".join(content_parts)
|
|
182
192
|
rich_print(f"[dim]Agent [/dim][blue]{agent_name}[/blue][dim]:[/dim] {content}")
|
|
183
193
|
# await _render_mcp_status(agent)
|
|
184
194
|
|
|
@@ -351,9 +361,11 @@ class AgentCompleter(Completer):
|
|
|
351
361
|
self.commands = {
|
|
352
362
|
"mcp": "Show MCP server status",
|
|
353
363
|
"history": "Show conversation history overview (optionally another agent)",
|
|
354
|
-
"tools": "List available MCP
|
|
364
|
+
"tools": "List available MCP Tools",
|
|
365
|
+
"skills": "List available Agent Skills",
|
|
355
366
|
"prompt": "List and choose MCP prompts, or apply specific prompt (/prompt <name>)",
|
|
356
367
|
"clear": "Clear history",
|
|
368
|
+
"clear last": "Remove the most recent message from history",
|
|
357
369
|
"agents": "List available agents",
|
|
358
370
|
"system": "Show the current system prompt",
|
|
359
371
|
"usage": "Show current usage statistics",
|
|
@@ -684,20 +696,26 @@ async def get_enhanced_input(
|
|
|
684
696
|
if llm:
|
|
685
697
|
model_name = getattr(llm, "model_name", None)
|
|
686
698
|
if not model_name:
|
|
687
|
-
model_name = getattr(
|
|
699
|
+
model_name = getattr(
|
|
700
|
+
getattr(llm, "default_request_params", None), "model", None
|
|
701
|
+
)
|
|
688
702
|
|
|
689
703
|
if not model_name:
|
|
690
704
|
model_name = getattr(agent.config, "model", None)
|
|
691
705
|
if not model_name and getattr(agent.config, "default_request_params", None):
|
|
692
706
|
model_name = getattr(agent.config.default_request_params, "model", None)
|
|
693
707
|
if not model_name:
|
|
694
|
-
context = getattr(agent, "context", None) or getattr(
|
|
708
|
+
context = getattr(agent, "context", None) or getattr(
|
|
709
|
+
agent_provider, "context", None
|
|
710
|
+
)
|
|
695
711
|
config_obj = getattr(context, "config", None) if context else None
|
|
696
712
|
model_name = getattr(config_obj, "default_model", None)
|
|
697
713
|
|
|
698
714
|
if model_name:
|
|
699
715
|
max_len = 25
|
|
700
|
-
model_display =
|
|
716
|
+
model_display = (
|
|
717
|
+
model_name[: max_len - 1] + "…" if len(model_name) > max_len else model_name
|
|
718
|
+
)
|
|
701
719
|
else:
|
|
702
720
|
print(f"[toolbar debug] no model resolved for agent '{agent_name}'")
|
|
703
721
|
model_display = "unknown"
|
|
@@ -848,8 +866,34 @@ async def get_enhanced_input(
|
|
|
848
866
|
)
|
|
849
867
|
session.app.key_bindings = bindings
|
|
850
868
|
|
|
869
|
+
shell_agent = None
|
|
870
|
+
shell_enabled = False
|
|
871
|
+
shell_access_modes: tuple[str, ...] = ()
|
|
872
|
+
shell_name: str | None = None
|
|
873
|
+
if agent_provider:
|
|
874
|
+
try:
|
|
875
|
+
shell_agent = agent_provider._agent(agent_name)
|
|
876
|
+
except Exception:
|
|
877
|
+
shell_agent = None
|
|
878
|
+
|
|
879
|
+
if shell_agent:
|
|
880
|
+
shell_enabled = bool(getattr(shell_agent, "_shell_runtime_enabled", False))
|
|
881
|
+
modes_attr = getattr(shell_agent, "_shell_access_modes", ())
|
|
882
|
+
if isinstance(modes_attr, (list, tuple)):
|
|
883
|
+
shell_access_modes = tuple(str(mode) for mode in modes_attr)
|
|
884
|
+
elif modes_attr:
|
|
885
|
+
shell_access_modes = (str(modes_attr),)
|
|
886
|
+
|
|
887
|
+
# Get the detected shell name from the runtime
|
|
888
|
+
if shell_enabled:
|
|
889
|
+
shell_runtime = getattr(shell_agent, "_shell_runtime", None)
|
|
890
|
+
if shell_runtime:
|
|
891
|
+
runtime_info = shell_runtime.runtime_info()
|
|
892
|
+
shell_name = runtime_info.get("name")
|
|
893
|
+
|
|
851
894
|
# Create formatted prompt text
|
|
852
|
-
|
|
895
|
+
arrow_segment = "<ansibrightyellow>❯</ansibrightyellow>" if shell_enabled else "❯"
|
|
896
|
+
prompt_text = f"<ansibrightblue>{agent_name}</ansibrightblue> {arrow_segment} "
|
|
853
897
|
|
|
854
898
|
# Add default value display if requested
|
|
855
899
|
if show_default and default and default != "STOP":
|
|
@@ -876,6 +920,53 @@ async def get_enhanced_input(
|
|
|
876
920
|
# Display info for all available agents with tree structure for workflows
|
|
877
921
|
await _display_all_agents_with_hierarchy(available_agents, agent_provider)
|
|
878
922
|
|
|
923
|
+
# Show streaming status message
|
|
924
|
+
if agent_provider:
|
|
925
|
+
# Get logger settings from the agent's context (not agent_provider)
|
|
926
|
+
logger_settings = None
|
|
927
|
+
try:
|
|
928
|
+
active_agent = shell_agent
|
|
929
|
+
if active_agent is None:
|
|
930
|
+
active_agent = agent_provider._agent(agent_name)
|
|
931
|
+
agent_context = active_agent._context or active_agent.context
|
|
932
|
+
logger_settings = agent_context.config.logger
|
|
933
|
+
except Exception:
|
|
934
|
+
# If we can't get the agent or its context, logger_settings stays None
|
|
935
|
+
pass
|
|
936
|
+
|
|
937
|
+
# Only show streaming messages if chat display is enabled AND we have logger_settings
|
|
938
|
+
if logger_settings:
|
|
939
|
+
show_chat = getattr(logger_settings, "show_chat", True)
|
|
940
|
+
|
|
941
|
+
if show_chat:
|
|
942
|
+
# Check for parallel agents
|
|
943
|
+
has_parallel = any(
|
|
944
|
+
agent.agent_type == AgentType.PARALLEL
|
|
945
|
+
for agent in agent_provider._agents.values()
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
# Note: streaming may have been disabled by fastagent.py if parallel agents exist
|
|
949
|
+
# So we check has_parallel first to show the appropriate message
|
|
950
|
+
if has_parallel:
|
|
951
|
+
# Streaming is disabled due to parallel agents
|
|
952
|
+
rich_print(
|
|
953
|
+
"[dim]Markdown Streaming disabled (Parallel Agents configured)[/dim]"
|
|
954
|
+
)
|
|
955
|
+
else:
|
|
956
|
+
# Check if streaming is enabled
|
|
957
|
+
streaming_enabled = getattr(logger_settings, "streaming_display", True)
|
|
958
|
+
streaming_mode = getattr(logger_settings, "streaming", "markdown")
|
|
959
|
+
if streaming_enabled and streaming_mode != "none":
|
|
960
|
+
# Streaming is enabled - notify users since it's experimental
|
|
961
|
+
rich_print(
|
|
962
|
+
f"[dim]Experimental: Streaming Enabled - {streaming_mode} mode[/dim]"
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
if shell_enabled:
|
|
966
|
+
modes_display = ", ".join(shell_access_modes or ("direct",))
|
|
967
|
+
shell_display = f"{modes_display}, {shell_name}" if shell_name else modes_display
|
|
968
|
+
rich_print(f"[yellow]Shell Access ({shell_display})[/yellow]")
|
|
969
|
+
|
|
879
970
|
rich_print()
|
|
880
971
|
help_message_shown = True
|
|
881
972
|
|
|
@@ -907,9 +998,16 @@ async def get_enhanced_input(
|
|
|
907
998
|
elif cmd == "clear":
|
|
908
999
|
target_agent = None
|
|
909
1000
|
if len(cmd_parts) > 1:
|
|
910
|
-
|
|
911
|
-
if
|
|
912
|
-
|
|
1001
|
+
remainder = cmd_parts[1].strip()
|
|
1002
|
+
if remainder:
|
|
1003
|
+
tokens = remainder.split(maxsplit=1)
|
|
1004
|
+
if tokens and tokens[0].lower() == "last":
|
|
1005
|
+
if len(tokens) > 1:
|
|
1006
|
+
candidate = tokens[1].strip()
|
|
1007
|
+
if candidate:
|
|
1008
|
+
target_agent = candidate
|
|
1009
|
+
return {"clear_last": {"agent": target_agent}}
|
|
1010
|
+
target_agent = remainder
|
|
913
1011
|
return {"clear_history": {"agent": target_agent}}
|
|
914
1012
|
elif cmd == "markdown":
|
|
915
1013
|
return "MARKDOWN"
|
|
@@ -938,6 +1036,8 @@ async def get_enhanced_input(
|
|
|
938
1036
|
elif cmd == "tools":
|
|
939
1037
|
# Return a dictionary with list_tools action
|
|
940
1038
|
return {"list_tools": True}
|
|
1039
|
+
elif cmd == "skills":
|
|
1040
|
+
return {"list_skills": True}
|
|
941
1041
|
elif cmd == "exit":
|
|
942
1042
|
return "EXIT"
|
|
943
1043
|
elif cmd.lower() == "stop":
|
|
@@ -1101,8 +1201,10 @@ async def handle_special_commands(
|
|
|
1101
1201
|
rich_print(" /system - Show the current system prompt")
|
|
1102
1202
|
rich_print(" /prompt <name> - Apply a specific prompt by name")
|
|
1103
1203
|
rich_print(" /usage - Show current usage statistics")
|
|
1204
|
+
rich_print(" /skills - List local skills for the active agent")
|
|
1104
1205
|
rich_print(" /history [agent_name] - Show chat history overview")
|
|
1105
1206
|
rich_print(" /clear [agent_name] - Clear conversation history (keeps templates)")
|
|
1207
|
+
rich_print(" /clear last [agent_name] - Remove the most recent message from history")
|
|
1106
1208
|
rich_print(" /markdown - Show last assistant message without markdown formatting")
|
|
1107
1209
|
rich_print(" /mcpstatus - Show MCP server status summary for the active agent")
|
|
1108
1210
|
rich_print(" /save_history <filename> - Save current chat history to a file")
|
fast_agent/ui/history_display.py
CHANGED
|
@@ -35,6 +35,7 @@ class Colours:
|
|
|
35
35
|
USER = "blue"
|
|
36
36
|
ASSISTANT = "green"
|
|
37
37
|
TOOL = "magenta"
|
|
38
|
+
TOOL_ERROR = "red"
|
|
38
39
|
HEADER = USER
|
|
39
40
|
TIMELINE_EMPTY = "dim default"
|
|
40
41
|
CONTEXT_SAFE = "green"
|
|
@@ -249,6 +250,7 @@ def _build_history_rows(history: Sequence[PromptMessageExtended]) -> list[dict]:
|
|
|
249
250
|
result_rows: list[dict] = []
|
|
250
251
|
tool_result_total_chars = 0
|
|
251
252
|
tool_result_has_non_text = False
|
|
253
|
+
tool_result_has_error = False
|
|
252
254
|
|
|
253
255
|
if tool_calls:
|
|
254
256
|
names: list[str] = []
|
|
@@ -273,6 +275,8 @@ def _build_history_rows(history: Sequence[PromptMessageExtended]) -> list[dict]:
|
|
|
273
275
|
tool_result_total_chars += result_chars
|
|
274
276
|
tool_result_has_non_text = tool_result_has_non_text or result_non_text
|
|
275
277
|
detail = _format_tool_detail("result→", [tool_name])
|
|
278
|
+
is_error = getattr(result, "isError", False)
|
|
279
|
+
tool_result_has_error = tool_result_has_error or is_error
|
|
276
280
|
result_rows.append(
|
|
277
281
|
{
|
|
278
282
|
"role": "tool",
|
|
@@ -284,6 +288,7 @@ def _build_history_rows(history: Sequence[PromptMessageExtended]) -> list[dict]:
|
|
|
284
288
|
"has_tool_request": False,
|
|
285
289
|
"hide_summary": False,
|
|
286
290
|
"include_in_timeline": False,
|
|
291
|
+
"is_error": is_error,
|
|
287
292
|
}
|
|
288
293
|
)
|
|
289
294
|
if role == "user":
|
|
@@ -308,6 +313,7 @@ def _build_history_rows(history: Sequence[PromptMessageExtended]) -> list[dict]:
|
|
|
308
313
|
if timeline_role == "tool" and tool_result_total_chars > 0:
|
|
309
314
|
row_chars = tool_result_total_chars
|
|
310
315
|
row_non_text = row_non_text or tool_result_has_non_text
|
|
316
|
+
row_is_error = tool_result_has_error
|
|
311
317
|
|
|
312
318
|
rows.append(
|
|
313
319
|
{
|
|
@@ -320,6 +326,7 @@ def _build_history_rows(history: Sequence[PromptMessageExtended]) -> list[dict]:
|
|
|
320
326
|
"has_tool_request": has_tool_request,
|
|
321
327
|
"hide_summary": hide_in_summary,
|
|
322
328
|
"include_in_timeline": include_in_timeline,
|
|
329
|
+
"is_error": row_is_error,
|
|
323
330
|
}
|
|
324
331
|
)
|
|
325
332
|
rows.extend(result_rows)
|
|
@@ -333,12 +340,23 @@ def _aggregate_timeline_entries(rows: Sequence[dict]) -> list[dict]:
|
|
|
333
340
|
"role": row.get("timeline_role", row["role"]),
|
|
334
341
|
"chars": row["chars"],
|
|
335
342
|
"non_text": row["non_text"],
|
|
343
|
+
"is_error": row.get("is_error", False),
|
|
336
344
|
}
|
|
337
345
|
for row in rows
|
|
338
346
|
if row.get("include_in_timeline", True)
|
|
339
347
|
]
|
|
340
348
|
|
|
341
349
|
|
|
350
|
+
def _get_role_color(role: str, *, is_error: bool = False) -> str:
|
|
351
|
+
"""Get the display color for a role, accounting for error states."""
|
|
352
|
+
color_map = {"user": Colours.USER, "assistant": Colours.ASSISTANT, "tool": Colours.TOOL}
|
|
353
|
+
|
|
354
|
+
if role == "tool" and is_error:
|
|
355
|
+
return Colours.TOOL_ERROR
|
|
356
|
+
|
|
357
|
+
return color_map.get(role, "white")
|
|
358
|
+
|
|
359
|
+
|
|
342
360
|
def _shade_block(chars: int, *, non_text: bool, color: str) -> Text:
|
|
343
361
|
if non_text:
|
|
344
362
|
return Text(NON_TEXT_MARKER, style=f"bold {color}")
|
|
@@ -356,12 +374,10 @@ def _shade_block(chars: int, *, non_text: bool, color: str) -> Text:
|
|
|
356
374
|
|
|
357
375
|
|
|
358
376
|
def _build_history_bar(entries: Sequence[dict], width: int = TIMELINE_WIDTH) -> tuple[Text, Text]:
|
|
359
|
-
color_map = {"user": Colours.USER, "assistant": Colours.ASSISTANT, "tool": Colours.TOOL}
|
|
360
|
-
|
|
361
377
|
recent = list(entries[-width:])
|
|
362
378
|
bar = Text(" history |", style="dim")
|
|
363
379
|
for entry in recent:
|
|
364
|
-
color =
|
|
380
|
+
color = _get_role_color(entry["role"], is_error=entry.get("is_error", False))
|
|
365
381
|
bar.append_text(
|
|
366
382
|
_shade_block(entry["chars"], non_text=entry.get("non_text", False), color=color)
|
|
367
383
|
)
|
|
@@ -507,7 +523,6 @@ def display_history_overview(
|
|
|
507
523
|
start_index = len(summary_candidates) - len(summary_rows) + 1
|
|
508
524
|
|
|
509
525
|
role_arrows = {"user": "▶", "assistant": "◀", "tool": "▶"}
|
|
510
|
-
role_styles = {"user": Colours.USER, "assistant": Colours.ASSISTANT, "tool": Colours.TOOL}
|
|
511
526
|
role_labels = {"user": "user", "assistant": "assistant", "tool": "tool result"}
|
|
512
527
|
|
|
513
528
|
try:
|
|
@@ -517,7 +532,7 @@ def display_history_overview(
|
|
|
517
532
|
|
|
518
533
|
for offset, row in enumerate(summary_rows):
|
|
519
534
|
role = row["role"]
|
|
520
|
-
color =
|
|
535
|
+
color = _get_role_color(role, is_error=row.get("is_error", False))
|
|
521
536
|
arrow = role_arrows.get(role, "▶")
|
|
522
537
|
label = role_labels.get(role, role)
|
|
523
538
|
if role == "assistant" and row.get("has_tool_request"):
|