fast-agent-mcp 0.3.15__py3-none-any.whl → 0.3.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fast-agent-mcp might be problematic. Click here for more details.
- 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 +13 -1
- fast_agent/cli/main.py +1 -0
- 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 +8 -0
- fast_agent/llm/fastagent_llm.py +45 -0
- fast_agent/llm/memory.py +26 -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/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 +200 -0
- fast_agent/tools/shell_runtime.py +404 -0
- fast_agent/ui/console_display.py +396 -129
- fast_agent/ui/elicitation_form.py +76 -24
- fast_agent/ui/elicitation_style.py +2 -2
- fast_agent/ui/enhanced_prompt.py +81 -25
- fast_agent/ui/history_display.py +20 -5
- fast_agent/ui/interactive_prompt.py +108 -3
- fast_agent/ui/markdown_truncator.py +1 -1
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.dist-info}/METADATA +8 -7
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.dist-info}/RECORD +39 -35
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.15.dist-info → fast_agent_mcp-0.3.16.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
|
@@ -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",
|
|
@@ -854,8 +866,34 @@ async def get_enhanced_input(
|
|
|
854
866
|
)
|
|
855
867
|
session.app.key_bindings = bindings
|
|
856
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
|
+
|
|
857
894
|
# Create formatted prompt text
|
|
858
|
-
|
|
895
|
+
arrow_segment = "<ansibrightyellow>❯</ansibrightyellow>" if shell_enabled else "❯"
|
|
896
|
+
prompt_text = f"<ansibrightblue>{agent_name}</ansibrightblue> {arrow_segment} "
|
|
859
897
|
|
|
860
898
|
# Add default value display if requested
|
|
861
899
|
if show_default and default and default != "STOP":
|
|
@@ -887,8 +925,10 @@ async def get_enhanced_input(
|
|
|
887
925
|
# Get logger settings from the agent's context (not agent_provider)
|
|
888
926
|
logger_settings = None
|
|
889
927
|
try:
|
|
890
|
-
|
|
891
|
-
|
|
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
|
|
892
932
|
logger_settings = agent_context.config.logger
|
|
893
933
|
except Exception:
|
|
894
934
|
# If we can't get the agent or its context, logger_settings stays None
|
|
@@ -922,6 +962,11 @@ async def get_enhanced_input(
|
|
|
922
962
|
f"[dim]Experimental: Streaming Enabled - {streaming_mode} mode[/dim]"
|
|
923
963
|
)
|
|
924
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
|
+
|
|
925
970
|
rich_print()
|
|
926
971
|
help_message_shown = True
|
|
927
972
|
|
|
@@ -953,9 +998,16 @@ async def get_enhanced_input(
|
|
|
953
998
|
elif cmd == "clear":
|
|
954
999
|
target_agent = None
|
|
955
1000
|
if len(cmd_parts) > 1:
|
|
956
|
-
|
|
957
|
-
if
|
|
958
|
-
|
|
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
|
|
959
1011
|
return {"clear_history": {"agent": target_agent}}
|
|
960
1012
|
elif cmd == "markdown":
|
|
961
1013
|
return "MARKDOWN"
|
|
@@ -984,6 +1036,8 @@ async def get_enhanced_input(
|
|
|
984
1036
|
elif cmd == "tools":
|
|
985
1037
|
# Return a dictionary with list_tools action
|
|
986
1038
|
return {"list_tools": True}
|
|
1039
|
+
elif cmd == "skills":
|
|
1040
|
+
return {"list_skills": True}
|
|
987
1041
|
elif cmd == "exit":
|
|
988
1042
|
return "EXIT"
|
|
989
1043
|
elif cmd.lower() == "stop":
|
|
@@ -1147,8 +1201,10 @@ async def handle_special_commands(
|
|
|
1147
1201
|
rich_print(" /system - Show the current system prompt")
|
|
1148
1202
|
rich_print(" /prompt <name> - Apply a specific prompt by name")
|
|
1149
1203
|
rich_print(" /usage - Show current usage statistics")
|
|
1204
|
+
rich_print(" /skills - List local skills for the active agent")
|
|
1150
1205
|
rich_print(" /history [agent_name] - Show chat history overview")
|
|
1151
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")
|
|
1152
1208
|
rich_print(" /markdown - Show last assistant message without markdown formatting")
|
|
1153
1209
|
rich_print(" /mcpstatus - Show MCP server status summary for the active agent")
|
|
1154
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"):
|
|
@@ -14,6 +14,7 @@ Usage:
|
|
|
14
14
|
)
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
+
from pathlib import Path
|
|
17
18
|
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union, cast
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
@@ -169,6 +170,9 @@ class InteractivePrompt:
|
|
|
169
170
|
# Handle tools list display
|
|
170
171
|
await self._list_tools(prompt_provider, agent)
|
|
171
172
|
continue
|
|
173
|
+
elif "list_skills" in command_dict:
|
|
174
|
+
await self._list_skills(prompt_provider, agent)
|
|
175
|
+
continue
|
|
172
176
|
elif "show_usage" in command_dict:
|
|
173
177
|
# Handle usage display
|
|
174
178
|
await self._show_usage(prompt_provider, agent)
|
|
@@ -189,6 +193,41 @@ class InteractivePrompt:
|
|
|
189
193
|
usage = getattr(agent_obj, "usage_accumulator", None)
|
|
190
194
|
display_history_overview(target_agent, history, usage)
|
|
191
195
|
continue
|
|
196
|
+
elif "clear_last" in command_dict:
|
|
197
|
+
clear_info = command_dict.get("clear_last")
|
|
198
|
+
clear_agent = (
|
|
199
|
+
clear_info.get("agent") if isinstance(clear_info, dict) else None
|
|
200
|
+
)
|
|
201
|
+
target_agent = clear_agent or agent
|
|
202
|
+
try:
|
|
203
|
+
agent_obj = prompt_provider._agent(target_agent)
|
|
204
|
+
except Exception:
|
|
205
|
+
rich_print(f"[red]Unable to load agent '{target_agent}'[/red]")
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
removed_message = None
|
|
209
|
+
pop_callable = getattr(agent_obj, "pop_last_message", None)
|
|
210
|
+
if callable(pop_callable):
|
|
211
|
+
removed_message = pop_callable()
|
|
212
|
+
else:
|
|
213
|
+
history = getattr(agent_obj, "message_history", [])
|
|
214
|
+
if history:
|
|
215
|
+
try:
|
|
216
|
+
removed_message = history.pop()
|
|
217
|
+
except Exception:
|
|
218
|
+
removed_message = None
|
|
219
|
+
|
|
220
|
+
if removed_message:
|
|
221
|
+
role = getattr(removed_message, "role", "message")
|
|
222
|
+
role_display = role.capitalize() if isinstance(role, str) else "Message"
|
|
223
|
+
rich_print(
|
|
224
|
+
f"[green]Removed last {role_display} for agent '{target_agent}'.[/green]"
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
rich_print(
|
|
228
|
+
f"[yellow]No messages to remove for agent '{target_agent}'.[/yellow]"
|
|
229
|
+
)
|
|
230
|
+
continue
|
|
192
231
|
elif "clear_history" in command_dict:
|
|
193
232
|
clear_info = command_dict.get("clear_history")
|
|
194
233
|
clear_agent = (
|
|
@@ -857,19 +896,21 @@ class InteractivePrompt:
|
|
|
857
896
|
rich_print()
|
|
858
897
|
|
|
859
898
|
# Display tools using clean compact format
|
|
860
|
-
|
|
899
|
+
index = 1
|
|
900
|
+
for tool in tools_result.tools:
|
|
861
901
|
# Main line: [ 1] tool_name Title
|
|
862
902
|
from rich.text import Text
|
|
863
903
|
|
|
904
|
+
meta = getattr(tool, "meta", {}) or {}
|
|
905
|
+
|
|
864
906
|
tool_line = Text()
|
|
865
|
-
tool_line.append(f"[{
|
|
907
|
+
tool_line.append(f"[{index:2}] ", style="dim cyan")
|
|
866
908
|
tool_line.append(tool.name, style="bright_blue bold")
|
|
867
909
|
|
|
868
910
|
# Add title if available
|
|
869
911
|
if tool.title and tool.title.strip():
|
|
870
912
|
tool_line.append(f" {tool.title}", style="default")
|
|
871
913
|
|
|
872
|
-
meta = getattr(tool, "meta", {}) or {}
|
|
873
914
|
if meta.get("openai/skybridgeEnabled"):
|
|
874
915
|
tool_line.append(" (skybridge)", style="cyan")
|
|
875
916
|
|
|
@@ -932,13 +973,77 @@ class InteractivePrompt:
|
|
|
932
973
|
rich_print(f" [dim magenta]template:[/dim magenta] {template}")
|
|
933
974
|
|
|
934
975
|
rich_print() # Space between tools
|
|
976
|
+
index += 1
|
|
935
977
|
|
|
978
|
+
if index == 1:
|
|
979
|
+
rich_print("[yellow]No MCP tools available for this agent[/yellow]")
|
|
936
980
|
except Exception as e:
|
|
937
981
|
import traceback
|
|
938
982
|
|
|
939
983
|
rich_print(f"[red]Error listing tools: {e}[/red]")
|
|
940
984
|
rich_print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
941
985
|
|
|
986
|
+
async def _list_skills(self, prompt_provider: "AgentApp", agent_name: str) -> None:
|
|
987
|
+
"""List available local skills for an agent."""
|
|
988
|
+
|
|
989
|
+
try:
|
|
990
|
+
assert hasattr(prompt_provider, "_agent"), (
|
|
991
|
+
"Interactive prompt expects an AgentApp with _agent()"
|
|
992
|
+
)
|
|
993
|
+
agent = prompt_provider._agent(agent_name)
|
|
994
|
+
|
|
995
|
+
rich_print(f"\n[bold]Skills for agent [cyan]{agent_name}[/cyan]:[/bold]")
|
|
996
|
+
|
|
997
|
+
skill_manifests = getattr(agent, "_skill_manifests", None)
|
|
998
|
+
manifests = list(skill_manifests) if skill_manifests else []
|
|
999
|
+
|
|
1000
|
+
if not manifests:
|
|
1001
|
+
rich_print("[yellow]No skills available for this agent[/yellow]")
|
|
1002
|
+
return
|
|
1003
|
+
|
|
1004
|
+
rich_print()
|
|
1005
|
+
|
|
1006
|
+
for index, manifest in enumerate(manifests, 1):
|
|
1007
|
+
from rich.text import Text
|
|
1008
|
+
|
|
1009
|
+
name = getattr(manifest, "name", "")
|
|
1010
|
+
description = getattr(manifest, "description", "")
|
|
1011
|
+
path = Path(getattr(manifest, "path", Path()))
|
|
1012
|
+
|
|
1013
|
+
tool_line = Text()
|
|
1014
|
+
tool_line.append(f"[{index:2}] ", style="dim cyan")
|
|
1015
|
+
tool_line.append(name, style="bright_blue bold")
|
|
1016
|
+
rich_print(tool_line)
|
|
1017
|
+
|
|
1018
|
+
if description:
|
|
1019
|
+
import textwrap
|
|
1020
|
+
|
|
1021
|
+
wrapped_lines = textwrap.wrap(
|
|
1022
|
+
description.strip(), width=72, subsequent_indent=" "
|
|
1023
|
+
)
|
|
1024
|
+
for line in wrapped_lines:
|
|
1025
|
+
if line.startswith(" "):
|
|
1026
|
+
rich_print(f" [white]{line[5:]}[/white]")
|
|
1027
|
+
else:
|
|
1028
|
+
rich_print(f" [white]{line}[/white]")
|
|
1029
|
+
|
|
1030
|
+
source_path = path if path else Path(".")
|
|
1031
|
+
if source_path.is_file():
|
|
1032
|
+
source_path = source_path.parent
|
|
1033
|
+
try:
|
|
1034
|
+
display_path = source_path.relative_to(Path.cwd())
|
|
1035
|
+
except ValueError:
|
|
1036
|
+
display_path = source_path
|
|
1037
|
+
|
|
1038
|
+
rich_print(f" [dim green]source:[/dim green] {display_path}")
|
|
1039
|
+
rich_print()
|
|
1040
|
+
|
|
1041
|
+
except Exception as exc: # noqa: BLE001
|
|
1042
|
+
import traceback
|
|
1043
|
+
|
|
1044
|
+
rich_print(f"[red]Error listing skills: {exc}[/red]")
|
|
1045
|
+
rich_print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
1046
|
+
|
|
942
1047
|
async def _show_usage(self, prompt_provider: "AgentApp", agent_name: str) -> None:
|
|
943
1048
|
"""
|
|
944
1049
|
Show usage statistics for the current agent(s) in a colorful table format.
|
|
@@ -876,7 +876,7 @@ class MarkdownTruncator:
|
|
|
876
876
|
return truncated_text
|
|
877
877
|
|
|
878
878
|
# Find where the truncated text starts in the original
|
|
879
|
-
truncation_pos = original_text.
|
|
879
|
+
truncation_pos = original_text.rfind(truncated_text)
|
|
880
880
|
if truncation_pos == -1:
|
|
881
881
|
# Can't find it, return as-is
|
|
882
882
|
return truncated_text
|