fast-agent-mcp 0.3.15__py3-none-any.whl → 0.3.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fast-agent-mcp might be problematic. Click here for more details.
- fast_agent/__init__.py +2 -0
- fast_agent/agents/agent_types.py +5 -0
- fast_agent/agents/llm_agent.py +7 -0
- fast_agent/agents/llm_decorator.py +6 -0
- fast_agent/agents/mcp_agent.py +134 -10
- fast_agent/cli/__main__.py +35 -0
- fast_agent/cli/commands/check_config.py +85 -0
- fast_agent/cli/commands/go.py +100 -36
- fast_agent/cli/constants.py +15 -1
- fast_agent/cli/main.py +2 -1
- fast_agent/config.py +39 -10
- fast_agent/constants.py +8 -0
- fast_agent/context.py +24 -15
- fast_agent/core/direct_decorators.py +9 -0
- fast_agent/core/fastagent.py +101 -1
- fast_agent/core/logging/listeners.py +8 -0
- fast_agent/interfaces.py +12 -0
- fast_agent/llm/fastagent_llm.py +45 -0
- fast_agent/llm/memory.py +26 -1
- fast_agent/llm/model_database.py +4 -1
- fast_agent/llm/model_factory.py +4 -2
- fast_agent/llm/model_info.py +19 -43
- fast_agent/llm/provider/anthropic/llm_anthropic.py +112 -0
- fast_agent/llm/provider/google/llm_google_native.py +238 -7
- fast_agent/llm/provider/openai/llm_openai.py +382 -19
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/resources/setup/agent.py +2 -0
- fast_agent/resources/setup/fastagent.config.yaml +6 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +208 -0
- fast_agent/tools/shell_runtime.py +404 -0
- fast_agent/ui/console_display.py +47 -996
- fast_agent/ui/elicitation_form.py +76 -24
- fast_agent/ui/elicitation_style.py +2 -2
- fast_agent/ui/enhanced_prompt.py +107 -37
- fast_agent/ui/history_display.py +20 -5
- fast_agent/ui/interactive_prompt.py +108 -3
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +103 -45
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/streaming.py +638 -0
- fast_agent/ui/tool_display.py +417 -0
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/METADATA +8 -7
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/RECORD +47 -39
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.17.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,6 +7,7 @@ from typing import Any, Dict, Optional
|
|
|
7
7
|
from mcp.types import ElicitRequestedSchema
|
|
8
8
|
from prompt_toolkit import Application
|
|
9
9
|
from prompt_toolkit.buffer import Buffer
|
|
10
|
+
from prompt_toolkit.filters import Condition
|
|
10
11
|
from prompt_toolkit.formatted_text import FormattedText
|
|
11
12
|
from prompt_toolkit.key_binding import KeyBindings
|
|
12
13
|
from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
|
|
@@ -25,6 +26,8 @@ from pydantic import ValidationError as PydanticValidationError
|
|
|
25
26
|
|
|
26
27
|
from fast_agent.ui.elicitation_style import ELICITATION_STYLE
|
|
27
28
|
|
|
29
|
+
text_navigation_mode = False
|
|
30
|
+
|
|
28
31
|
|
|
29
32
|
class SimpleNumberValidator(Validator):
|
|
30
33
|
"""Simple number validator with real-time feedback."""
|
|
@@ -68,7 +71,7 @@ class SimpleStringValidator(Validator):
|
|
|
68
71
|
self,
|
|
69
72
|
min_length: Optional[int] = None,
|
|
70
73
|
max_length: Optional[int] = None,
|
|
71
|
-
pattern: Optional[str] = None
|
|
74
|
+
pattern: Optional[str] = None,
|
|
72
75
|
):
|
|
73
76
|
self.min_length = min_length
|
|
74
77
|
self.max_length = max_length
|
|
@@ -314,6 +317,10 @@ class ElicitationForm:
|
|
|
314
317
|
]
|
|
315
318
|
)
|
|
316
319
|
|
|
320
|
+
# Use field navigation mode as default
|
|
321
|
+
global text_navigation_mode
|
|
322
|
+
text_navigation_mode = False
|
|
323
|
+
|
|
317
324
|
# Key bindings
|
|
318
325
|
kb = KeyBindings()
|
|
319
326
|
|
|
@@ -325,31 +332,49 @@ class ElicitationForm:
|
|
|
325
332
|
def focus_previous_with_refresh(event):
|
|
326
333
|
focus_previous(event)
|
|
327
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
|
+
|
|
328
342
|
# Arrow key navigation - let radio lists handle up/down first
|
|
329
|
-
@kb.add("down")
|
|
343
|
+
@kb.add("down", filter=Condition(lambda: not text_navigation_mode))
|
|
330
344
|
def focus_next_arrow(event):
|
|
331
345
|
focus_next(event)
|
|
332
346
|
|
|
333
|
-
@kb.add("up")
|
|
347
|
+
@kb.add("up", filter=Condition(lambda: not text_navigation_mode))
|
|
334
348
|
def focus_previous_arrow(event):
|
|
335
349
|
focus_previous(event)
|
|
336
350
|
|
|
337
|
-
@kb.add("right", eager=True)
|
|
351
|
+
@kb.add("right", eager=True, filter=Condition(lambda: not text_navigation_mode))
|
|
338
352
|
def focus_next_right(event):
|
|
339
353
|
focus_next(event)
|
|
340
354
|
|
|
341
|
-
@kb.add("left", eager=True)
|
|
355
|
+
@kb.add("left", eager=True, filter=Condition(lambda: not text_navigation_mode))
|
|
342
356
|
def focus_previous_left(event):
|
|
343
357
|
focus_previous(event)
|
|
344
358
|
|
|
345
|
-
# Enter
|
|
346
|
-
@kb.add("c-m")
|
|
347
|
-
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):
|
|
348
362
|
self._accept()
|
|
349
363
|
|
|
350
|
-
# Ctrl+J inserts newlines
|
|
351
|
-
@kb.add("c-j")
|
|
352
|
-
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):
|
|
353
378
|
# Insert a newline at the cursor position
|
|
354
379
|
event.current_buffer.insert_text("\n")
|
|
355
380
|
# Mark this field as multiline when user adds a newline
|
|
@@ -358,6 +383,11 @@ class ElicitationForm:
|
|
|
358
383
|
self.multiline_fields.add(field_name)
|
|
359
384
|
break
|
|
360
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
|
+
|
|
361
391
|
# ESC should ALWAYS cancel immediately, no matter what
|
|
362
392
|
@kb.add("escape", eager=True, is_global=True)
|
|
363
393
|
def cancel(event):
|
|
@@ -369,18 +399,40 @@ class ElicitationForm:
|
|
|
369
399
|
if hasattr(self, "_toolbar_hidden") and self._toolbar_hidden:
|
|
370
400
|
return FormattedText([])
|
|
371
401
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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)
|
|
384
436
|
|
|
385
437
|
# Store toolbar function reference for later control
|
|
386
438
|
self._get_toolbar = get_toolbar
|
|
@@ -388,7 +440,7 @@ class ElicitationForm:
|
|
|
388
440
|
|
|
389
441
|
# Create toolbar window that we can reference later
|
|
390
442
|
self._toolbar_window = Window(
|
|
391
|
-
FormattedTextControl(get_toolbar), height=
|
|
443
|
+
FormattedTextControl(get_toolbar), height=2, style="class:bottom-toolbar"
|
|
392
444
|
)
|
|
393
445
|
|
|
394
446
|
# Add toolbar to the layout
|
|
@@ -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
|
@@ -9,6 +9,7 @@ import shlex
|
|
|
9
9
|
import subprocess
|
|
10
10
|
import tempfile
|
|
11
11
|
from importlib.metadata import version
|
|
12
|
+
from pathlib import Path
|
|
12
13
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
13
14
|
|
|
14
15
|
from prompt_toolkit import PromptSession
|
|
@@ -23,7 +24,7 @@ from rich import print as rich_print
|
|
|
23
24
|
from fast_agent.agents.agent_types import AgentType
|
|
24
25
|
from fast_agent.constants import FAST_AGENT_ERROR_CHANNEL, FAST_AGENT_REMOVED_METADATA_CHANNEL
|
|
25
26
|
from fast_agent.core.exceptions import PromptExitError
|
|
26
|
-
from fast_agent.llm.model_info import
|
|
27
|
+
from fast_agent.llm.model_info import ModelInfo
|
|
27
28
|
from fast_agent.mcp.types import McpAgentProtocol
|
|
28
29
|
from fast_agent.ui.mcp_display import render_mcp_status
|
|
29
30
|
|
|
@@ -121,6 +122,14 @@ async def _display_agent_info_helper(agent_name: str, agent_provider: "AgentApp
|
|
|
121
122
|
prompts_dict = await agent.list_prompts()
|
|
122
123
|
prompt_count = sum(len(prompts) for prompts in prompts_dict.values()) if prompts_dict else 0
|
|
123
124
|
|
|
125
|
+
skill_count = 0
|
|
126
|
+
skill_manifests = getattr(agent, "_skill_manifests", None)
|
|
127
|
+
if skill_manifests:
|
|
128
|
+
try:
|
|
129
|
+
skill_count = len(list(skill_manifests))
|
|
130
|
+
except TypeError:
|
|
131
|
+
skill_count = 0
|
|
132
|
+
|
|
124
133
|
# Handle different agent types
|
|
125
134
|
if agent.agent_type == AgentType.PARALLEL:
|
|
126
135
|
# Count child agents for parallel agents
|
|
@@ -149,36 +158,38 @@ async def _display_agent_info_helper(agent_name: str, agent_provider: "AgentApp
|
|
|
149
158
|
f"[dim]Agent [/dim][blue]{agent_name}[/blue][dim]:[/dim] {child_count:,}[dim] {child_word}[/dim]"
|
|
150
159
|
)
|
|
151
160
|
else:
|
|
152
|
-
|
|
153
|
-
if server_count > 0:
|
|
154
|
-
# Build display parts in order: tools, prompts, resources (omit if count is 0)
|
|
155
|
-
display_parts = []
|
|
161
|
+
content_parts = []
|
|
156
162
|
|
|
163
|
+
if server_count > 0:
|
|
164
|
+
sub_parts = []
|
|
157
165
|
if tool_count > 0:
|
|
158
166
|
tool_word = "tool" if tool_count == 1 else "tools"
|
|
159
|
-
|
|
160
|
-
|
|
167
|
+
sub_parts.append(f"{tool_count:,}[dim] {tool_word}[/dim]")
|
|
161
168
|
if prompt_count > 0:
|
|
162
169
|
prompt_word = "prompt" if prompt_count == 1 else "prompts"
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
sub_parts.append(f"{prompt_count:,}[dim] {prompt_word}[/dim]")
|
|
165
171
|
if resource_count > 0:
|
|
166
172
|
resource_word = "resource" if resource_count == 1 else "resources"
|
|
167
|
-
|
|
173
|
+
sub_parts.append(f"{resource_count:,}[dim] {resource_word}[/dim]")
|
|
168
174
|
|
|
169
|
-
# Always show server count
|
|
170
175
|
server_word = "Server" if server_count == 1 else "Servers"
|
|
171
176
|
server_text = f"{server_count:,}[dim] MCP {server_word}[/dim]"
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
+ "[dim]
|
|
177
|
-
+ "[dim] available[/dim]"
|
|
177
|
+
if sub_parts:
|
|
178
|
+
server_text = (
|
|
179
|
+
f"{server_text}[dim] ([/dim]"
|
|
180
|
+
+ "[dim], [/dim]".join(sub_parts)
|
|
181
|
+
+ "[dim])[/dim]"
|
|
178
182
|
)
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
content_parts.append(server_text)
|
|
184
|
+
|
|
185
|
+
if skill_count > 0:
|
|
186
|
+
skill_word = "skill" if skill_count == 1 else "skills"
|
|
187
|
+
content_parts.append(
|
|
188
|
+
f"{skill_count:,}[dim] {skill_word}[/dim][dim] available[/dim]"
|
|
189
|
+
)
|
|
181
190
|
|
|
191
|
+
if content_parts:
|
|
192
|
+
content = "[dim]. [/dim]".join(content_parts)
|
|
182
193
|
rich_print(f"[dim]Agent [/dim][blue]{agent_name}[/blue][dim]:[/dim] {content}")
|
|
183
194
|
# await _render_mcp_status(agent)
|
|
184
195
|
|
|
@@ -351,9 +362,11 @@ class AgentCompleter(Completer):
|
|
|
351
362
|
self.commands = {
|
|
352
363
|
"mcp": "Show MCP server status",
|
|
353
364
|
"history": "Show conversation history overview (optionally another agent)",
|
|
354
|
-
"tools": "List available MCP
|
|
365
|
+
"tools": "List available MCP Tools",
|
|
366
|
+
"skills": "List available Agent Skills",
|
|
355
367
|
"prompt": "List and choose MCP prompts, or apply specific prompt (/prompt <name>)",
|
|
356
368
|
"clear": "Clear history",
|
|
369
|
+
"clear last": "Remove the most recent message from history",
|
|
357
370
|
"agents": "List available agents",
|
|
358
371
|
"system": "Show the current system prompt",
|
|
359
372
|
"usage": "Show current usage statistics",
|
|
@@ -711,18 +724,9 @@ async def get_enhanced_input(
|
|
|
711
724
|
# Build TDV capability segment based on model database
|
|
712
725
|
info = None
|
|
713
726
|
if llm:
|
|
714
|
-
|
|
715
|
-
info = get_model_info(llm)
|
|
716
|
-
except TypeError:
|
|
717
|
-
info = None
|
|
727
|
+
info = ModelInfo.from_llm(llm)
|
|
718
728
|
if not info and model_name:
|
|
719
|
-
|
|
720
|
-
info = get_model_info(model_name)
|
|
721
|
-
except TypeError:
|
|
722
|
-
info = None
|
|
723
|
-
except Exception as exc:
|
|
724
|
-
print(f"[toolbar debug] get_model_info failed for '{agent_name}': {exc}")
|
|
725
|
-
info = None
|
|
729
|
+
info = ModelInfo.from_name(model_name)
|
|
726
730
|
|
|
727
731
|
# Default to text-only if info resolution fails for any reason
|
|
728
732
|
t, d, v = (True, False, False)
|
|
@@ -854,8 +858,34 @@ async def get_enhanced_input(
|
|
|
854
858
|
)
|
|
855
859
|
session.app.key_bindings = bindings
|
|
856
860
|
|
|
861
|
+
shell_agent = None
|
|
862
|
+
shell_enabled = False
|
|
863
|
+
shell_access_modes: tuple[str, ...] = ()
|
|
864
|
+
shell_name: str | None = None
|
|
865
|
+
if agent_provider:
|
|
866
|
+
try:
|
|
867
|
+
shell_agent = agent_provider._agent(agent_name)
|
|
868
|
+
except Exception:
|
|
869
|
+
shell_agent = None
|
|
870
|
+
|
|
871
|
+
if shell_agent:
|
|
872
|
+
shell_enabled = bool(getattr(shell_agent, "_shell_runtime_enabled", False))
|
|
873
|
+
modes_attr = getattr(shell_agent, "_shell_access_modes", ())
|
|
874
|
+
if isinstance(modes_attr, (list, tuple)):
|
|
875
|
+
shell_access_modes = tuple(str(mode) for mode in modes_attr)
|
|
876
|
+
elif modes_attr:
|
|
877
|
+
shell_access_modes = (str(modes_attr),)
|
|
878
|
+
|
|
879
|
+
# Get the detected shell name from the runtime
|
|
880
|
+
if shell_enabled:
|
|
881
|
+
shell_runtime = getattr(shell_agent, "_shell_runtime", None)
|
|
882
|
+
if shell_runtime:
|
|
883
|
+
runtime_info = shell_runtime.runtime_info()
|
|
884
|
+
shell_name = runtime_info.get("name")
|
|
885
|
+
|
|
857
886
|
# Create formatted prompt text
|
|
858
|
-
|
|
887
|
+
arrow_segment = "<ansibrightyellow>❯</ansibrightyellow>" if shell_enabled else "❯"
|
|
888
|
+
prompt_text = f"<ansibrightblue>{agent_name}</ansibrightblue> {arrow_segment} "
|
|
859
889
|
|
|
860
890
|
# Add default value display if requested
|
|
861
891
|
if show_default and default and default != "STOP":
|
|
@@ -887,8 +917,10 @@ async def get_enhanced_input(
|
|
|
887
917
|
# Get logger settings from the agent's context (not agent_provider)
|
|
888
918
|
logger_settings = None
|
|
889
919
|
try:
|
|
890
|
-
|
|
891
|
-
|
|
920
|
+
active_agent = shell_agent
|
|
921
|
+
if active_agent is None:
|
|
922
|
+
active_agent = agent_provider._agent(agent_name)
|
|
923
|
+
agent_context = active_agent._context or active_agent.context
|
|
892
924
|
logger_settings = agent_context.config.logger
|
|
893
925
|
except Exception:
|
|
894
926
|
# If we can't get the agent or its context, logger_settings stays None
|
|
@@ -922,6 +954,33 @@ async def get_enhanced_input(
|
|
|
922
954
|
f"[dim]Experimental: Streaming Enabled - {streaming_mode} mode[/dim]"
|
|
923
955
|
)
|
|
924
956
|
|
|
957
|
+
if shell_enabled:
|
|
958
|
+
modes_display = ", ".join(shell_access_modes or ("direct",))
|
|
959
|
+
shell_display = f"{modes_display}, {shell_name}" if shell_name else modes_display
|
|
960
|
+
|
|
961
|
+
# Add working directory info
|
|
962
|
+
shell_runtime = getattr(shell_agent, "_shell_runtime", None)
|
|
963
|
+
if shell_runtime:
|
|
964
|
+
working_dir = shell_runtime.working_directory()
|
|
965
|
+
try:
|
|
966
|
+
# Try to show relative to cwd for cleaner display
|
|
967
|
+
working_dir_display = str(working_dir.relative_to(Path.cwd()))
|
|
968
|
+
if working_dir_display == ".":
|
|
969
|
+
# Show last 2 parts of the path (e.g., "source/fast-agent")
|
|
970
|
+
parts = Path.cwd().parts
|
|
971
|
+
if len(parts) >= 2:
|
|
972
|
+
working_dir_display = "/".join(parts[-2:])
|
|
973
|
+
elif len(parts) == 1:
|
|
974
|
+
working_dir_display = parts[0]
|
|
975
|
+
else:
|
|
976
|
+
working_dir_display = str(Path.cwd())
|
|
977
|
+
except ValueError:
|
|
978
|
+
# If not relative to cwd, show absolute path
|
|
979
|
+
working_dir_display = str(working_dir)
|
|
980
|
+
shell_display = f"{shell_display} | cwd: {working_dir_display}"
|
|
981
|
+
|
|
982
|
+
rich_print(f"[yellow]Shell Access ({shell_display})[/yellow]")
|
|
983
|
+
|
|
925
984
|
rich_print()
|
|
926
985
|
help_message_shown = True
|
|
927
986
|
|
|
@@ -953,9 +1012,16 @@ async def get_enhanced_input(
|
|
|
953
1012
|
elif cmd == "clear":
|
|
954
1013
|
target_agent = None
|
|
955
1014
|
if len(cmd_parts) > 1:
|
|
956
|
-
|
|
957
|
-
if
|
|
958
|
-
|
|
1015
|
+
remainder = cmd_parts[1].strip()
|
|
1016
|
+
if remainder:
|
|
1017
|
+
tokens = remainder.split(maxsplit=1)
|
|
1018
|
+
if tokens and tokens[0].lower() == "last":
|
|
1019
|
+
if len(tokens) > 1:
|
|
1020
|
+
candidate = tokens[1].strip()
|
|
1021
|
+
if candidate:
|
|
1022
|
+
target_agent = candidate
|
|
1023
|
+
return {"clear_last": {"agent": target_agent}}
|
|
1024
|
+
target_agent = remainder
|
|
959
1025
|
return {"clear_history": {"agent": target_agent}}
|
|
960
1026
|
elif cmd == "markdown":
|
|
961
1027
|
return "MARKDOWN"
|
|
@@ -984,6 +1050,8 @@ async def get_enhanced_input(
|
|
|
984
1050
|
elif cmd == "tools":
|
|
985
1051
|
# Return a dictionary with list_tools action
|
|
986
1052
|
return {"list_tools": True}
|
|
1053
|
+
elif cmd == "skills":
|
|
1054
|
+
return {"list_skills": True}
|
|
987
1055
|
elif cmd == "exit":
|
|
988
1056
|
return "EXIT"
|
|
989
1057
|
elif cmd.lower() == "stop":
|
|
@@ -1147,8 +1215,10 @@ async def handle_special_commands(
|
|
|
1147
1215
|
rich_print(" /system - Show the current system prompt")
|
|
1148
1216
|
rich_print(" /prompt <name> - Apply a specific prompt by name")
|
|
1149
1217
|
rich_print(" /usage - Show current usage statistics")
|
|
1218
|
+
rich_print(" /skills - List local skills for the active agent")
|
|
1150
1219
|
rich_print(" /history [agent_name] - Show chat history overview")
|
|
1151
1220
|
rich_print(" /clear [agent_name] - Clear conversation history (keeps templates)")
|
|
1221
|
+
rich_print(" /clear last [agent_name] - Remove the most recent message from history")
|
|
1152
1222
|
rich_print(" /markdown - Show last assistant message without markdown formatting")
|
|
1153
1223
|
rich_print(" /mcpstatus - Show MCP server status summary for the active agent")
|
|
1154
1224
|
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"):
|