fast-agent-mcp 0.3.11__py3-none-any.whl → 0.3.13__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/agents/llm_agent.py +1 -1
- fast_agent/agents/llm_decorator.py +7 -0
- fast_agent/agents/tool_agent.py +2 -2
- fast_agent/cli/commands/quickstart.py +142 -129
- fast_agent/core/direct_decorators.py +18 -26
- fast_agent/core/fastagent.py +2 -1
- fast_agent/interfaces.py +4 -0
- fast_agent/llm/fastagent_llm.py +17 -0
- fast_agent/llm/model_database.py +2 -0
- fast_agent/llm/model_factory.py +4 -1
- fast_agent/llm/provider/google/google_converter.py +10 -3
- fast_agent/mcp/mcp_agent_client_session.py +13 -0
- fast_agent/mcp/mcp_connection_manager.py +58 -15
- fast_agent/mcp/prompts/prompt_load.py +2 -3
- fast_agent/ui/console_display.py +52 -2
- fast_agent/ui/enhanced_prompt.py +47 -16
- fast_agent/ui/history_display.py +555 -0
- fast_agent/ui/interactive_prompt.py +44 -12
- fast_agent/ui/mcp_display.py +54 -14
- fast_agent/ui/notification_tracker.py +62 -23
- fast_agent/ui/rich_progress.py +1 -1
- {fast_agent_mcp-0.3.11.dist-info → fast_agent_mcp-0.3.13.dist-info}/METADATA +18 -9
- {fast_agent_mcp-0.3.11.dist-info → fast_agent_mcp-0.3.13.dist-info}/RECORD +26 -25
- {fast_agent_mcp-0.3.11.dist-info → fast_agent_mcp-0.3.13.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.11.dist-info → fast_agent_mcp-0.3.13.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.11.dist-info → fast_agent_mcp-0.3.13.dist-info}/licenses/LICENSE +0 -0
fast_agent/llm/fastagent_llm.py
CHANGED
|
@@ -129,6 +129,7 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
|
|
|
129
129
|
self.history: Memory[MessageParamT] = SimpleMemory[MessageParamT]()
|
|
130
130
|
|
|
131
131
|
self._message_history: List[PromptMessageExtended] = []
|
|
132
|
+
self._template_messages: List[PromptMessageExtended] = []
|
|
132
133
|
|
|
133
134
|
# Initialize the display component
|
|
134
135
|
from fast_agent.ui.console_display import ConsoleDisplay
|
|
@@ -575,11 +576,15 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
|
|
|
575
576
|
|
|
576
577
|
# Convert to PromptMessageExtended objects
|
|
577
578
|
multipart_messages = PromptMessageExtended.parse_get_prompt_result(prompt_result)
|
|
579
|
+
# Store a local copy of template messages so we can retain them across clears
|
|
580
|
+
self._template_messages = [msg.model_copy(deep=True) for msg in multipart_messages]
|
|
578
581
|
|
|
579
582
|
# Delegate to the provider-specific implementation
|
|
580
583
|
result = await self._apply_prompt_provider_specific(
|
|
581
584
|
multipart_messages, None, is_template=True
|
|
582
585
|
)
|
|
586
|
+
# Ensure message history always includes the stored template when applied
|
|
587
|
+
self._message_history = [msg.model_copy(deep=True) for msg in self._template_messages]
|
|
583
588
|
return result.first_text()
|
|
584
589
|
|
|
585
590
|
async def _save_history(self, filename: str) -> None:
|
|
@@ -607,6 +612,18 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
|
|
|
607
612
|
"""
|
|
608
613
|
return self._message_history
|
|
609
614
|
|
|
615
|
+
def clear(self, *, clear_prompts: bool = False) -> None:
|
|
616
|
+
"""Reset stored message history while optionally retaining prompt templates."""
|
|
617
|
+
|
|
618
|
+
self.history.clear(clear_prompts=clear_prompts)
|
|
619
|
+
if clear_prompts:
|
|
620
|
+
self._template_messages = []
|
|
621
|
+
self._message_history = []
|
|
622
|
+
return
|
|
623
|
+
|
|
624
|
+
# Restore message history to template messages only; new turns will append as normal
|
|
625
|
+
self._message_history = [msg.model_copy(deep=True) for msg in self._template_messages]
|
|
626
|
+
|
|
610
627
|
def _api_key(self):
|
|
611
628
|
if self._init_api_key:
|
|
612
629
|
return self._init_api_key
|
fast_agent/llm/model_database.py
CHANGED
|
@@ -232,6 +232,8 @@ class ModelDatabase:
|
|
|
232
232
|
"claude-3-7-sonnet-latest": ANTHROPIC_37_SERIES,
|
|
233
233
|
"claude-sonnet-4-0": ANTHROPIC_SONNET_4_VERSIONED,
|
|
234
234
|
"claude-sonnet-4-20250514": ANTHROPIC_SONNET_4_VERSIONED,
|
|
235
|
+
"claude-sonnet-4-5": ANTHROPIC_SONNET_4_VERSIONED,
|
|
236
|
+
"claude-sonnet-4-5-20250929": ANTHROPIC_SONNET_4_VERSIONED,
|
|
235
237
|
"claude-opus-4-0": ANTHROPIC_OPUS_4_VERSIONED,
|
|
236
238
|
"claude-opus-4-1": ANTHROPIC_OPUS_4_VERSIONED,
|
|
237
239
|
"claude-opus-4-20250514": ANTHROPIC_OPUS_4_VERSIONED,
|
fast_agent/llm/model_factory.py
CHANGED
|
@@ -84,6 +84,8 @@ class ModelFactory:
|
|
|
84
84
|
"claude-opus-4-20250514": Provider.ANTHROPIC,
|
|
85
85
|
"claude-sonnet-4-20250514": Provider.ANTHROPIC,
|
|
86
86
|
"claude-sonnet-4-0": Provider.ANTHROPIC,
|
|
87
|
+
"claude-sonnet-4-5-20250929": Provider.ANTHROPIC,
|
|
88
|
+
"claude-sonnet-4-5": Provider.ANTHROPIC,
|
|
87
89
|
"deepseek-chat": Provider.DEEPSEEK,
|
|
88
90
|
"gemini-2.0-flash": Provider.GOOGLE,
|
|
89
91
|
"gemini-2.5-flash-preview-05-20": Provider.GOOGLE,
|
|
@@ -101,8 +103,9 @@ class ModelFactory:
|
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
MODEL_ALIASES = {
|
|
104
|
-
"sonnet": "claude-sonnet-4-
|
|
106
|
+
"sonnet": "claude-sonnet-4-5",
|
|
105
107
|
"sonnet4": "claude-sonnet-4-0",
|
|
108
|
+
"sonnet45": "claude-sonnet-4-5",
|
|
106
109
|
"sonnet35": "claude-3-5-sonnet-latest",
|
|
107
110
|
"sonnet37": "claude-3-7-sonnet-latest",
|
|
108
111
|
"claude": "claude-sonnet-4-0",
|
|
@@ -53,6 +53,14 @@ class GoogleConverter:
|
|
|
53
53
|
if key in unsupported_keys:
|
|
54
54
|
continue # Skip this key
|
|
55
55
|
|
|
56
|
+
# Rewrite unsupported 'const' to a safe form for Gemini tools
|
|
57
|
+
# - For string const, convert to enum [value]
|
|
58
|
+
# - For non-string const (booleans/numbers), drop the constraint
|
|
59
|
+
if key == "const":
|
|
60
|
+
if isinstance(value, str):
|
|
61
|
+
cleaned_schema["enum"] = [value]
|
|
62
|
+
continue
|
|
63
|
+
|
|
56
64
|
if (
|
|
57
65
|
key == "format"
|
|
58
66
|
and schema.get("type") == "string"
|
|
@@ -140,9 +148,8 @@ class GoogleConverter:
|
|
|
140
148
|
)
|
|
141
149
|
elif is_resource_content(part_content):
|
|
142
150
|
assert isinstance(part_content, EmbeddedResource)
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
and isinstance(part_content.resource, BlobResourceContents)
|
|
151
|
+
if "application/pdf" == part_content.resource.mimeType and isinstance(
|
|
152
|
+
part_content.resource, BlobResourceContents
|
|
146
153
|
):
|
|
147
154
|
pdf_bytes = base64.b64decode(part_content.resource.blob)
|
|
148
155
|
parts.append(
|
|
@@ -4,6 +4,7 @@ It adds logging and supports sampling requests.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from datetime import timedelta
|
|
7
|
+
from time import perf_counter
|
|
7
8
|
from typing import TYPE_CHECKING
|
|
8
9
|
|
|
9
10
|
from mcp import ClientSession, ServerNotification
|
|
@@ -207,6 +208,7 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
|
|
|
207
208
|
) -> ReceiveResultT:
|
|
208
209
|
logger.debug("send_request: request=", data=request.model_dump())
|
|
209
210
|
request_id = getattr(self, "_request_id", None)
|
|
211
|
+
start_time = perf_counter()
|
|
210
212
|
try:
|
|
211
213
|
result = await super().send_request(
|
|
212
214
|
request=request,
|
|
@@ -220,6 +222,7 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
|
|
|
220
222
|
data=result.model_dump() if result is not None else "no response returned",
|
|
221
223
|
)
|
|
222
224
|
self._attach_transport_channel(request_id, result)
|
|
225
|
+
self._attach_transport_elapsed(result, perf_counter() - start_time)
|
|
223
226
|
return result
|
|
224
227
|
except Exception as e:
|
|
225
228
|
# Handle connection errors cleanly
|
|
@@ -250,6 +253,16 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
|
|
|
250
253
|
# If result cannot be mutated, ignore silently
|
|
251
254
|
pass
|
|
252
255
|
|
|
256
|
+
@staticmethod
|
|
257
|
+
def _attach_transport_elapsed(result, elapsed: float | None) -> None:
|
|
258
|
+
if result is None or elapsed is None:
|
|
259
|
+
return
|
|
260
|
+
try:
|
|
261
|
+
setattr(result, "transport_elapsed", max(elapsed, 0.0))
|
|
262
|
+
except Exception:
|
|
263
|
+
# Ignore if result is immutable
|
|
264
|
+
pass
|
|
265
|
+
|
|
253
266
|
async def _received_notification(self, notification: ServerNotification) -> None:
|
|
254
267
|
"""
|
|
255
268
|
Can be overridden by subclasses to handle a notification without needing
|
|
@@ -38,6 +38,8 @@ from fast_agent.mcp.streamable_http_tracking import tracking_streamablehttp_clie
|
|
|
38
38
|
from fast_agent.mcp.transport_tracking import TransportChannelMetrics
|
|
39
39
|
|
|
40
40
|
if TYPE_CHECKING:
|
|
41
|
+
from mcp.client.auth import OAuthClientProvider
|
|
42
|
+
|
|
41
43
|
from fast_agent.context import Context
|
|
42
44
|
from fast_agent.mcp_server_registry import ServerRegistry
|
|
43
45
|
|
|
@@ -65,6 +67,38 @@ def _add_none_to_context(context_manager):
|
|
|
65
67
|
return StreamingContextAdapter(context_manager)
|
|
66
68
|
|
|
67
69
|
|
|
70
|
+
def _prepare_headers_and_auth(
|
|
71
|
+
server_config: MCPServerSettings,
|
|
72
|
+
) -> tuple[dict[str, str], Optional["OAuthClientProvider"], set[str]]:
|
|
73
|
+
"""
|
|
74
|
+
Prepare request headers and determine if OAuth authentication should be used.
|
|
75
|
+
|
|
76
|
+
Returns a copy of the headers, an OAuth auth provider when applicable, and the set
|
|
77
|
+
of user-supplied authorization header keys.
|
|
78
|
+
"""
|
|
79
|
+
headers: dict[str, str] = dict(server_config.headers or {})
|
|
80
|
+
auth_header_keys = {"authorization", "x-hf-authorization"}
|
|
81
|
+
user_provided_auth_keys = {key for key in headers if key.lower() in auth_header_keys}
|
|
82
|
+
|
|
83
|
+
# OAuth is only relevant for SSE/HTTP transports and should be skipped when the
|
|
84
|
+
# user has already supplied explicit Authorization headers.
|
|
85
|
+
if server_config.transport not in ("sse", "http") or user_provided_auth_keys:
|
|
86
|
+
return headers, None, user_provided_auth_keys
|
|
87
|
+
|
|
88
|
+
oauth_auth = build_oauth_provider(server_config)
|
|
89
|
+
if oauth_auth is not None:
|
|
90
|
+
# Scrub Authorization headers so OAuth-managed credentials are the only ones sent.
|
|
91
|
+
for header_name in (
|
|
92
|
+
"Authorization",
|
|
93
|
+
"authorization",
|
|
94
|
+
"X-HF-Authorization",
|
|
95
|
+
"x-hf-authorization",
|
|
96
|
+
):
|
|
97
|
+
headers.pop(header_name, None)
|
|
98
|
+
|
|
99
|
+
return headers, oauth_auth, user_provided_auth_keys
|
|
100
|
+
|
|
101
|
+
|
|
68
102
|
class ServerConnection:
|
|
69
103
|
"""
|
|
70
104
|
Represents a long-lived MCP server connection, including:
|
|
@@ -113,7 +147,9 @@ class ServerConnection:
|
|
|
113
147
|
self.server_implementation: Implementation | None = None
|
|
114
148
|
self.client_capabilities: dict | None = None
|
|
115
149
|
self.server_instructions_available: bool = False
|
|
116
|
-
self.server_instructions_enabled: bool =
|
|
150
|
+
self.server_instructions_enabled: bool = (
|
|
151
|
+
server_config.include_instructions if server_config else True
|
|
152
|
+
)
|
|
117
153
|
self.session_id: str | None = None
|
|
118
154
|
self._get_session_id_cb: GetSessionIdCallback | None = None
|
|
119
155
|
self.transport_metrics: TransportChannelMetrics | None = None
|
|
@@ -404,7 +440,9 @@ class MCPConnectionManager(ContextDependent):
|
|
|
404
440
|
|
|
405
441
|
logger.debug(f"{server_name}: Found server configuration=", data=config.model_dump())
|
|
406
442
|
|
|
407
|
-
transport_metrics =
|
|
443
|
+
transport_metrics = (
|
|
444
|
+
TransportChannelMetrics() if config.transport in ("http", "stdio") else None
|
|
445
|
+
)
|
|
408
446
|
|
|
409
447
|
def transport_context_factory():
|
|
410
448
|
if config.transport == "stdio":
|
|
@@ -425,7 +463,9 @@ class MCPConnectionManager(ContextDependent):
|
|
|
425
463
|
|
|
426
464
|
channel_hook = transport_metrics.record_event if transport_metrics else None
|
|
427
465
|
return _add_none_to_context(
|
|
428
|
-
tracking_stdio_client(
|
|
466
|
+
tracking_stdio_client(
|
|
467
|
+
server_params, channel_hook=channel_hook, errlog=error_handler
|
|
468
|
+
)
|
|
429
469
|
)
|
|
430
470
|
elif config.transport == "sse":
|
|
431
471
|
if not config.url:
|
|
@@ -434,12 +474,12 @@ class MCPConnectionManager(ContextDependent):
|
|
|
434
474
|
)
|
|
435
475
|
# Suppress MCP library error spam
|
|
436
476
|
self._suppress_mcp_sse_errors()
|
|
437
|
-
oauth_auth =
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
477
|
+
headers, oauth_auth, user_auth_keys = _prepare_headers_and_auth(config)
|
|
478
|
+
if user_auth_keys:
|
|
479
|
+
logger.debug(
|
|
480
|
+
f"{server_name}: Using user-specified auth header(s); skipping OAuth provider.",
|
|
481
|
+
user_auth_headers=sorted(user_auth_keys),
|
|
482
|
+
)
|
|
443
483
|
return _add_none_to_context(
|
|
444
484
|
sse_client(
|
|
445
485
|
config.url,
|
|
@@ -453,19 +493,22 @@ class MCPConnectionManager(ContextDependent):
|
|
|
453
493
|
raise ValueError(
|
|
454
494
|
f"Server '{server_name}' uses http transport but no url is specified"
|
|
455
495
|
)
|
|
456
|
-
oauth_auth =
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
496
|
+
headers, oauth_auth, user_auth_keys = _prepare_headers_and_auth(config)
|
|
497
|
+
if user_auth_keys:
|
|
498
|
+
logger.debug(
|
|
499
|
+
f"{server_name}: Using user-specified auth header(s); skipping OAuth provider.",
|
|
500
|
+
user_auth_headers=sorted(user_auth_keys),
|
|
501
|
+
)
|
|
461
502
|
channel_hook = None
|
|
462
503
|
if transport_metrics is not None:
|
|
504
|
+
|
|
463
505
|
def channel_hook(event):
|
|
464
506
|
try:
|
|
465
507
|
transport_metrics.record_event(event)
|
|
466
508
|
except Exception: # pragma: no cover - defensive guard
|
|
467
509
|
logger.debug(
|
|
468
|
-
"%s: transport metrics hook failed",
|
|
510
|
+
"%s: transport metrics hook failed",
|
|
511
|
+
server_name,
|
|
469
512
|
exc_info=True,
|
|
470
513
|
)
|
|
471
514
|
|
|
@@ -117,6 +117,7 @@ def load_prompt(file: Path) -> List[PromptMessageExtended]:
|
|
|
117
117
|
if path_str.endswith(".json"):
|
|
118
118
|
# JSON files use the serialization module directly
|
|
119
119
|
from fast_agent.mcp.prompt_serialization import load_messages
|
|
120
|
+
|
|
120
121
|
return load_messages(str(file))
|
|
121
122
|
else:
|
|
122
123
|
# Non-JSON files need template processing for resource loading
|
|
@@ -128,15 +129,13 @@ def load_prompt(file: Path) -> List[PromptMessageExtended]:
|
|
|
128
129
|
# Render the template without arguments to get the messages
|
|
129
130
|
messages = create_messages_with_resources(
|
|
130
131
|
template.content_sections,
|
|
131
|
-
[file] # Pass the file path for resource resolution
|
|
132
|
+
[file], # Pass the file path for resource resolution
|
|
132
133
|
)
|
|
133
134
|
|
|
134
135
|
# Convert to PromptMessageExtended
|
|
135
136
|
return PromptMessageExtended.to_extended(messages)
|
|
136
137
|
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
140
139
|
def load_prompt_as_get_prompt_result(file: Path):
|
|
141
140
|
"""
|
|
142
141
|
Load a prompt from a file and convert to GetPromptResult format for MCP compatibility.
|
fast_agent/ui/console_display.py
CHANGED
|
@@ -6,6 +6,7 @@ from mcp.types import CallToolResult
|
|
|
6
6
|
from rich.panel import Panel
|
|
7
7
|
from rich.text import Text
|
|
8
8
|
|
|
9
|
+
from fast_agent.constants import REASONING
|
|
9
10
|
from fast_agent.ui import console
|
|
10
11
|
from fast_agent.ui.mcp_ui_utils import UILink
|
|
11
12
|
from fast_agent.ui.mermaid_utils import (
|
|
@@ -144,6 +145,25 @@ class ConsoleDisplay:
|
|
|
144
145
|
self._markup = config.logger.enable_markup if config else True
|
|
145
146
|
self._escape_xml = True
|
|
146
147
|
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _format_elapsed(elapsed: float) -> str:
|
|
150
|
+
"""Format elapsed seconds for display."""
|
|
151
|
+
if elapsed < 0:
|
|
152
|
+
elapsed = 0.0
|
|
153
|
+
if elapsed < 0.001:
|
|
154
|
+
return "<1ms"
|
|
155
|
+
if elapsed < 1:
|
|
156
|
+
return f"{elapsed * 1000:.0f}ms"
|
|
157
|
+
if elapsed < 10:
|
|
158
|
+
return f"{elapsed:.2f}s"
|
|
159
|
+
if elapsed < 60:
|
|
160
|
+
return f"{elapsed:.1f}s"
|
|
161
|
+
minutes, seconds = divmod(elapsed, 60)
|
|
162
|
+
if minutes < 60:
|
|
163
|
+
return f"{int(minutes)}m {seconds:02.0f}s"
|
|
164
|
+
hours, minutes = divmod(int(minutes), 60)
|
|
165
|
+
return f"{hours}h {minutes:02d}m"
|
|
166
|
+
|
|
147
167
|
def display_message(
|
|
148
168
|
self,
|
|
149
169
|
content: Any,
|
|
@@ -156,6 +176,7 @@ class ConsoleDisplay:
|
|
|
156
176
|
is_error: bool = False,
|
|
157
177
|
truncate_content: bool = True,
|
|
158
178
|
additional_message: Text | None = None,
|
|
179
|
+
pre_content: Text | None = None,
|
|
159
180
|
) -> None:
|
|
160
181
|
"""
|
|
161
182
|
Unified method to display formatted messages to the console.
|
|
@@ -170,6 +191,8 @@ class ConsoleDisplay:
|
|
|
170
191
|
max_item_length: Optional max length for bottom metadata items (with ellipsis)
|
|
171
192
|
is_error: For tool results, whether this is an error (uses red color)
|
|
172
193
|
truncate_content: Whether to truncate long content
|
|
194
|
+
additional_message: Optional Rich Text appended after the main content
|
|
195
|
+
pre_content: Optional Rich Text shown before the main content
|
|
173
196
|
"""
|
|
174
197
|
# Get configuration for this message type
|
|
175
198
|
config = MESSAGE_CONFIGS[message_type]
|
|
@@ -191,6 +214,8 @@ class ConsoleDisplay:
|
|
|
191
214
|
self._create_combined_separator_status(left, right_info)
|
|
192
215
|
|
|
193
216
|
# Display the content
|
|
217
|
+
if pre_content and pre_content.plain:
|
|
218
|
+
console.console.print(pre_content, markup=self._markup)
|
|
194
219
|
self._display_content(
|
|
195
220
|
content, truncate_content, is_error, message_type, check_markdown_markers=False
|
|
196
221
|
)
|
|
@@ -544,7 +569,7 @@ class ConsoleDisplay:
|
|
|
544
569
|
|
|
545
570
|
# Build transport channel info for bottom bar
|
|
546
571
|
channel = getattr(result, "transport_channel", None)
|
|
547
|
-
|
|
572
|
+
bottom_metadata_items: List[str] = []
|
|
548
573
|
if channel:
|
|
549
574
|
# Format channel info for bottom bar
|
|
550
575
|
if channel == "post-json":
|
|
@@ -560,7 +585,13 @@ class ConsoleDisplay:
|
|
|
560
585
|
else:
|
|
561
586
|
transport_info = channel.upper()
|
|
562
587
|
|
|
563
|
-
|
|
588
|
+
bottom_metadata_items.append(transport_info)
|
|
589
|
+
|
|
590
|
+
elapsed = getattr(result, "transport_elapsed", None)
|
|
591
|
+
if isinstance(elapsed, (int, float)):
|
|
592
|
+
bottom_metadata_items.append(self._format_elapsed(float(elapsed)))
|
|
593
|
+
|
|
594
|
+
bottom_metadata = bottom_metadata_items or None
|
|
564
595
|
|
|
565
596
|
# Build right info (without channel info)
|
|
566
597
|
right_info = f"[dim]tool result - {status}[/dim]"
|
|
@@ -724,8 +755,26 @@ class ConsoleDisplay:
|
|
|
724
755
|
# Extract text from PromptMessageExtended if needed
|
|
725
756
|
from fast_agent.types import PromptMessageExtended
|
|
726
757
|
|
|
758
|
+
pre_content: Text | None = None
|
|
759
|
+
|
|
727
760
|
if isinstance(message_text, PromptMessageExtended):
|
|
728
761
|
display_text = message_text.last_text() or ""
|
|
762
|
+
|
|
763
|
+
channels = message_text.channels or {}
|
|
764
|
+
reasoning_blocks = channels.get(REASONING) or []
|
|
765
|
+
if reasoning_blocks:
|
|
766
|
+
from fast_agent.mcp.helpers.content_helpers import get_text
|
|
767
|
+
|
|
768
|
+
reasoning_segments = []
|
|
769
|
+
for block in reasoning_blocks:
|
|
770
|
+
text = get_text(block)
|
|
771
|
+
if text:
|
|
772
|
+
reasoning_segments.append(text)
|
|
773
|
+
|
|
774
|
+
if reasoning_segments:
|
|
775
|
+
joined = "\n".join(reasoning_segments)
|
|
776
|
+
if joined.strip():
|
|
777
|
+
pre_content = Text(joined, style="dim default")
|
|
729
778
|
else:
|
|
730
779
|
display_text = message_text
|
|
731
780
|
|
|
@@ -743,6 +792,7 @@ class ConsoleDisplay:
|
|
|
743
792
|
max_item_length=max_item_length,
|
|
744
793
|
truncate_content=False, # Assistant messages shouldn't be truncated
|
|
745
794
|
additional_message=additional_message,
|
|
795
|
+
pre_content=pre_content,
|
|
746
796
|
)
|
|
747
797
|
|
|
748
798
|
# Handle mermaid diagrams separately (after the main message)
|
fast_agent/ui/enhanced_prompt.py
CHANGED
|
@@ -339,15 +339,16 @@ class AgentCompleter(Completer):
|
|
|
339
339
|
# Map commands to their descriptions for better completion hints
|
|
340
340
|
self.commands = {
|
|
341
341
|
"mcp": "Show MCP server status",
|
|
342
|
+
"history": "Show conversation history overview (optionally another agent)",
|
|
342
343
|
"tools": "List available MCP tools",
|
|
343
344
|
"prompt": "List and choose MCP prompts, or apply specific prompt (/prompt <name>)",
|
|
345
|
+
"clear": "Clear history",
|
|
344
346
|
"agents": "List available agents",
|
|
345
347
|
"system": "Show the current system prompt",
|
|
346
348
|
"usage": "Show current usage statistics",
|
|
347
349
|
"markdown": "Show last assistant message without markdown formatting",
|
|
348
350
|
"save_history": "Save history; .json = MCP JSON, others = Markdown",
|
|
349
351
|
"help": "Show commands and shortcuts",
|
|
350
|
-
"clear": "Clear the screen",
|
|
351
352
|
"EXIT": "Exit fast-agent, terminating any running workflows",
|
|
352
353
|
"STOP": "Stop this prompting session and move to next workflow step",
|
|
353
354
|
**(commands or {}), # Allow custom commands to be passed in
|
|
@@ -518,7 +519,15 @@ def create_keybindings(
|
|
|
518
519
|
|
|
519
520
|
@kb.add("c-l")
|
|
520
521
|
def _(event) -> None:
|
|
521
|
-
"""Ctrl+L: Clear the
|
|
522
|
+
"""Ctrl+L: Clear and redraw the terminal screen."""
|
|
523
|
+
app_ref = event.app or app
|
|
524
|
+
if app_ref and getattr(app_ref, "renderer", None):
|
|
525
|
+
app_ref.renderer.clear()
|
|
526
|
+
app_ref.invalidate()
|
|
527
|
+
|
|
528
|
+
@kb.add("c-u")
|
|
529
|
+
def _(event) -> None:
|
|
530
|
+
"""Ctrl+U: Clear the input buffer."""
|
|
522
531
|
event.current_buffer.text = ""
|
|
523
532
|
|
|
524
533
|
@kb.add("c-e")
|
|
@@ -725,13 +734,24 @@ async def get_enhanced_input(
|
|
|
725
734
|
# Check for active events first (highest priority)
|
|
726
735
|
active_status = notification_tracker.get_active_status()
|
|
727
736
|
if active_status:
|
|
728
|
-
event_type = active_status[
|
|
729
|
-
server = active_status[
|
|
730
|
-
notification_segment =
|
|
737
|
+
event_type = active_status["type"].upper()
|
|
738
|
+
server = active_status["server"]
|
|
739
|
+
notification_segment = (
|
|
740
|
+
f" | <style fg='ansired' bg='ansiblack'>◀ {event_type} ({server})</style>"
|
|
741
|
+
)
|
|
731
742
|
elif notification_tracker.get_count() > 0:
|
|
732
743
|
# Show completed events summary when no active events
|
|
733
|
-
|
|
734
|
-
|
|
744
|
+
counts_by_type = notification_tracker.get_counts_by_type()
|
|
745
|
+
total_events = sum(counts_by_type.values()) if counts_by_type else 0
|
|
746
|
+
|
|
747
|
+
if len(counts_by_type) == 1:
|
|
748
|
+
event_type, count = next(iter(counts_by_type.items()))
|
|
749
|
+
label_text = notification_tracker.format_event_label(event_type, count)
|
|
750
|
+
notification_segment = f" | ◀ {label_text}"
|
|
751
|
+
else:
|
|
752
|
+
summary = notification_tracker.get_summary(compact=True)
|
|
753
|
+
heading = "event" if total_events == 1 else "events"
|
|
754
|
+
notification_segment = f" | ◀ {total_events} {heading} ({summary})"
|
|
735
755
|
|
|
736
756
|
if middle:
|
|
737
757
|
return HTML(
|
|
@@ -824,14 +844,26 @@ async def get_enhanced_input(
|
|
|
824
844
|
|
|
825
845
|
if cmd == "help":
|
|
826
846
|
return "HELP"
|
|
827
|
-
elif cmd == "clear":
|
|
828
|
-
return "CLEAR"
|
|
829
847
|
elif cmd == "agents":
|
|
830
848
|
return "LIST_AGENTS"
|
|
831
849
|
elif cmd == "system":
|
|
832
850
|
return "SHOW_SYSTEM"
|
|
833
851
|
elif cmd == "usage":
|
|
834
852
|
return "SHOW_USAGE"
|
|
853
|
+
elif cmd == "history":
|
|
854
|
+
target_agent = None
|
|
855
|
+
if len(cmd_parts) > 1:
|
|
856
|
+
candidate = cmd_parts[1].strip()
|
|
857
|
+
if candidate:
|
|
858
|
+
target_agent = candidate
|
|
859
|
+
return {"show_history": {"agent": target_agent}}
|
|
860
|
+
elif cmd == "clear":
|
|
861
|
+
target_agent = None
|
|
862
|
+
if len(cmd_parts) > 1:
|
|
863
|
+
candidate = cmd_parts[1].strip()
|
|
864
|
+
if candidate:
|
|
865
|
+
target_agent = candidate
|
|
866
|
+
return {"clear_history": {"agent": target_agent}}
|
|
835
867
|
elif cmd == "markdown":
|
|
836
868
|
return "MARKDOWN"
|
|
837
869
|
elif cmd in ("save_history", "save"):
|
|
@@ -1010,15 +1042,18 @@ async def handle_special_commands(command, agent_app=None):
|
|
|
1010
1042
|
if isinstance(command, dict):
|
|
1011
1043
|
return command
|
|
1012
1044
|
|
|
1045
|
+
global agent_histories
|
|
1046
|
+
|
|
1013
1047
|
# Check for special string commands
|
|
1014
1048
|
if command == "HELP":
|
|
1015
1049
|
rich_print("\n[bold]Available Commands:[/bold]")
|
|
1016
1050
|
rich_print(" /help - Show this help")
|
|
1017
|
-
rich_print(" /clear - Clear screen")
|
|
1018
1051
|
rich_print(" /agents - List available agents")
|
|
1019
1052
|
rich_print(" /system - Show the current system prompt")
|
|
1020
1053
|
rich_print(" /prompt <name> - Apply a specific prompt by name")
|
|
1021
1054
|
rich_print(" /usage - Show current usage statistics")
|
|
1055
|
+
rich_print(" /history [agent_name] - Show chat history overview")
|
|
1056
|
+
rich_print(" /clear [agent_name] - Clear conversation history (keeps templates)")
|
|
1022
1057
|
rich_print(" /markdown - Show last assistant message without markdown formatting")
|
|
1023
1058
|
rich_print(" /mcpstatus - Show MCP server status summary for the active agent")
|
|
1024
1059
|
rich_print(" /save_history <filename> - Save current chat history to a file")
|
|
@@ -1034,15 +1069,11 @@ async def handle_special_commands(command, agent_app=None):
|
|
|
1034
1069
|
rich_print(" Ctrl+T - Toggle multiline mode")
|
|
1035
1070
|
rich_print(" Ctrl+E - Edit in external editor")
|
|
1036
1071
|
rich_print(" Ctrl+Y - Copy last assistant response to clipboard")
|
|
1037
|
-
rich_print(" Ctrl+L -
|
|
1072
|
+
rich_print(" Ctrl+L - Redraw the screen")
|
|
1073
|
+
rich_print(" Ctrl+U - Clear input")
|
|
1038
1074
|
rich_print(" Up/Down - Navigate history")
|
|
1039
1075
|
return True
|
|
1040
1076
|
|
|
1041
|
-
elif command == "CLEAR":
|
|
1042
|
-
# Clear screen (ANSI escape sequence)
|
|
1043
|
-
print("\033c", end="")
|
|
1044
|
-
return True
|
|
1045
|
-
|
|
1046
1077
|
elif isinstance(command, str) and command.upper() == "EXIT":
|
|
1047
1078
|
raise PromptExitError("User requested to exit fast-agent session")
|
|
1048
1079
|
|