fast-agent-mcp 0.3.10__py3-none-any.whl → 0.3.12__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_decorator.py +7 -0
- fast_agent/agents/tool_agent.py +2 -2
- fast_agent/cli/commands/quickstart.py +142 -129
- fast_agent/config.py +2 -2
- fast_agent/core/direct_decorators.py +18 -26
- fast_agent/core/fastagent.py +2 -1
- fast_agent/human_input/elicitation_handler.py +71 -54
- 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/mcp/elicitation_factory.py +2 -2
- fast_agent/mcp/mcp_aggregator.py +9 -16
- fast_agent/mcp/prompts/prompt_load.py +2 -3
- fast_agent/mcp/sampling.py +21 -0
- fast_agent/ui/console_display.py +27 -22
- fast_agent/ui/enhanced_prompt.py +61 -14
- fast_agent/ui/history_display.py +555 -0
- fast_agent/ui/interactive_prompt.py +44 -12
- fast_agent/ui/mcp_display.py +31 -11
- fast_agent/ui/notification_tracker.py +206 -0
- fast_agent/ui/rich_progress.py +1 -1
- {fast_agent_mcp-0.3.10.dist-info → fast_agent_mcp-0.3.12.dist-info}/METADATA +4 -4
- {fast_agent_mcp-0.3.10.dist-info → fast_agent_mcp-0.3.12.dist-info}/RECORD +27 -25
- {fast_agent_mcp-0.3.10.dist-info → fast_agent_mcp-0.3.12.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.3.10.dist-info → fast_agent_mcp-0.3.12.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.3.10.dist-info → fast_agent_mcp-0.3.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"""Display helpers for agent conversation history."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from shutil import get_terminal_size
|
|
6
|
+
from typing import TYPE_CHECKING, Optional, Sequence
|
|
7
|
+
|
|
8
|
+
from rich import print as rich_print
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
from fast_agent.mcp.helpers.content_helpers import get_text
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
from fast_agent.llm.usage_tracking import UsageAccumulator
|
|
19
|
+
from fast_agent.types import PromptMessageExtended
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
NON_TEXT_MARKER = "^"
|
|
23
|
+
TIMELINE_WIDTH = 20
|
|
24
|
+
SUMMARY_COUNT = 8
|
|
25
|
+
ROLE_COLUMN_WIDTH = 17
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _normalize_text(value: Optional[str]) -> str:
|
|
29
|
+
return "" if not value else " ".join(value.split())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Colours:
|
|
33
|
+
"""Central colour palette for history display output."""
|
|
34
|
+
|
|
35
|
+
USER = "blue"
|
|
36
|
+
ASSISTANT = "green"
|
|
37
|
+
TOOL = "magenta"
|
|
38
|
+
HEADER = USER
|
|
39
|
+
TIMELINE_EMPTY = "dim default"
|
|
40
|
+
CONTEXT_SAFE = "green"
|
|
41
|
+
CONTEXT_CAUTION = "yellow"
|
|
42
|
+
CONTEXT_ALERT = "bright_red"
|
|
43
|
+
TOOL_DETAIL = "dim magenta"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _char_count(value: Optional[str]) -> int:
|
|
47
|
+
return len(_normalize_text(value))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _format_tool_detail(prefix: str, names: Sequence[str]) -> Text:
|
|
51
|
+
detail = Text(prefix, style=Colours.TOOL_DETAIL)
|
|
52
|
+
if names:
|
|
53
|
+
detail.append(", ".join(names), style=Colours.TOOL_DETAIL)
|
|
54
|
+
return detail
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _ensure_text(value: object | None) -> Text:
|
|
58
|
+
"""Coerce various value types into a Rich Text instance."""
|
|
59
|
+
|
|
60
|
+
if isinstance(value, Text):
|
|
61
|
+
return value.copy()
|
|
62
|
+
if value is None:
|
|
63
|
+
return Text("")
|
|
64
|
+
if isinstance(value, str):
|
|
65
|
+
return Text(value)
|
|
66
|
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, Text)):
|
|
67
|
+
return Text(", ".join(str(item) for item in value if item))
|
|
68
|
+
return Text(str(value))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _truncate_text_segment(segment: Text, width: int) -> Text:
|
|
72
|
+
if width <= 0 or segment.cell_len == 0:
|
|
73
|
+
return Text("")
|
|
74
|
+
if segment.cell_len <= width:
|
|
75
|
+
return segment.copy()
|
|
76
|
+
truncated = segment.copy()
|
|
77
|
+
truncated.truncate(width, overflow="ellipsis")
|
|
78
|
+
return truncated
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _compose_summary_text(
|
|
82
|
+
preview: Text,
|
|
83
|
+
detail: Optional[Text],
|
|
84
|
+
*,
|
|
85
|
+
include_non_text: bool,
|
|
86
|
+
max_width: Optional[int],
|
|
87
|
+
) -> Text:
|
|
88
|
+
marker_component = Text()
|
|
89
|
+
if include_non_text:
|
|
90
|
+
marker_component.append(" ")
|
|
91
|
+
marker_component.append(NON_TEXT_MARKER, style="dim")
|
|
92
|
+
|
|
93
|
+
if max_width is None:
|
|
94
|
+
combined = Text()
|
|
95
|
+
combined.append_text(preview)
|
|
96
|
+
if detail and detail.cell_len > 0:
|
|
97
|
+
if combined.cell_len > 0:
|
|
98
|
+
combined.append(" ")
|
|
99
|
+
combined.append_text(detail)
|
|
100
|
+
combined.append_text(marker_component)
|
|
101
|
+
return combined
|
|
102
|
+
|
|
103
|
+
width_available = max_width
|
|
104
|
+
if width_available <= 0:
|
|
105
|
+
return Text("")
|
|
106
|
+
|
|
107
|
+
if marker_component.cell_len > width_available:
|
|
108
|
+
marker_component = Text("")
|
|
109
|
+
marker_width = marker_component.cell_len
|
|
110
|
+
width_after_marker = max(0, width_available - marker_width)
|
|
111
|
+
|
|
112
|
+
preview_len = preview.cell_len
|
|
113
|
+
detail_component = detail.copy() if detail else Text("")
|
|
114
|
+
detail_len = detail_component.cell_len
|
|
115
|
+
detail_plain = detail_component.plain
|
|
116
|
+
|
|
117
|
+
preview_allow = min(preview_len, width_after_marker)
|
|
118
|
+
detail_allow = 0
|
|
119
|
+
if detail_len > 0 and width_after_marker > 0:
|
|
120
|
+
detail_allow = min(detail_len, max(0, width_after_marker - preview_allow))
|
|
121
|
+
|
|
122
|
+
if width_after_marker > 0:
|
|
123
|
+
min_detail_allow = 1
|
|
124
|
+
for prefix in ("tool→", "result→"):
|
|
125
|
+
if detail_plain.startswith(prefix):
|
|
126
|
+
min_detail_allow = min(detail_len, len(prefix))
|
|
127
|
+
break
|
|
128
|
+
else:
|
|
129
|
+
min_detail_allow = 0
|
|
130
|
+
if detail_allow < min_detail_allow:
|
|
131
|
+
needed = min_detail_allow - detail_allow
|
|
132
|
+
reduction = min(preview_allow, needed)
|
|
133
|
+
preview_allow -= reduction
|
|
134
|
+
detail_allow += reduction
|
|
135
|
+
|
|
136
|
+
preview_allow = max(0, preview_allow)
|
|
137
|
+
detail_allow = max(0, min(detail_allow, detail_len))
|
|
138
|
+
|
|
139
|
+
space = 1 if preview_allow > 0 and detail_allow > 0 else 0
|
|
140
|
+
total = preview_allow + detail_allow + space
|
|
141
|
+
if total > width_after_marker:
|
|
142
|
+
overflow = total - width_after_marker
|
|
143
|
+
reduction = min(preview_allow, overflow)
|
|
144
|
+
preview_allow -= reduction
|
|
145
|
+
overflow -= reduction
|
|
146
|
+
if overflow > 0:
|
|
147
|
+
detail_allow = max(0, detail_allow - overflow)
|
|
148
|
+
|
|
149
|
+
preview_allow = max(0, preview_allow)
|
|
150
|
+
detail_allow = max(0, min(detail_allow, detail_len))
|
|
151
|
+
else:
|
|
152
|
+
preview_allow = min(preview_len, width_after_marker)
|
|
153
|
+
detail_allow = 0
|
|
154
|
+
|
|
155
|
+
preview_segment = _truncate_text_segment(preview, preview_allow)
|
|
156
|
+
detail_segment = (
|
|
157
|
+
_truncate_text_segment(detail_component, detail_allow) if detail_allow > 0 else Text("")
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
combined = Text()
|
|
161
|
+
combined.append_text(preview_segment)
|
|
162
|
+
if preview_segment.cell_len > 0 and detail_segment.cell_len > 0:
|
|
163
|
+
combined.append(" ")
|
|
164
|
+
combined.append_text(detail_segment)
|
|
165
|
+
|
|
166
|
+
if marker_component.cell_len > 0:
|
|
167
|
+
if combined.cell_len + marker_component.cell_len <= max_width:
|
|
168
|
+
combined.append_text(marker_component)
|
|
169
|
+
|
|
170
|
+
return combined
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _preview_text(value: Optional[str], limit: int = 80) -> str:
|
|
174
|
+
normalized = _normalize_text(value)
|
|
175
|
+
if not normalized:
|
|
176
|
+
return "<no text>"
|
|
177
|
+
if len(normalized) <= limit:
|
|
178
|
+
return normalized
|
|
179
|
+
return normalized[: limit - 1] + "…"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _has_non_text_content(message: PromptMessageExtended) -> bool:
|
|
183
|
+
for block in getattr(message, "content", []) or []:
|
|
184
|
+
block_type = getattr(block, "type", None)
|
|
185
|
+
if block_type and block_type != "text":
|
|
186
|
+
return True
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _extract_tool_result_summary(result, *, limit: int = 80) -> tuple[str, int, bool]:
|
|
191
|
+
preview: Optional[str] = None
|
|
192
|
+
total_chars = 0
|
|
193
|
+
saw_non_text = False
|
|
194
|
+
|
|
195
|
+
for block in getattr(result, "content", []) or []:
|
|
196
|
+
text = get_text(block)
|
|
197
|
+
if text:
|
|
198
|
+
normalized = _normalize_text(text)
|
|
199
|
+
if preview is None:
|
|
200
|
+
preview = _preview_text(normalized, limit=limit)
|
|
201
|
+
total_chars += len(normalized)
|
|
202
|
+
else:
|
|
203
|
+
saw_non_text = True
|
|
204
|
+
|
|
205
|
+
if preview is not None:
|
|
206
|
+
return preview, total_chars, saw_non_text
|
|
207
|
+
return f"{NON_TEXT_MARKER} non-text tool result", 0, True
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def format_chars(value: int) -> str:
|
|
211
|
+
if value <= 0:
|
|
212
|
+
return "—"
|
|
213
|
+
if value >= 1_000_000:
|
|
214
|
+
return f"{value / 1_000_000:.1f}M"
|
|
215
|
+
if value >= 10_000:
|
|
216
|
+
return f"{value / 1_000:.1f}k"
|
|
217
|
+
return str(value)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _build_history_rows(history: Sequence[PromptMessageExtended]) -> list[dict]:
|
|
221
|
+
rows: list[dict] = []
|
|
222
|
+
call_name_lookup: dict[str, str] = {}
|
|
223
|
+
|
|
224
|
+
for message in history:
|
|
225
|
+
role_raw = getattr(message, "role", "assistant")
|
|
226
|
+
role_value = getattr(role_raw, "value", role_raw)
|
|
227
|
+
role = str(role_value).lower() if role_value else "assistant"
|
|
228
|
+
|
|
229
|
+
text = ""
|
|
230
|
+
if hasattr(message, "first_text"):
|
|
231
|
+
try:
|
|
232
|
+
text = message.first_text() or ""
|
|
233
|
+
except Exception: # pragma: no cover - defensive
|
|
234
|
+
text = ""
|
|
235
|
+
normalized_text = _normalize_text(text)
|
|
236
|
+
chars = len(normalized_text)
|
|
237
|
+
preview = _preview_text(text)
|
|
238
|
+
non_text = _has_non_text_content(message) or chars == 0
|
|
239
|
+
|
|
240
|
+
tool_calls: Optional[Mapping[str, object]] = getattr(message, "tool_calls", None)
|
|
241
|
+
tool_results: Optional[Mapping[str, object]] = getattr(message, "tool_results", None)
|
|
242
|
+
|
|
243
|
+
detail_sections: list[Text] = []
|
|
244
|
+
row_non_text = non_text
|
|
245
|
+
has_tool_request = False
|
|
246
|
+
hide_in_summary = False
|
|
247
|
+
timeline_role = role
|
|
248
|
+
include_in_timeline = True
|
|
249
|
+
result_rows: list[dict] = []
|
|
250
|
+
tool_result_total_chars = 0
|
|
251
|
+
tool_result_has_non_text = False
|
|
252
|
+
|
|
253
|
+
if tool_calls:
|
|
254
|
+
names: list[str] = []
|
|
255
|
+
for call_id, call in tool_calls.items():
|
|
256
|
+
params = getattr(call, "params", None)
|
|
257
|
+
name = getattr(params, "name", None) or getattr(call, "name", None) or call_id
|
|
258
|
+
call_name_lookup[call_id] = name
|
|
259
|
+
names.append(name)
|
|
260
|
+
if names:
|
|
261
|
+
detail_sections.append(_format_tool_detail("tool→", names))
|
|
262
|
+
row_non_text = row_non_text and chars == 0 # treat call as activity
|
|
263
|
+
has_tool_request = True
|
|
264
|
+
if not normalized_text and tool_calls:
|
|
265
|
+
preview = "(issuing tool request)"
|
|
266
|
+
|
|
267
|
+
if tool_results:
|
|
268
|
+
result_names: list[str] = []
|
|
269
|
+
for call_id, result in tool_results.items():
|
|
270
|
+
tool_name = call_name_lookup.get(call_id, call_id)
|
|
271
|
+
result_names.append(tool_name)
|
|
272
|
+
summary, result_chars, result_non_text = _extract_tool_result_summary(result)
|
|
273
|
+
tool_result_total_chars += result_chars
|
|
274
|
+
tool_result_has_non_text = tool_result_has_non_text or result_non_text
|
|
275
|
+
detail = _format_tool_detail("result→", [tool_name])
|
|
276
|
+
result_rows.append(
|
|
277
|
+
{
|
|
278
|
+
"role": "tool",
|
|
279
|
+
"timeline_role": "tool",
|
|
280
|
+
"chars": result_chars,
|
|
281
|
+
"preview": summary,
|
|
282
|
+
"details": detail,
|
|
283
|
+
"non_text": result_non_text,
|
|
284
|
+
"has_tool_request": False,
|
|
285
|
+
"hide_summary": False,
|
|
286
|
+
"include_in_timeline": False,
|
|
287
|
+
}
|
|
288
|
+
)
|
|
289
|
+
if role == "user":
|
|
290
|
+
timeline_role = "tool"
|
|
291
|
+
hide_in_summary = True
|
|
292
|
+
if result_names:
|
|
293
|
+
detail_sections.append(_format_tool_detail("result→", result_names))
|
|
294
|
+
|
|
295
|
+
if detail_sections:
|
|
296
|
+
if len(detail_sections) == 1:
|
|
297
|
+
details: Text | None = detail_sections[0]
|
|
298
|
+
else:
|
|
299
|
+
details = Text()
|
|
300
|
+
for index, section in enumerate(detail_sections):
|
|
301
|
+
if index > 0:
|
|
302
|
+
details.append(" ")
|
|
303
|
+
details.append_text(section)
|
|
304
|
+
else:
|
|
305
|
+
details = None
|
|
306
|
+
|
|
307
|
+
row_chars = chars
|
|
308
|
+
if timeline_role == "tool" and tool_result_total_chars > 0:
|
|
309
|
+
row_chars = tool_result_total_chars
|
|
310
|
+
row_non_text = row_non_text or tool_result_has_non_text
|
|
311
|
+
|
|
312
|
+
rows.append(
|
|
313
|
+
{
|
|
314
|
+
"role": role,
|
|
315
|
+
"timeline_role": timeline_role,
|
|
316
|
+
"chars": row_chars,
|
|
317
|
+
"preview": preview,
|
|
318
|
+
"details": details,
|
|
319
|
+
"non_text": row_non_text,
|
|
320
|
+
"has_tool_request": has_tool_request,
|
|
321
|
+
"hide_summary": hide_in_summary,
|
|
322
|
+
"include_in_timeline": include_in_timeline,
|
|
323
|
+
}
|
|
324
|
+
)
|
|
325
|
+
rows.extend(result_rows)
|
|
326
|
+
|
|
327
|
+
return rows
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _aggregate_timeline_entries(rows: Sequence[dict]) -> list[dict]:
|
|
331
|
+
return [
|
|
332
|
+
{
|
|
333
|
+
"role": row.get("timeline_role", row["role"]),
|
|
334
|
+
"chars": row["chars"],
|
|
335
|
+
"non_text": row["non_text"],
|
|
336
|
+
}
|
|
337
|
+
for row in rows
|
|
338
|
+
if row.get("include_in_timeline", True)
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _shade_block(chars: int, *, non_text: bool, color: str) -> Text:
|
|
343
|
+
if non_text:
|
|
344
|
+
return Text(NON_TEXT_MARKER, style=f"bold {color}")
|
|
345
|
+
if chars <= 0:
|
|
346
|
+
return Text("·", style="dim")
|
|
347
|
+
if chars < 50:
|
|
348
|
+
return Text("░", style=f"dim {color}")
|
|
349
|
+
if chars < 200:
|
|
350
|
+
return Text("▒", style=f"dim {color}")
|
|
351
|
+
if chars < 500:
|
|
352
|
+
return Text("▒", style=color)
|
|
353
|
+
if chars < 2000:
|
|
354
|
+
return Text("▓", style=color)
|
|
355
|
+
return Text("█", style=f"bold {color}")
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
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
|
+
recent = list(entries[-width:])
|
|
362
|
+
bar = Text(" history |", style="dim")
|
|
363
|
+
for entry in recent:
|
|
364
|
+
color = color_map.get(entry["role"], "ansiwhite")
|
|
365
|
+
bar.append_text(
|
|
366
|
+
_shade_block(entry["chars"], non_text=entry.get("non_text", False), color=color)
|
|
367
|
+
)
|
|
368
|
+
remaining = width - len(recent)
|
|
369
|
+
if remaining > 0:
|
|
370
|
+
bar.append("░" * remaining, style=Colours.TIMELINE_EMPTY)
|
|
371
|
+
bar.append("|", style="dim")
|
|
372
|
+
|
|
373
|
+
detail = Text(f"{len(entries)} turns", style="dim")
|
|
374
|
+
return bar, detail
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _build_context_bar_line(
|
|
378
|
+
current: int,
|
|
379
|
+
window: Optional[int],
|
|
380
|
+
width: int = TIMELINE_WIDTH,
|
|
381
|
+
) -> tuple[Text, Text]:
|
|
382
|
+
bar = Text(" context |", style="dim")
|
|
383
|
+
|
|
384
|
+
if not window or window <= 0:
|
|
385
|
+
bar.append("░" * width, style=Colours.TIMELINE_EMPTY)
|
|
386
|
+
bar.append("|", style="dim")
|
|
387
|
+
detail = Text(f"{format_chars(current)} tokens (unknown window)", style="dim")
|
|
388
|
+
return bar, detail
|
|
389
|
+
|
|
390
|
+
percent = current / window if window else 0.0
|
|
391
|
+
filled = min(width, int(round(min(percent, 1.0) * width)))
|
|
392
|
+
|
|
393
|
+
def color_for(pct: float) -> str:
|
|
394
|
+
if pct >= 0.9:
|
|
395
|
+
return Colours.CONTEXT_ALERT
|
|
396
|
+
if pct >= 0.7:
|
|
397
|
+
return Colours.CONTEXT_CAUTION
|
|
398
|
+
return Colours.CONTEXT_SAFE
|
|
399
|
+
|
|
400
|
+
color = color_for(percent)
|
|
401
|
+
if filled > 0:
|
|
402
|
+
bar.append("█" * filled, style=color)
|
|
403
|
+
if filled < width:
|
|
404
|
+
bar.append("░" * (width - filled), style=Colours.TIMELINE_EMPTY)
|
|
405
|
+
bar.append("|", style="dim")
|
|
406
|
+
bar.append(f" {percent * 100:5.1f}%", style="dim")
|
|
407
|
+
if percent > 1.0:
|
|
408
|
+
bar.append(f" +{(percent - 1) * 100:.0f}%", style="bold bright_red")
|
|
409
|
+
|
|
410
|
+
detail = Text(f"{format_chars(current)} / {format_chars(window)} →", style="dim")
|
|
411
|
+
return bar, detail
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _render_header_line(agent_name: str, *, console: Optional[Console], printer) -> None:
|
|
415
|
+
header = Text()
|
|
416
|
+
header.append("▎", style=Colours.HEADER)
|
|
417
|
+
header.append("●", style=f"dim {Colours.HEADER}")
|
|
418
|
+
header.append(" [ 1] ", style=Colours.HEADER)
|
|
419
|
+
header.append(str(agent_name), style=f"bold {Colours.USER}")
|
|
420
|
+
|
|
421
|
+
line = Text()
|
|
422
|
+
line.append_text(header)
|
|
423
|
+
line.append(" ")
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
total_width = console.width if console else get_terminal_size().columns
|
|
427
|
+
except Exception:
|
|
428
|
+
total_width = 80
|
|
429
|
+
|
|
430
|
+
separator_width = max(1, total_width - line.cell_len)
|
|
431
|
+
line.append("─" * separator_width, style="dim")
|
|
432
|
+
|
|
433
|
+
printer("")
|
|
434
|
+
printer(line)
|
|
435
|
+
printer("")
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def display_history_overview(
|
|
439
|
+
agent_name: str,
|
|
440
|
+
history: Sequence[PromptMessageExtended],
|
|
441
|
+
usage_accumulator: Optional["UsageAccumulator"] = None,
|
|
442
|
+
*,
|
|
443
|
+
console: Optional[Console] = None,
|
|
444
|
+
) -> None:
|
|
445
|
+
if not history:
|
|
446
|
+
printer = console.print if console else rich_print
|
|
447
|
+
printer("[dim]No conversation history yet[/dim]")
|
|
448
|
+
return
|
|
449
|
+
|
|
450
|
+
printer = console.print if console else rich_print
|
|
451
|
+
|
|
452
|
+
rows = _build_history_rows(history)
|
|
453
|
+
timeline_entries = _aggregate_timeline_entries(rows)
|
|
454
|
+
|
|
455
|
+
history_bar, history_detail = _build_history_bar(timeline_entries)
|
|
456
|
+
if usage_accumulator:
|
|
457
|
+
current_tokens = getattr(usage_accumulator, "current_context_tokens", 0)
|
|
458
|
+
window = getattr(usage_accumulator, "context_window_size", None)
|
|
459
|
+
else:
|
|
460
|
+
current_tokens = 0
|
|
461
|
+
window = None
|
|
462
|
+
context_bar, context_detail = _build_context_bar_line(current_tokens, window)
|
|
463
|
+
|
|
464
|
+
_render_header_line(agent_name, console=console, printer=printer)
|
|
465
|
+
|
|
466
|
+
gap = Text(" ")
|
|
467
|
+
combined_line = Text()
|
|
468
|
+
combined_line.append_text(history_bar)
|
|
469
|
+
combined_line.append_text(gap)
|
|
470
|
+
combined_line.append_text(context_bar)
|
|
471
|
+
printer(combined_line)
|
|
472
|
+
|
|
473
|
+
history_label_len = len(" history |")
|
|
474
|
+
context_label_len = len(" context |")
|
|
475
|
+
|
|
476
|
+
history_available = history_bar.cell_len - history_label_len
|
|
477
|
+
context_available = context_bar.cell_len - context_label_len
|
|
478
|
+
|
|
479
|
+
detail_line = Text()
|
|
480
|
+
detail_line.append(" " * history_label_len, style="dim")
|
|
481
|
+
detail_line.append_text(history_detail)
|
|
482
|
+
if history_available > history_detail.cell_len:
|
|
483
|
+
detail_line.append(" " * (history_available - history_detail.cell_len), style="dim")
|
|
484
|
+
detail_line.append_text(gap)
|
|
485
|
+
detail_line.append(" " * context_label_len, style="dim")
|
|
486
|
+
detail_line.append_text(context_detail)
|
|
487
|
+
if context_available > context_detail.cell_len:
|
|
488
|
+
detail_line.append(" " * (context_available - context_detail.cell_len), style="dim")
|
|
489
|
+
printer(detail_line)
|
|
490
|
+
|
|
491
|
+
printer("")
|
|
492
|
+
printer(
|
|
493
|
+
Text(" " + "─" * (history_bar.cell_len + context_bar.cell_len + gap.cell_len), style="dim")
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
header_line = Text(" ")
|
|
497
|
+
header_line.append(" #", style="dim")
|
|
498
|
+
header_line.append(" ", style="dim")
|
|
499
|
+
header_line.append(f" {'Role':<{ROLE_COLUMN_WIDTH}}", style="dim")
|
|
500
|
+
header_line.append(f" {'Chars':>7}", style="dim")
|
|
501
|
+
header_line.append(" ", style="dim")
|
|
502
|
+
header_line.append("Summary", style="dim")
|
|
503
|
+
printer(header_line)
|
|
504
|
+
|
|
505
|
+
summary_candidates = [row for row in rows if not row.get("hide_summary")]
|
|
506
|
+
summary_rows = summary_candidates[-SUMMARY_COUNT:]
|
|
507
|
+
start_index = len(summary_candidates) - len(summary_rows) + 1
|
|
508
|
+
|
|
509
|
+
role_arrows = {"user": "▶", "assistant": "◀", "tool": "▶"}
|
|
510
|
+
role_styles = {"user": Colours.USER, "assistant": Colours.ASSISTANT, "tool": Colours.TOOL}
|
|
511
|
+
role_labels = {"user": "user", "assistant": "assistant", "tool": "tool result"}
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
total_width = console.width if console else get_terminal_size().columns
|
|
515
|
+
except Exception:
|
|
516
|
+
total_width = 80
|
|
517
|
+
|
|
518
|
+
for offset, row in enumerate(summary_rows):
|
|
519
|
+
role = row["role"]
|
|
520
|
+
color = role_styles.get(role, "white")
|
|
521
|
+
arrow = role_arrows.get(role, "▶")
|
|
522
|
+
label = role_labels.get(role, role)
|
|
523
|
+
if role == "assistant" and row.get("has_tool_request"):
|
|
524
|
+
label = f"{label}*"
|
|
525
|
+
chars = row["chars"]
|
|
526
|
+
block = _shade_block(chars, non_text=row.get("non_text", False), color=color)
|
|
527
|
+
|
|
528
|
+
details = row.get("details")
|
|
529
|
+
preview_value = row["preview"]
|
|
530
|
+
preview_text = _ensure_text(preview_value)
|
|
531
|
+
detail_text = _ensure_text(details) if details else Text("")
|
|
532
|
+
if detail_text.cell_len == 0:
|
|
533
|
+
detail_text = None
|
|
534
|
+
|
|
535
|
+
line = Text(" ")
|
|
536
|
+
line.append(f"{start_index + offset:>2}", style="dim")
|
|
537
|
+
line.append(" ")
|
|
538
|
+
line.append_text(block)
|
|
539
|
+
line.append(" ")
|
|
540
|
+
line.append(arrow, style=color)
|
|
541
|
+
line.append(" ")
|
|
542
|
+
line.append(f"{label:<{ROLE_COLUMN_WIDTH}}", style=color)
|
|
543
|
+
line.append(f" {format_chars(chars):>7}", style="dim")
|
|
544
|
+
line.append(" ")
|
|
545
|
+
summary_width = max(0, total_width - line.cell_len)
|
|
546
|
+
summary_text = _compose_summary_text(
|
|
547
|
+
preview_text,
|
|
548
|
+
detail_text,
|
|
549
|
+
include_non_text=row.get("non_text", False),
|
|
550
|
+
max_width=summary_width,
|
|
551
|
+
)
|
|
552
|
+
line.append_text(summary_text)
|
|
553
|
+
printer(line)
|
|
554
|
+
|
|
555
|
+
printer("")
|
|
@@ -34,6 +34,7 @@ from fast_agent.ui.enhanced_prompt import (
|
|
|
34
34
|
handle_special_commands,
|
|
35
35
|
show_mcp_status,
|
|
36
36
|
)
|
|
37
|
+
from fast_agent.ui.history_display import display_history_overview
|
|
37
38
|
from fast_agent.ui.progress_display import progress_display
|
|
38
39
|
from fast_agent.ui.usage_display import collect_agents_from_provider, display_usage_report
|
|
39
40
|
|
|
@@ -170,6 +171,39 @@ class InteractivePrompt:
|
|
|
170
171
|
# Handle usage display
|
|
171
172
|
await self._show_usage(prompt_provider, agent)
|
|
172
173
|
continue
|
|
174
|
+
elif "show_history" in command_result:
|
|
175
|
+
target_agent = command_result.get("show_history", {}).get("agent") or agent
|
|
176
|
+
try:
|
|
177
|
+
agent_obj = prompt_provider._agent(target_agent)
|
|
178
|
+
except Exception:
|
|
179
|
+
rich_print(f"[red]Unable to load agent '{target_agent}'[/red]")
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
history = getattr(agent_obj, "message_history", [])
|
|
183
|
+
usage = getattr(agent_obj, "usage_accumulator", None)
|
|
184
|
+
display_history_overview(target_agent, history, usage)
|
|
185
|
+
continue
|
|
186
|
+
elif "clear_history" in command_result:
|
|
187
|
+
target_agent = command_result.get("clear_history", {}).get("agent") or agent
|
|
188
|
+
try:
|
|
189
|
+
agent_obj = prompt_provider._agent(target_agent)
|
|
190
|
+
except Exception:
|
|
191
|
+
rich_print(f"[red]Unable to load agent '{target_agent}'[/red]")
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
if hasattr(agent_obj, "clear"):
|
|
195
|
+
try:
|
|
196
|
+
agent_obj.clear()
|
|
197
|
+
rich_print(f"[green]History cleared for agent '{target_agent}'.[/green]")
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
rich_print(
|
|
200
|
+
f"[red]Failed to clear history for '{target_agent}': {exc}[/red]"
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
rich_print(
|
|
204
|
+
f"[yellow]Agent '{target_agent}' does not support clearing history.[/yellow]"
|
|
205
|
+
)
|
|
206
|
+
continue
|
|
173
207
|
elif "show_system" in command_result:
|
|
174
208
|
# Handle system prompt display
|
|
175
209
|
await self._show_system(prompt_provider, agent)
|
|
@@ -266,9 +300,7 @@ class InteractivePrompt:
|
|
|
266
300
|
console.print(combined)
|
|
267
301
|
rich_print()
|
|
268
302
|
|
|
269
|
-
async def _get_all_prompts(
|
|
270
|
-
self, prompt_provider: "AgentApp", agent_name: Optional[str] = None
|
|
271
|
-
):
|
|
303
|
+
async def _get_all_prompts(self, prompt_provider: "AgentApp", agent_name: Optional[str] = None):
|
|
272
304
|
"""
|
|
273
305
|
Get a list of all available prompts.
|
|
274
306
|
|
|
@@ -923,7 +955,7 @@ class InteractivePrompt:
|
|
|
923
955
|
agent = prompt_provider._agent(agent_name)
|
|
924
956
|
|
|
925
957
|
# Get the system prompt
|
|
926
|
-
system_prompt = getattr(agent,
|
|
958
|
+
system_prompt = getattr(agent, "instruction", None)
|
|
927
959
|
if not system_prompt:
|
|
928
960
|
rich_print("[yellow]No system prompt available[/yellow]")
|
|
929
961
|
return
|
|
@@ -936,24 +968,24 @@ class InteractivePrompt:
|
|
|
936
968
|
)
|
|
937
969
|
|
|
938
970
|
# Use the display utility to show the system prompt
|
|
939
|
-
if hasattr(agent,
|
|
971
|
+
if hasattr(agent, "display") and agent.display:
|
|
940
972
|
agent.display.show_system_message(
|
|
941
|
-
system_prompt=system_prompt,
|
|
942
|
-
agent_name=agent_name,
|
|
943
|
-
server_count=server_count
|
|
973
|
+
system_prompt=system_prompt, agent_name=agent_name, server_count=server_count
|
|
944
974
|
)
|
|
945
975
|
else:
|
|
946
976
|
# Fallback to basic display
|
|
947
977
|
from fast_agent.ui.console_display import ConsoleDisplay
|
|
948
|
-
|
|
978
|
+
|
|
979
|
+
display = ConsoleDisplay(
|
|
980
|
+
config=agent.context.config if hasattr(agent, "context") else None
|
|
981
|
+
)
|
|
949
982
|
display.show_system_message(
|
|
950
|
-
system_prompt=system_prompt,
|
|
951
|
-
agent_name=agent_name,
|
|
952
|
-
server_count=server_count
|
|
983
|
+
system_prompt=system_prompt, agent_name=agent_name, server_count=server_count
|
|
953
984
|
)
|
|
954
985
|
|
|
955
986
|
except Exception as e:
|
|
956
987
|
import traceback
|
|
988
|
+
|
|
957
989
|
rich_print(f"[red]Error showing system prompt: {e}[/red]")
|
|
958
990
|
rich_print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
959
991
|
|