fast-agent-mcp 0.4.7__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.
- fast_agent/__init__.py +183 -0
- fast_agent/acp/__init__.py +19 -0
- fast_agent/acp/acp_aware_mixin.py +304 -0
- fast_agent/acp/acp_context.py +437 -0
- fast_agent/acp/content_conversion.py +136 -0
- fast_agent/acp/filesystem_runtime.py +427 -0
- fast_agent/acp/permission_store.py +269 -0
- fast_agent/acp/server/__init__.py +5 -0
- fast_agent/acp/server/agent_acp_server.py +1472 -0
- fast_agent/acp/slash_commands.py +1050 -0
- fast_agent/acp/terminal_runtime.py +408 -0
- fast_agent/acp/tool_permission_adapter.py +125 -0
- fast_agent/acp/tool_permissions.py +474 -0
- fast_agent/acp/tool_progress.py +814 -0
- fast_agent/agents/__init__.py +85 -0
- fast_agent/agents/agent_types.py +64 -0
- fast_agent/agents/llm_agent.py +350 -0
- fast_agent/agents/llm_decorator.py +1139 -0
- fast_agent/agents/mcp_agent.py +1337 -0
- fast_agent/agents/tool_agent.py +271 -0
- fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
- fast_agent/agents/workflow/chain_agent.py +212 -0
- fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
- fast_agent/agents/workflow/iterative_planner.py +652 -0
- fast_agent/agents/workflow/maker_agent.py +379 -0
- fast_agent/agents/workflow/orchestrator_models.py +218 -0
- fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
- fast_agent/agents/workflow/parallel_agent.py +250 -0
- fast_agent/agents/workflow/router_agent.py +353 -0
- fast_agent/cli/__init__.py +0 -0
- fast_agent/cli/__main__.py +73 -0
- fast_agent/cli/commands/acp.py +159 -0
- fast_agent/cli/commands/auth.py +404 -0
- fast_agent/cli/commands/check_config.py +783 -0
- fast_agent/cli/commands/go.py +514 -0
- fast_agent/cli/commands/quickstart.py +557 -0
- fast_agent/cli/commands/serve.py +143 -0
- fast_agent/cli/commands/server_helpers.py +114 -0
- fast_agent/cli/commands/setup.py +174 -0
- fast_agent/cli/commands/url_parser.py +190 -0
- fast_agent/cli/constants.py +40 -0
- fast_agent/cli/main.py +115 -0
- fast_agent/cli/terminal.py +24 -0
- fast_agent/config.py +798 -0
- fast_agent/constants.py +41 -0
- fast_agent/context.py +279 -0
- fast_agent/context_dependent.py +50 -0
- fast_agent/core/__init__.py +92 -0
- fast_agent/core/agent_app.py +448 -0
- fast_agent/core/core_app.py +137 -0
- fast_agent/core/direct_decorators.py +784 -0
- fast_agent/core/direct_factory.py +620 -0
- fast_agent/core/error_handling.py +27 -0
- fast_agent/core/exceptions.py +90 -0
- fast_agent/core/executor/__init__.py +0 -0
- fast_agent/core/executor/executor.py +280 -0
- fast_agent/core/executor/task_registry.py +32 -0
- fast_agent/core/executor/workflow_signal.py +324 -0
- fast_agent/core/fastagent.py +1186 -0
- fast_agent/core/logging/__init__.py +5 -0
- fast_agent/core/logging/events.py +138 -0
- fast_agent/core/logging/json_serializer.py +164 -0
- fast_agent/core/logging/listeners.py +309 -0
- fast_agent/core/logging/logger.py +278 -0
- fast_agent/core/logging/transport.py +481 -0
- fast_agent/core/prompt.py +9 -0
- fast_agent/core/prompt_templates.py +183 -0
- fast_agent/core/validation.py +326 -0
- fast_agent/event_progress.py +62 -0
- fast_agent/history/history_exporter.py +49 -0
- fast_agent/human_input/__init__.py +47 -0
- fast_agent/human_input/elicitation_handler.py +123 -0
- fast_agent/human_input/elicitation_state.py +33 -0
- fast_agent/human_input/form_elements.py +59 -0
- fast_agent/human_input/form_fields.py +256 -0
- fast_agent/human_input/simple_form.py +113 -0
- fast_agent/human_input/types.py +40 -0
- fast_agent/interfaces.py +310 -0
- fast_agent/llm/__init__.py +9 -0
- fast_agent/llm/cancellation.py +22 -0
- fast_agent/llm/fastagent_llm.py +931 -0
- fast_agent/llm/internal/passthrough.py +161 -0
- fast_agent/llm/internal/playback.py +129 -0
- fast_agent/llm/internal/silent.py +41 -0
- fast_agent/llm/internal/slow.py +38 -0
- fast_agent/llm/memory.py +275 -0
- fast_agent/llm/model_database.py +490 -0
- fast_agent/llm/model_factory.py +388 -0
- fast_agent/llm/model_info.py +102 -0
- fast_agent/llm/prompt_utils.py +155 -0
- fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
- fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
- fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
- fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
- fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
- fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
- fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
- fast_agent/llm/provider/google/google_converter.py +466 -0
- fast_agent/llm/provider/google/llm_google_native.py +681 -0
- fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
- fast_agent/llm/provider/openai/llm_azure.py +143 -0
- fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
- fast_agent/llm/provider/openai/llm_generic.py +35 -0
- fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
- fast_agent/llm/provider/openai/llm_groq.py +42 -0
- fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
- fast_agent/llm/provider/openai/llm_openai.py +1195 -0
- fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
- fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
- fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
- fast_agent/llm/provider/openai/llm_xai.py +38 -0
- fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
- fast_agent/llm/provider/openai/openai_multipart.py +169 -0
- fast_agent/llm/provider/openai/openai_utils.py +67 -0
- fast_agent/llm/provider/openai/responses.py +133 -0
- fast_agent/llm/provider_key_manager.py +139 -0
- fast_agent/llm/provider_types.py +34 -0
- fast_agent/llm/request_params.py +61 -0
- fast_agent/llm/sampling_converter.py +98 -0
- fast_agent/llm/stream_types.py +9 -0
- fast_agent/llm/usage_tracking.py +445 -0
- fast_agent/mcp/__init__.py +56 -0
- fast_agent/mcp/common.py +26 -0
- fast_agent/mcp/elicitation_factory.py +84 -0
- fast_agent/mcp/elicitation_handlers.py +164 -0
- fast_agent/mcp/gen_client.py +83 -0
- fast_agent/mcp/helpers/__init__.py +36 -0
- fast_agent/mcp/helpers/content_helpers.py +352 -0
- fast_agent/mcp/helpers/server_config_helpers.py +25 -0
- fast_agent/mcp/hf_auth.py +147 -0
- fast_agent/mcp/interfaces.py +92 -0
- fast_agent/mcp/logger_textio.py +108 -0
- fast_agent/mcp/mcp_agent_client_session.py +411 -0
- fast_agent/mcp/mcp_aggregator.py +2175 -0
- fast_agent/mcp/mcp_connection_manager.py +723 -0
- fast_agent/mcp/mcp_content.py +262 -0
- fast_agent/mcp/mime_utils.py +108 -0
- fast_agent/mcp/oauth_client.py +509 -0
- fast_agent/mcp/prompt.py +159 -0
- fast_agent/mcp/prompt_message_extended.py +155 -0
- fast_agent/mcp/prompt_render.py +84 -0
- fast_agent/mcp/prompt_serialization.py +580 -0
- fast_agent/mcp/prompts/__init__.py +0 -0
- fast_agent/mcp/prompts/__main__.py +7 -0
- fast_agent/mcp/prompts/prompt_constants.py +18 -0
- fast_agent/mcp/prompts/prompt_helpers.py +238 -0
- fast_agent/mcp/prompts/prompt_load.py +186 -0
- fast_agent/mcp/prompts/prompt_server.py +552 -0
- fast_agent/mcp/prompts/prompt_template.py +438 -0
- fast_agent/mcp/resource_utils.py +215 -0
- fast_agent/mcp/sampling.py +200 -0
- fast_agent/mcp/server/__init__.py +4 -0
- fast_agent/mcp/server/agent_server.py +613 -0
- fast_agent/mcp/skybridge.py +44 -0
- fast_agent/mcp/sse_tracking.py +287 -0
- fast_agent/mcp/stdio_tracking_simple.py +59 -0
- fast_agent/mcp/streamable_http_tracking.py +309 -0
- fast_agent/mcp/tool_execution_handler.py +137 -0
- fast_agent/mcp/tool_permission_handler.py +88 -0
- fast_agent/mcp/transport_tracking.py +634 -0
- fast_agent/mcp/types.py +24 -0
- fast_agent/mcp/ui_agent.py +48 -0
- fast_agent/mcp/ui_mixin.py +209 -0
- fast_agent/mcp_server_registry.py +89 -0
- fast_agent/py.typed +0 -0
- fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
- fast_agent/resources/examples/data-analysis/analysis.py +68 -0
- fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
- fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
- fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
- fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
- fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
- fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
- fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
- fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
- fast_agent/resources/examples/researcher/researcher.py +36 -0
- fast_agent/resources/examples/tensorzero/.env.sample +2 -0
- fast_agent/resources/examples/tensorzero/Makefile +31 -0
- fast_agent/resources/examples/tensorzero/README.md +56 -0
- fast_agent/resources/examples/tensorzero/agent.py +35 -0
- fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
- fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
- fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
- fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
- fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
- fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
- fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
- fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
- fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
- fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
- fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
- fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
- fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
- fast_agent/resources/examples/workflows/chaining.py +37 -0
- fast_agent/resources/examples/workflows/evaluator.py +77 -0
- fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
- fast_agent/resources/examples/workflows/graded_report.md +89 -0
- fast_agent/resources/examples/workflows/human_input.py +28 -0
- fast_agent/resources/examples/workflows/maker.py +156 -0
- fast_agent/resources/examples/workflows/orchestrator.py +70 -0
- fast_agent/resources/examples/workflows/parallel.py +56 -0
- fast_agent/resources/examples/workflows/router.py +69 -0
- fast_agent/resources/examples/workflows/short_story.md +13 -0
- fast_agent/resources/examples/workflows/short_story.txt +19 -0
- fast_agent/resources/setup/.gitignore +30 -0
- fast_agent/resources/setup/agent.py +28 -0
- fast_agent/resources/setup/fastagent.config.yaml +65 -0
- fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
- fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
- fast_agent/skills/__init__.py +9 -0
- fast_agent/skills/registry.py +235 -0
- fast_agent/tools/elicitation.py +369 -0
- fast_agent/tools/shell_runtime.py +402 -0
- fast_agent/types/__init__.py +59 -0
- fast_agent/types/conversation_summary.py +294 -0
- fast_agent/types/llm_stop_reason.py +78 -0
- fast_agent/types/message_search.py +249 -0
- fast_agent/ui/__init__.py +38 -0
- fast_agent/ui/console.py +59 -0
- fast_agent/ui/console_display.py +1080 -0
- fast_agent/ui/elicitation_form.py +946 -0
- fast_agent/ui/elicitation_style.py +59 -0
- fast_agent/ui/enhanced_prompt.py +1400 -0
- fast_agent/ui/history_display.py +734 -0
- fast_agent/ui/interactive_prompt.py +1199 -0
- fast_agent/ui/markdown_helpers.py +104 -0
- fast_agent/ui/markdown_truncator.py +1004 -0
- fast_agent/ui/mcp_display.py +857 -0
- fast_agent/ui/mcp_ui_utils.py +235 -0
- fast_agent/ui/mermaid_utils.py +169 -0
- fast_agent/ui/message_primitives.py +50 -0
- fast_agent/ui/notification_tracker.py +205 -0
- fast_agent/ui/plain_text_truncator.py +68 -0
- fast_agent/ui/progress_display.py +10 -0
- fast_agent/ui/rich_progress.py +195 -0
- fast_agent/ui/streaming.py +774 -0
- fast_agent/ui/streaming_buffer.py +449 -0
- fast_agent/ui/tool_display.py +422 -0
- fast_agent/ui/usage_display.py +204 -0
- fast_agent/utils/__init__.py +5 -0
- fast_agent/utils/reasoning_stream_parser.py +77 -0
- fast_agent/utils/time.py +22 -0
- fast_agent/workflow_telemetry.py +261 -0
- fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
- fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
- fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
- fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
- fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
"""Display helpers for agent conversation history."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from shutil import get_terminal_size
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from rich import print as rich_print
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from fast_agent.constants import FAST_AGENT_TIMING, FAST_AGENT_TOOL_TIMING
|
|
14
|
+
from fast_agent.mcp.helpers.content_helpers import get_text
|
|
15
|
+
from fast_agent.types.conversation_summary import ConversationSummary
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
|
|
20
|
+
from fast_agent.llm.usage_tracking import UsageAccumulator
|
|
21
|
+
from fast_agent.types import PromptMessageExtended
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
NON_TEXT_MARKER = "^"
|
|
25
|
+
TIMELINE_WIDTH = 20
|
|
26
|
+
SUMMARY_COUNT = 8
|
|
27
|
+
ROLE_COLUMN_WIDTH = 17
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _normalize_text(value: str | None) -> str:
|
|
31
|
+
return "" if not value else " ".join(value.split())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Colours:
|
|
35
|
+
"""Central colour palette for history display output."""
|
|
36
|
+
|
|
37
|
+
USER = "blue"
|
|
38
|
+
ASSISTANT = "green"
|
|
39
|
+
TOOL = "magenta"
|
|
40
|
+
TOOL_ERROR = "red"
|
|
41
|
+
HEADER = USER
|
|
42
|
+
TIMELINE_EMPTY = "dim default"
|
|
43
|
+
CONTEXT_SAFE = "green"
|
|
44
|
+
CONTEXT_CAUTION = "yellow"
|
|
45
|
+
CONTEXT_ALERT = "bright_red"
|
|
46
|
+
TOOL_DETAIL = "dim magenta"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _char_count(value: str | None) -> int:
|
|
50
|
+
return len(_normalize_text(value))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _format_tool_detail(prefix: str, names: Sequence[str]) -> Text:
|
|
54
|
+
detail = Text(prefix, style=Colours.TOOL_DETAIL)
|
|
55
|
+
if names:
|
|
56
|
+
detail.append(", ".join(names), style=Colours.TOOL_DETAIL)
|
|
57
|
+
return detail
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _ensure_text(value: object | None) -> Text:
|
|
61
|
+
"""Coerce various value types into a Rich Text instance."""
|
|
62
|
+
|
|
63
|
+
if isinstance(value, Text):
|
|
64
|
+
return value.copy()
|
|
65
|
+
if value is None:
|
|
66
|
+
return Text("")
|
|
67
|
+
if isinstance(value, str):
|
|
68
|
+
return Text(value)
|
|
69
|
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, Text)):
|
|
70
|
+
return Text(", ".join(str(item) for item in value if item))
|
|
71
|
+
return Text(str(value))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _truncate_text_segment(segment: Text, width: int) -> Text:
|
|
75
|
+
if width <= 0 or segment.cell_len == 0:
|
|
76
|
+
return Text("")
|
|
77
|
+
if segment.cell_len <= width:
|
|
78
|
+
return segment.copy()
|
|
79
|
+
truncated = segment.copy()
|
|
80
|
+
truncated.truncate(width, overflow="ellipsis")
|
|
81
|
+
return truncated
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _compose_summary_text(
|
|
85
|
+
preview: Text,
|
|
86
|
+
detail: Text | None,
|
|
87
|
+
*,
|
|
88
|
+
include_non_text: bool,
|
|
89
|
+
max_width: int | None,
|
|
90
|
+
) -> Text:
|
|
91
|
+
marker_component = Text()
|
|
92
|
+
if include_non_text:
|
|
93
|
+
marker_component.append(" ")
|
|
94
|
+
marker_component.append(NON_TEXT_MARKER, style="dim")
|
|
95
|
+
|
|
96
|
+
if max_width is None:
|
|
97
|
+
combined = Text()
|
|
98
|
+
combined.append_text(preview)
|
|
99
|
+
if detail and detail.cell_len > 0:
|
|
100
|
+
if combined.cell_len > 0:
|
|
101
|
+
combined.append(" ")
|
|
102
|
+
combined.append_text(detail)
|
|
103
|
+
combined.append_text(marker_component)
|
|
104
|
+
return combined
|
|
105
|
+
|
|
106
|
+
width_available = max_width
|
|
107
|
+
if width_available <= 0:
|
|
108
|
+
return Text("")
|
|
109
|
+
|
|
110
|
+
if marker_component.cell_len > width_available:
|
|
111
|
+
marker_component = Text("")
|
|
112
|
+
marker_width = marker_component.cell_len
|
|
113
|
+
width_after_marker = max(0, width_available - marker_width)
|
|
114
|
+
|
|
115
|
+
preview_len = preview.cell_len
|
|
116
|
+
detail_component = detail.copy() if detail else Text("")
|
|
117
|
+
detail_len = detail_component.cell_len
|
|
118
|
+
detail_plain = detail_component.plain
|
|
119
|
+
|
|
120
|
+
preview_allow = min(preview_len, width_after_marker)
|
|
121
|
+
detail_allow = 0
|
|
122
|
+
if detail_len > 0 and width_after_marker > 0:
|
|
123
|
+
detail_allow = min(detail_len, max(0, width_after_marker - preview_allow))
|
|
124
|
+
|
|
125
|
+
if width_after_marker > 0:
|
|
126
|
+
min_detail_allow = 1
|
|
127
|
+
for prefix in ("tool→", "result→"):
|
|
128
|
+
if detail_plain.startswith(prefix):
|
|
129
|
+
min_detail_allow = min(detail_len, len(prefix))
|
|
130
|
+
break
|
|
131
|
+
else:
|
|
132
|
+
min_detail_allow = 0
|
|
133
|
+
if detail_allow < min_detail_allow:
|
|
134
|
+
needed = min_detail_allow - detail_allow
|
|
135
|
+
reduction = min(preview_allow, needed)
|
|
136
|
+
preview_allow -= reduction
|
|
137
|
+
detail_allow += reduction
|
|
138
|
+
|
|
139
|
+
preview_allow = max(0, preview_allow)
|
|
140
|
+
detail_allow = max(0, min(detail_allow, detail_len))
|
|
141
|
+
|
|
142
|
+
space = 1 if preview_allow > 0 and detail_allow > 0 else 0
|
|
143
|
+
total = preview_allow + detail_allow + space
|
|
144
|
+
if total > width_after_marker:
|
|
145
|
+
overflow = total - width_after_marker
|
|
146
|
+
reduction = min(preview_allow, overflow)
|
|
147
|
+
preview_allow -= reduction
|
|
148
|
+
overflow -= reduction
|
|
149
|
+
if overflow > 0:
|
|
150
|
+
detail_allow = max(0, detail_allow - overflow)
|
|
151
|
+
|
|
152
|
+
preview_allow = max(0, preview_allow)
|
|
153
|
+
detail_allow = max(0, min(detail_allow, detail_len))
|
|
154
|
+
else:
|
|
155
|
+
preview_allow = min(preview_len, width_after_marker)
|
|
156
|
+
detail_allow = 0
|
|
157
|
+
|
|
158
|
+
preview_segment = _truncate_text_segment(preview, preview_allow)
|
|
159
|
+
detail_segment = (
|
|
160
|
+
_truncate_text_segment(detail_component, detail_allow) if detail_allow > 0 else Text("")
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
combined = Text()
|
|
164
|
+
combined.append_text(preview_segment)
|
|
165
|
+
if preview_segment.cell_len > 0 and detail_segment.cell_len > 0:
|
|
166
|
+
combined.append(" ")
|
|
167
|
+
combined.append_text(detail_segment)
|
|
168
|
+
|
|
169
|
+
if marker_component.cell_len > 0:
|
|
170
|
+
if combined.cell_len + marker_component.cell_len <= max_width:
|
|
171
|
+
combined.append_text(marker_component)
|
|
172
|
+
|
|
173
|
+
return combined
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _preview_text(value: str | None, limit: int = 80) -> str:
|
|
177
|
+
normalized = _normalize_text(value)
|
|
178
|
+
if not normalized:
|
|
179
|
+
return "<no text>"
|
|
180
|
+
if len(normalized) <= limit:
|
|
181
|
+
return normalized
|
|
182
|
+
return normalized[: limit - 1] + "…"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _has_non_text_content(message: PromptMessageExtended) -> bool:
|
|
186
|
+
for block in getattr(message, "content", []) or []:
|
|
187
|
+
block_type = getattr(block, "type", None)
|
|
188
|
+
if block_type and block_type != "text":
|
|
189
|
+
return True
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _extract_tool_result_summary(result, *, limit: int = 80) -> tuple[str, int, bool]:
|
|
194
|
+
preview: str | None = None
|
|
195
|
+
total_chars = 0
|
|
196
|
+
saw_non_text = False
|
|
197
|
+
|
|
198
|
+
for block in getattr(result, "content", []) or []:
|
|
199
|
+
text = get_text(block)
|
|
200
|
+
if text:
|
|
201
|
+
normalized = _normalize_text(text)
|
|
202
|
+
if preview is None:
|
|
203
|
+
preview = _preview_text(normalized, limit=limit)
|
|
204
|
+
total_chars += len(normalized)
|
|
205
|
+
else:
|
|
206
|
+
saw_non_text = True
|
|
207
|
+
|
|
208
|
+
if preview is not None:
|
|
209
|
+
return preview, total_chars, saw_non_text
|
|
210
|
+
return f"{NON_TEXT_MARKER} non-text tool result", 0, True
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def format_chars(value: int) -> str:
|
|
214
|
+
if value <= 0:
|
|
215
|
+
return "—"
|
|
216
|
+
if value >= 1_000_000:
|
|
217
|
+
return f"{value / 1_000_000:.1f}M"
|
|
218
|
+
if value >= 10_000:
|
|
219
|
+
return f"{value / 1_000:.1f}k"
|
|
220
|
+
return str(value)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _extract_timing_ms(message: PromptMessageExtended) -> float | None:
|
|
224
|
+
"""Extract timing duration in milliseconds from message channels."""
|
|
225
|
+
channels = getattr(message, "channels", None)
|
|
226
|
+
if not channels:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
timing_blocks = channels.get(FAST_AGENT_TIMING, [])
|
|
230
|
+
if not timing_blocks:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
timing_text = get_text(timing_blocks[0])
|
|
234
|
+
if not timing_text:
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
timing_data = json.loads(timing_text)
|
|
239
|
+
return timing_data.get("duration_ms")
|
|
240
|
+
except (json.JSONDecodeError, AttributeError, KeyError):
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _extract_tool_timings(message: PromptMessageExtended) -> dict[str, dict[str, float | str | None]]:
|
|
245
|
+
"""Extract tool timing data from message channels.
|
|
246
|
+
|
|
247
|
+
Returns a dict mapping tool_id to timing info:
|
|
248
|
+
{
|
|
249
|
+
"tool_id": {
|
|
250
|
+
"timing_ms": 123.45,
|
|
251
|
+
"transport_channel": "post-sse"
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
Handles backward compatibility with old format where values were just floats.
|
|
256
|
+
"""
|
|
257
|
+
channels = getattr(message, "channels", None)
|
|
258
|
+
if not channels:
|
|
259
|
+
return {}
|
|
260
|
+
|
|
261
|
+
timing_blocks = channels.get(FAST_AGENT_TOOL_TIMING, [])
|
|
262
|
+
if not timing_blocks:
|
|
263
|
+
return {}
|
|
264
|
+
|
|
265
|
+
timing_text = get_text(timing_blocks[0])
|
|
266
|
+
if not timing_text:
|
|
267
|
+
return {}
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
raw_data = json.loads(timing_text)
|
|
271
|
+
# Normalize to new format for backward compatibility
|
|
272
|
+
normalized = {}
|
|
273
|
+
for tool_id, value in raw_data.items():
|
|
274
|
+
if isinstance(value, dict):
|
|
275
|
+
# New format - already has timing_ms and transport_channel
|
|
276
|
+
normalized[tool_id] = value
|
|
277
|
+
else:
|
|
278
|
+
# Old format - value is just a float (timing in ms)
|
|
279
|
+
normalized[tool_id] = {
|
|
280
|
+
"timing_ms": value,
|
|
281
|
+
"transport_channel": None
|
|
282
|
+
}
|
|
283
|
+
return normalized
|
|
284
|
+
except (json.JSONDecodeError, TypeError):
|
|
285
|
+
return {}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def format_time(value: float | None) -> str:
|
|
289
|
+
"""Format timing value for display."""
|
|
290
|
+
if value is None:
|
|
291
|
+
return "-"
|
|
292
|
+
if value < 1000:
|
|
293
|
+
return f"{value:.0f}ms"
|
|
294
|
+
return f"{value / 1000:.1f}s"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _build_history_rows(history: Sequence[PromptMessageExtended]) -> list[dict]:
|
|
298
|
+
rows: list[dict] = []
|
|
299
|
+
call_name_lookup: dict[str, str] = {}
|
|
300
|
+
|
|
301
|
+
for message in history:
|
|
302
|
+
role_raw = getattr(message, "role", "assistant")
|
|
303
|
+
role_value = getattr(role_raw, "value", role_raw)
|
|
304
|
+
role = str(role_value).lower() if role_value else "assistant"
|
|
305
|
+
|
|
306
|
+
text = ""
|
|
307
|
+
if hasattr(message, "first_text"):
|
|
308
|
+
try:
|
|
309
|
+
text = message.first_text() or ""
|
|
310
|
+
except Exception: # pragma: no cover - defensive
|
|
311
|
+
text = ""
|
|
312
|
+
normalized_text = _normalize_text(text)
|
|
313
|
+
chars = len(normalized_text)
|
|
314
|
+
preview = _preview_text(text)
|
|
315
|
+
non_text = _has_non_text_content(message) or chars == 0
|
|
316
|
+
|
|
317
|
+
# Extract timing data
|
|
318
|
+
timing_ms = _extract_timing_ms(message)
|
|
319
|
+
tool_timings = _extract_tool_timings(message)
|
|
320
|
+
|
|
321
|
+
tool_calls: Mapping[str, object] | None = getattr(message, "tool_calls", None)
|
|
322
|
+
tool_results: Mapping[str, object] | None = getattr(message, "tool_results", None)
|
|
323
|
+
|
|
324
|
+
detail_sections: list[Text] = []
|
|
325
|
+
row_non_text = non_text
|
|
326
|
+
has_tool_request = False
|
|
327
|
+
hide_in_summary = False
|
|
328
|
+
timeline_role = role
|
|
329
|
+
include_in_timeline = True
|
|
330
|
+
result_rows: list[dict] = []
|
|
331
|
+
tool_result_total_chars = 0
|
|
332
|
+
tool_result_has_non_text = False
|
|
333
|
+
tool_result_has_error = False
|
|
334
|
+
|
|
335
|
+
if tool_calls:
|
|
336
|
+
names: list[str] = []
|
|
337
|
+
for call_id, call in tool_calls.items():
|
|
338
|
+
params = getattr(call, "params", None)
|
|
339
|
+
name = getattr(params, "name", None) or getattr(call, "name", None) or call_id
|
|
340
|
+
call_name_lookup[call_id] = name
|
|
341
|
+
names.append(name)
|
|
342
|
+
if names:
|
|
343
|
+
detail_sections.append(_format_tool_detail("tool→", names))
|
|
344
|
+
row_non_text = row_non_text and chars == 0 # treat call as activity
|
|
345
|
+
has_tool_request = True
|
|
346
|
+
if not normalized_text and tool_calls:
|
|
347
|
+
preview = "(issuing tool request)"
|
|
348
|
+
|
|
349
|
+
if tool_results:
|
|
350
|
+
result_names: list[str] = []
|
|
351
|
+
for call_id, result in tool_results.items():
|
|
352
|
+
tool_name = call_name_lookup.get(call_id, call_id)
|
|
353
|
+
result_names.append(tool_name)
|
|
354
|
+
summary, result_chars, result_non_text = _extract_tool_result_summary(result)
|
|
355
|
+
tool_result_total_chars += result_chars
|
|
356
|
+
tool_result_has_non_text = tool_result_has_non_text or result_non_text
|
|
357
|
+
detail = _format_tool_detail("result→", [tool_name])
|
|
358
|
+
is_error = getattr(result, "isError", False)
|
|
359
|
+
tool_result_has_error = tool_result_has_error or is_error
|
|
360
|
+
# Get timing info for this specific tool call
|
|
361
|
+
tool_timing_info = tool_timings.get(call_id)
|
|
362
|
+
timing_ms = tool_timing_info.get("timing_ms") if tool_timing_info else None
|
|
363
|
+
transport_channel = tool_timing_info.get("transport_channel") if tool_timing_info else None
|
|
364
|
+
result_rows.append(
|
|
365
|
+
{
|
|
366
|
+
"role": "tool",
|
|
367
|
+
"timeline_role": "tool",
|
|
368
|
+
"chars": result_chars,
|
|
369
|
+
"preview": summary,
|
|
370
|
+
"details": detail,
|
|
371
|
+
"non_text": result_non_text,
|
|
372
|
+
"has_tool_request": False,
|
|
373
|
+
"hide_summary": False,
|
|
374
|
+
"include_in_timeline": False,
|
|
375
|
+
"is_error": is_error,
|
|
376
|
+
"timing_ms": timing_ms,
|
|
377
|
+
"transport_channel": transport_channel,
|
|
378
|
+
}
|
|
379
|
+
)
|
|
380
|
+
if role == "user":
|
|
381
|
+
timeline_role = "tool"
|
|
382
|
+
hide_in_summary = True
|
|
383
|
+
if result_names:
|
|
384
|
+
detail_sections.append(_format_tool_detail("result→", result_names))
|
|
385
|
+
|
|
386
|
+
if detail_sections:
|
|
387
|
+
if len(detail_sections) == 1:
|
|
388
|
+
details: Text | None = detail_sections[0]
|
|
389
|
+
else:
|
|
390
|
+
details = Text()
|
|
391
|
+
for index, section in enumerate(detail_sections):
|
|
392
|
+
if index > 0:
|
|
393
|
+
details.append(" ")
|
|
394
|
+
details.append_text(section)
|
|
395
|
+
else:
|
|
396
|
+
details = None
|
|
397
|
+
|
|
398
|
+
row_chars = chars
|
|
399
|
+
if timeline_role == "tool" and tool_result_total_chars > 0:
|
|
400
|
+
row_chars = tool_result_total_chars
|
|
401
|
+
row_non_text = row_non_text or tool_result_has_non_text
|
|
402
|
+
row_is_error = tool_result_has_error
|
|
403
|
+
|
|
404
|
+
rows.append(
|
|
405
|
+
{
|
|
406
|
+
"role": role,
|
|
407
|
+
"timeline_role": timeline_role,
|
|
408
|
+
"chars": row_chars,
|
|
409
|
+
"preview": preview,
|
|
410
|
+
"details": details,
|
|
411
|
+
"non_text": row_non_text,
|
|
412
|
+
"has_tool_request": has_tool_request,
|
|
413
|
+
"hide_summary": hide_in_summary,
|
|
414
|
+
"include_in_timeline": include_in_timeline,
|
|
415
|
+
"is_error": row_is_error,
|
|
416
|
+
"timing_ms": timing_ms,
|
|
417
|
+
}
|
|
418
|
+
)
|
|
419
|
+
rows.extend(result_rows)
|
|
420
|
+
|
|
421
|
+
return rows
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _aggregate_timeline_entries(rows: Sequence[dict]) -> list[dict]:
|
|
425
|
+
return [
|
|
426
|
+
{
|
|
427
|
+
"role": row.get("timeline_role", row["role"]),
|
|
428
|
+
"chars": row["chars"],
|
|
429
|
+
"non_text": row["non_text"],
|
|
430
|
+
"is_error": row.get("is_error", False),
|
|
431
|
+
}
|
|
432
|
+
for row in rows
|
|
433
|
+
if row.get("include_in_timeline", True)
|
|
434
|
+
]
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _get_role_color(role: str, *, is_error: bool = False) -> str:
|
|
438
|
+
"""Get the display color for a role, accounting for error states."""
|
|
439
|
+
color_map = {"user": Colours.USER, "assistant": Colours.ASSISTANT, "tool": Colours.TOOL}
|
|
440
|
+
|
|
441
|
+
if role == "tool" and is_error:
|
|
442
|
+
return Colours.TOOL_ERROR
|
|
443
|
+
|
|
444
|
+
return color_map.get(role, "white")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _shade_block(chars: int, *, non_text: bool, color: str) -> Text:
|
|
448
|
+
if non_text:
|
|
449
|
+
return Text(NON_TEXT_MARKER, style=f"bold {color}")
|
|
450
|
+
if chars <= 0:
|
|
451
|
+
return Text("·", style="dim")
|
|
452
|
+
if chars < 50:
|
|
453
|
+
return Text("░", style=f"dim {color}")
|
|
454
|
+
if chars < 200:
|
|
455
|
+
return Text("▒", style=f"dim {color}")
|
|
456
|
+
if chars < 500:
|
|
457
|
+
return Text("▒", style=color)
|
|
458
|
+
if chars < 2000:
|
|
459
|
+
return Text("▓", style=color)
|
|
460
|
+
return Text("█", style=f"bold {color}")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _build_history_bar(entries: Sequence[dict], width: int = TIMELINE_WIDTH) -> tuple[Text, Text]:
|
|
464
|
+
recent = list(entries[-width:])
|
|
465
|
+
bar = Text(" history |", style="dim")
|
|
466
|
+
for entry in recent:
|
|
467
|
+
color = _get_role_color(entry["role"], is_error=entry.get("is_error", False))
|
|
468
|
+
bar.append_text(
|
|
469
|
+
_shade_block(entry["chars"], non_text=entry.get("non_text", False), color=color)
|
|
470
|
+
)
|
|
471
|
+
remaining = width - len(recent)
|
|
472
|
+
if remaining > 0:
|
|
473
|
+
bar.append("░" * remaining, style=Colours.TIMELINE_EMPTY)
|
|
474
|
+
bar.append("|", style="dim")
|
|
475
|
+
|
|
476
|
+
detail = Text(f"{len(entries)} turns", style="dim")
|
|
477
|
+
return bar, detail
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _build_context_bar_line(
|
|
481
|
+
current: int,
|
|
482
|
+
window: int | None,
|
|
483
|
+
width: int = TIMELINE_WIDTH,
|
|
484
|
+
) -> tuple[Text, Text]:
|
|
485
|
+
bar = Text(" context |", style="dim")
|
|
486
|
+
|
|
487
|
+
if not window or window <= 0:
|
|
488
|
+
bar.append("░" * width, style=Colours.TIMELINE_EMPTY)
|
|
489
|
+
bar.append("|", style="dim")
|
|
490
|
+
detail = Text(f"{format_chars(current)} tokens (unknown window)", style="dim")
|
|
491
|
+
return bar, detail
|
|
492
|
+
|
|
493
|
+
percent = current / window if window else 0.0
|
|
494
|
+
filled = min(width, int(round(min(percent, 1.0) * width)))
|
|
495
|
+
|
|
496
|
+
def color_for(pct: float) -> str:
|
|
497
|
+
if pct >= 0.9:
|
|
498
|
+
return Colours.CONTEXT_ALERT
|
|
499
|
+
if pct >= 0.7:
|
|
500
|
+
return Colours.CONTEXT_CAUTION
|
|
501
|
+
return Colours.CONTEXT_SAFE
|
|
502
|
+
|
|
503
|
+
color = color_for(percent)
|
|
504
|
+
if filled > 0:
|
|
505
|
+
bar.append("█" * filled, style=color)
|
|
506
|
+
if filled < width:
|
|
507
|
+
bar.append("░" * (width - filled), style=Colours.TIMELINE_EMPTY)
|
|
508
|
+
bar.append("|", style="dim")
|
|
509
|
+
bar.append(f" {percent * 100:5.1f}%", style="dim")
|
|
510
|
+
if percent > 1.0:
|
|
511
|
+
bar.append(f" +{(percent - 1) * 100:.0f}%", style="bold bright_red")
|
|
512
|
+
|
|
513
|
+
detail = Text(f"{format_chars(current)} / {format_chars(window)} →", style="dim")
|
|
514
|
+
return bar, detail
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _render_header_line(agent_name: str, *, console: Console | None, printer) -> None:
|
|
518
|
+
header = Text()
|
|
519
|
+
header.append("▎", style=Colours.HEADER)
|
|
520
|
+
header.append("●", style=f"dim {Colours.HEADER}")
|
|
521
|
+
header.append(" [ 1] ", style=Colours.HEADER)
|
|
522
|
+
header.append(str(agent_name), style=f"bold {Colours.USER}")
|
|
523
|
+
|
|
524
|
+
line = Text()
|
|
525
|
+
line.append_text(header)
|
|
526
|
+
line.append(" ")
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
total_width = console.width if console else get_terminal_size().columns
|
|
530
|
+
except Exception:
|
|
531
|
+
total_width = 80
|
|
532
|
+
|
|
533
|
+
separator_width = max(1, total_width - line.cell_len)
|
|
534
|
+
line.append("─" * separator_width, style="dim")
|
|
535
|
+
|
|
536
|
+
printer("")
|
|
537
|
+
printer(line)
|
|
538
|
+
printer("")
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _render_statistics(
|
|
542
|
+
summary: ConversationSummary,
|
|
543
|
+
*,
|
|
544
|
+
console: Console | None,
|
|
545
|
+
printer,
|
|
546
|
+
) -> None:
|
|
547
|
+
"""Render compact conversation statistics section."""
|
|
548
|
+
|
|
549
|
+
# Format timing values
|
|
550
|
+
llm_time = (
|
|
551
|
+
format_time(summary.total_elapsed_time_ms) if summary.total_elapsed_time_ms > 0 else "-"
|
|
552
|
+
)
|
|
553
|
+
runtime = format_time(summary.conversation_span_ms) if summary.conversation_span_ms > 0 else "-"
|
|
554
|
+
|
|
555
|
+
# Build compact statistics lines
|
|
556
|
+
stats_lines = []
|
|
557
|
+
|
|
558
|
+
if summary.total_elapsed_time_ms > 0 or summary.conversation_span_ms > 0:
|
|
559
|
+
timing_line = Text(" ", style="dim")
|
|
560
|
+
timing_line.append("LLM Time: ", style="dim")
|
|
561
|
+
timing_line.append(llm_time, style="default")
|
|
562
|
+
timing_line.append(" • ", style="dim")
|
|
563
|
+
timing_line.append("Runtime: ", style="dim")
|
|
564
|
+
timing_line.append(runtime, style="default")
|
|
565
|
+
stats_lines.append(timing_line)
|
|
566
|
+
|
|
567
|
+
tool_counts = Text(" ", style="dim")
|
|
568
|
+
tool_counts.append("Tool Calls: ", style="dim")
|
|
569
|
+
tool_counts.append(str(summary.tool_calls), style="default")
|
|
570
|
+
if summary.tool_calls > 0:
|
|
571
|
+
tool_counts.append(
|
|
572
|
+
f" (successes: {summary.tool_successes}, errors: {summary.tool_errors})", style="dim"
|
|
573
|
+
)
|
|
574
|
+
stats_lines.append(tool_counts)
|
|
575
|
+
|
|
576
|
+
# Tool Usage Breakdown (if tools were used)
|
|
577
|
+
if summary.tool_calls > 0 and summary.tool_call_map:
|
|
578
|
+
# Get top tools sorted by count
|
|
579
|
+
sorted_tools = sorted(summary.tool_call_map.items(), key=lambda x: x[1], reverse=True)
|
|
580
|
+
|
|
581
|
+
# Show compact breakdown
|
|
582
|
+
tool_details = Text(" ", style="dim")
|
|
583
|
+
tool_details.append("Tools: ", style="dim")
|
|
584
|
+
|
|
585
|
+
tool_parts = []
|
|
586
|
+
for tool_name, count in sorted_tools[:5]: # Show max 5 tools
|
|
587
|
+
tool_parts.append(f"{tool_name} ({count})")
|
|
588
|
+
|
|
589
|
+
tool_details.append(", ".join(tool_parts), style=Colours.TOOL_DETAIL)
|
|
590
|
+
stats_lines.append(tool_details)
|
|
591
|
+
|
|
592
|
+
# Print all statistics lines
|
|
593
|
+
for line in stats_lines:
|
|
594
|
+
printer(line)
|
|
595
|
+
|
|
596
|
+
printer("")
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def display_history_overview(
|
|
600
|
+
agent_name: str,
|
|
601
|
+
history: Sequence[PromptMessageExtended],
|
|
602
|
+
usage_accumulator: "UsageAccumulator" | None = None,
|
|
603
|
+
*,
|
|
604
|
+
console: Console | None = None,
|
|
605
|
+
) -> None:
|
|
606
|
+
if not history:
|
|
607
|
+
printer = console.print if console else rich_print
|
|
608
|
+
printer("[dim]No conversation history yet[/dim]")
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
printer = console.print if console else rich_print
|
|
612
|
+
|
|
613
|
+
# Create conversation summary for statistics
|
|
614
|
+
summary = ConversationSummary(messages=list(history))
|
|
615
|
+
|
|
616
|
+
rows = _build_history_rows(history)
|
|
617
|
+
timeline_entries = _aggregate_timeline_entries(rows)
|
|
618
|
+
|
|
619
|
+
history_bar, history_detail = _build_history_bar(timeline_entries)
|
|
620
|
+
if usage_accumulator:
|
|
621
|
+
current_tokens = getattr(usage_accumulator, "current_context_tokens", 0)
|
|
622
|
+
window = getattr(usage_accumulator, "context_window_size", None)
|
|
623
|
+
else:
|
|
624
|
+
current_tokens = 0
|
|
625
|
+
window = None
|
|
626
|
+
context_bar, context_detail = _build_context_bar_line(current_tokens, window)
|
|
627
|
+
|
|
628
|
+
_render_header_line(agent_name, console=console, printer=printer)
|
|
629
|
+
|
|
630
|
+
# Render conversation statistics
|
|
631
|
+
_render_statistics(summary, console=console, printer=printer)
|
|
632
|
+
|
|
633
|
+
gap = Text(" ")
|
|
634
|
+
combined_line = Text()
|
|
635
|
+
combined_line.append_text(history_bar)
|
|
636
|
+
combined_line.append_text(gap)
|
|
637
|
+
combined_line.append_text(context_bar)
|
|
638
|
+
printer(combined_line)
|
|
639
|
+
|
|
640
|
+
history_label_len = len(" history |")
|
|
641
|
+
context_label_len = len(" context |")
|
|
642
|
+
|
|
643
|
+
history_available = history_bar.cell_len - history_label_len
|
|
644
|
+
context_available = context_bar.cell_len - context_label_len
|
|
645
|
+
|
|
646
|
+
detail_line = Text()
|
|
647
|
+
detail_line.append(" " * history_label_len, style="dim")
|
|
648
|
+
detail_line.append_text(history_detail)
|
|
649
|
+
if history_available > history_detail.cell_len:
|
|
650
|
+
detail_line.append(" " * (history_available - history_detail.cell_len), style="dim")
|
|
651
|
+
detail_line.append_text(gap)
|
|
652
|
+
detail_line.append(" " * context_label_len, style="dim")
|
|
653
|
+
detail_line.append_text(context_detail)
|
|
654
|
+
if context_available > context_detail.cell_len:
|
|
655
|
+
detail_line.append(" " * (context_available - context_detail.cell_len), style="dim")
|
|
656
|
+
printer(detail_line)
|
|
657
|
+
|
|
658
|
+
printer("")
|
|
659
|
+
printer(
|
|
660
|
+
Text(" " + "─" * (history_bar.cell_len + context_bar.cell_len + gap.cell_len), style="dim")
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
summary_candidates = [row for row in rows if not row.get("hide_summary")]
|
|
664
|
+
summary_rows = summary_candidates[-SUMMARY_COUNT:]
|
|
665
|
+
start_index = len(summary_candidates) - len(summary_rows) + 1
|
|
666
|
+
|
|
667
|
+
role_arrows = {"user": "▶", "assistant": "◀", "tool": "▶"}
|
|
668
|
+
role_labels = {"user": "user", "assistant": "assistant", "tool": "tool result"}
|
|
669
|
+
|
|
670
|
+
try:
|
|
671
|
+
total_width = console.width if console else get_terminal_size().columns
|
|
672
|
+
except Exception:
|
|
673
|
+
total_width = 80
|
|
674
|
+
|
|
675
|
+
# Responsive column layout based on terminal width
|
|
676
|
+
show_time = total_width >= 60
|
|
677
|
+
show_chars = total_width >= 50
|
|
678
|
+
|
|
679
|
+
header_line = Text(" ")
|
|
680
|
+
header_line.append(" #", style="dim")
|
|
681
|
+
header_line.append(" ", style="dim")
|
|
682
|
+
header_line.append(f" {'Role':<{ROLE_COLUMN_WIDTH}}", style="dim")
|
|
683
|
+
if show_time:
|
|
684
|
+
header_line.append(f" {'Time':>7}", style="dim")
|
|
685
|
+
if show_chars:
|
|
686
|
+
header_line.append(f" {'Chars':>7}", style="dim")
|
|
687
|
+
header_line.append(" ", style="dim")
|
|
688
|
+
header_line.append("Summary", style="dim")
|
|
689
|
+
printer(header_line)
|
|
690
|
+
|
|
691
|
+
for offset, row in enumerate(summary_rows):
|
|
692
|
+
role = row["role"]
|
|
693
|
+
color = _get_role_color(role, is_error=row.get("is_error", False))
|
|
694
|
+
arrow = role_arrows.get(role, "▶")
|
|
695
|
+
label = role_labels.get(role, role)
|
|
696
|
+
if role == "assistant" and row.get("has_tool_request"):
|
|
697
|
+
label = f"{label}*"
|
|
698
|
+
chars = row["chars"]
|
|
699
|
+
block = _shade_block(chars, non_text=row.get("non_text", False), color=color)
|
|
700
|
+
|
|
701
|
+
details = row.get("details")
|
|
702
|
+
preview_value = row["preview"]
|
|
703
|
+
preview_text = _ensure_text(preview_value)
|
|
704
|
+
detail_text = _ensure_text(details) if details else Text("")
|
|
705
|
+
if detail_text.cell_len == 0:
|
|
706
|
+
detail_text = None
|
|
707
|
+
|
|
708
|
+
timing_ms = row.get("timing_ms")
|
|
709
|
+
timing_str = format_time(timing_ms)
|
|
710
|
+
|
|
711
|
+
line = Text(" ")
|
|
712
|
+
line.append(f"{start_index + offset:>2}", style="dim")
|
|
713
|
+
line.append(" ")
|
|
714
|
+
line.append_text(block)
|
|
715
|
+
line.append(" ")
|
|
716
|
+
line.append(arrow, style=color)
|
|
717
|
+
line.append(" ")
|
|
718
|
+
line.append(f"{label:<{ROLE_COLUMN_WIDTH}}", style=color)
|
|
719
|
+
if show_time:
|
|
720
|
+
line.append(f" {timing_str:>7}", style="dim")
|
|
721
|
+
if show_chars:
|
|
722
|
+
line.append(f" {format_chars(chars):>7}", style="dim")
|
|
723
|
+
line.append(" ")
|
|
724
|
+
summary_width = max(0, total_width - line.cell_len)
|
|
725
|
+
summary_text = _compose_summary_text(
|
|
726
|
+
preview_text,
|
|
727
|
+
detail_text,
|
|
728
|
+
include_non_text=row.get("non_text", False),
|
|
729
|
+
max_width=summary_width,
|
|
730
|
+
)
|
|
731
|
+
line.append_text(summary_text)
|
|
732
|
+
printer(line)
|
|
733
|
+
|
|
734
|
+
printer("")
|