fast-agent-mcp 0.2.58__py3-none-any.whl → 0.3.0__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.

Files changed (233) hide show
  1. fast_agent/__init__.py +127 -0
  2. fast_agent/agents/__init__.py +36 -0
  3. {mcp_agent/core → fast_agent/agents}/agent_types.py +2 -1
  4. fast_agent/agents/llm_agent.py +217 -0
  5. fast_agent/agents/llm_decorator.py +486 -0
  6. mcp_agent/agents/base_agent.py → fast_agent/agents/mcp_agent.py +377 -385
  7. fast_agent/agents/tool_agent.py +168 -0
  8. {mcp_agent → fast_agent}/agents/workflow/chain_agent.py +43 -33
  9. {mcp_agent → fast_agent}/agents/workflow/evaluator_optimizer.py +31 -35
  10. {mcp_agent → fast_agent}/agents/workflow/iterative_planner.py +56 -47
  11. {mcp_agent → fast_agent}/agents/workflow/orchestrator_models.py +4 -4
  12. {mcp_agent → fast_agent}/agents/workflow/parallel_agent.py +34 -41
  13. {mcp_agent → fast_agent}/agents/workflow/router_agent.py +54 -39
  14. {mcp_agent → fast_agent}/cli/__main__.py +5 -3
  15. {mcp_agent → fast_agent}/cli/commands/check_config.py +95 -66
  16. {mcp_agent → fast_agent}/cli/commands/go.py +20 -11
  17. {mcp_agent → fast_agent}/cli/commands/quickstart.py +4 -4
  18. {mcp_agent → fast_agent}/cli/commands/server_helpers.py +1 -1
  19. {mcp_agent → fast_agent}/cli/commands/setup.py +64 -134
  20. {mcp_agent → fast_agent}/cli/commands/url_parser.py +9 -8
  21. {mcp_agent → fast_agent}/cli/main.py +36 -16
  22. {mcp_agent → fast_agent}/cli/terminal.py +2 -2
  23. {mcp_agent → fast_agent}/config.py +10 -2
  24. fast_agent/constants.py +8 -0
  25. {mcp_agent → fast_agent}/context.py +24 -19
  26. {mcp_agent → fast_agent}/context_dependent.py +9 -5
  27. fast_agent/core/__init__.py +17 -0
  28. {mcp_agent → fast_agent}/core/agent_app.py +39 -36
  29. fast_agent/core/core_app.py +135 -0
  30. {mcp_agent → fast_agent}/core/direct_decorators.py +12 -26
  31. {mcp_agent → fast_agent}/core/direct_factory.py +95 -73
  32. {mcp_agent → fast_agent/core}/executor/executor.py +4 -5
  33. {mcp_agent → fast_agent}/core/fastagent.py +32 -32
  34. fast_agent/core/logging/__init__.py +5 -0
  35. {mcp_agent → fast_agent/core}/logging/events.py +3 -3
  36. {mcp_agent → fast_agent/core}/logging/json_serializer.py +1 -1
  37. {mcp_agent → fast_agent/core}/logging/listeners.py +85 -7
  38. {mcp_agent → fast_agent/core}/logging/logger.py +7 -7
  39. {mcp_agent → fast_agent/core}/logging/transport.py +10 -11
  40. fast_agent/core/prompt.py +9 -0
  41. {mcp_agent → fast_agent}/core/validation.py +4 -4
  42. fast_agent/event_progress.py +61 -0
  43. fast_agent/history/history_exporter.py +44 -0
  44. {mcp_agent → fast_agent}/human_input/__init__.py +9 -12
  45. {mcp_agent → fast_agent}/human_input/elicitation_handler.py +26 -8
  46. {mcp_agent → fast_agent}/human_input/elicitation_state.py +7 -7
  47. {mcp_agent → fast_agent}/human_input/simple_form.py +6 -4
  48. {mcp_agent → fast_agent}/human_input/types.py +1 -18
  49. fast_agent/interfaces.py +228 -0
  50. fast_agent/llm/__init__.py +9 -0
  51. mcp_agent/llm/augmented_llm.py → fast_agent/llm/fastagent_llm.py +127 -218
  52. fast_agent/llm/internal/passthrough.py +137 -0
  53. mcp_agent/llm/augmented_llm_playback.py → fast_agent/llm/internal/playback.py +29 -25
  54. mcp_agent/llm/augmented_llm_silent.py → fast_agent/llm/internal/silent.py +10 -17
  55. fast_agent/llm/internal/slow.py +38 -0
  56. {mcp_agent → fast_agent}/llm/memory.py +40 -30
  57. {mcp_agent → fast_agent}/llm/model_database.py +35 -2
  58. {mcp_agent → fast_agent}/llm/model_factory.py +103 -77
  59. fast_agent/llm/model_info.py +126 -0
  60. {mcp_agent/llm/providers → fast_agent/llm/provider/anthropic}/anthropic_utils.py +7 -7
  61. fast_agent/llm/provider/anthropic/llm_anthropic.py +603 -0
  62. {mcp_agent/llm/providers → fast_agent/llm/provider/anthropic}/multipart_converter_anthropic.py +79 -86
  63. {mcp_agent/llm/providers → fast_agent/llm/provider/bedrock}/bedrock_utils.py +3 -1
  64. mcp_agent/llm/providers/augmented_llm_bedrock.py → fast_agent/llm/provider/bedrock/llm_bedrock.py +833 -717
  65. {mcp_agent/llm/providers → fast_agent/llm/provider/google}/google_converter.py +66 -14
  66. fast_agent/llm/provider/google/llm_google_native.py +431 -0
  67. mcp_agent/llm/providers/augmented_llm_aliyun.py → fast_agent/llm/provider/openai/llm_aliyun.py +6 -7
  68. mcp_agent/llm/providers/augmented_llm_azure.py → fast_agent/llm/provider/openai/llm_azure.py +4 -4
  69. mcp_agent/llm/providers/augmented_llm_deepseek.py → fast_agent/llm/provider/openai/llm_deepseek.py +10 -11
  70. mcp_agent/llm/providers/augmented_llm_generic.py → fast_agent/llm/provider/openai/llm_generic.py +4 -4
  71. mcp_agent/llm/providers/augmented_llm_google_oai.py → fast_agent/llm/provider/openai/llm_google_oai.py +4 -4
  72. mcp_agent/llm/providers/augmented_llm_groq.py → fast_agent/llm/provider/openai/llm_groq.py +14 -16
  73. mcp_agent/llm/providers/augmented_llm_openai.py → fast_agent/llm/provider/openai/llm_openai.py +133 -207
  74. mcp_agent/llm/providers/augmented_llm_openrouter.py → fast_agent/llm/provider/openai/llm_openrouter.py +6 -6
  75. mcp_agent/llm/providers/augmented_llm_tensorzero_openai.py → fast_agent/llm/provider/openai/llm_tensorzero_openai.py +17 -16
  76. mcp_agent/llm/providers/augmented_llm_xai.py → fast_agent/llm/provider/openai/llm_xai.py +6 -6
  77. {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/multipart_converter_openai.py +125 -63
  78. {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/openai_multipart.py +12 -12
  79. {mcp_agent/llm/providers → fast_agent/llm/provider/openai}/openai_utils.py +18 -16
  80. {mcp_agent → fast_agent}/llm/provider_key_manager.py +2 -2
  81. {mcp_agent → fast_agent}/llm/provider_types.py +2 -0
  82. {mcp_agent → fast_agent}/llm/sampling_converter.py +15 -12
  83. {mcp_agent → fast_agent}/llm/usage_tracking.py +23 -5
  84. fast_agent/mcp/__init__.py +43 -0
  85. {mcp_agent → fast_agent}/mcp/elicitation_factory.py +3 -3
  86. {mcp_agent → fast_agent}/mcp/elicitation_handlers.py +19 -10
  87. {mcp_agent → fast_agent}/mcp/gen_client.py +3 -3
  88. fast_agent/mcp/helpers/__init__.py +36 -0
  89. fast_agent/mcp/helpers/content_helpers.py +183 -0
  90. {mcp_agent → fast_agent}/mcp/helpers/server_config_helpers.py +8 -8
  91. {mcp_agent → fast_agent}/mcp/hf_auth.py +25 -23
  92. fast_agent/mcp/interfaces.py +93 -0
  93. {mcp_agent → fast_agent}/mcp/logger_textio.py +4 -4
  94. {mcp_agent → fast_agent}/mcp/mcp_agent_client_session.py +49 -44
  95. {mcp_agent → fast_agent}/mcp/mcp_aggregator.py +66 -115
  96. {mcp_agent → fast_agent}/mcp/mcp_connection_manager.py +16 -23
  97. {mcp_agent/core → fast_agent/mcp}/mcp_content.py +23 -15
  98. {mcp_agent → fast_agent}/mcp/mime_utils.py +39 -0
  99. fast_agent/mcp/prompt.py +159 -0
  100. mcp_agent/mcp/prompt_message_multipart.py → fast_agent/mcp/prompt_message_extended.py +27 -20
  101. {mcp_agent → fast_agent}/mcp/prompt_render.py +21 -19
  102. {mcp_agent → fast_agent}/mcp/prompt_serialization.py +46 -46
  103. fast_agent/mcp/prompts/__main__.py +7 -0
  104. {mcp_agent → fast_agent}/mcp/prompts/prompt_helpers.py +31 -30
  105. {mcp_agent → fast_agent}/mcp/prompts/prompt_load.py +8 -8
  106. {mcp_agent → fast_agent}/mcp/prompts/prompt_server.py +11 -19
  107. {mcp_agent → fast_agent}/mcp/prompts/prompt_template.py +18 -18
  108. {mcp_agent → fast_agent}/mcp/resource_utils.py +1 -1
  109. {mcp_agent → fast_agent}/mcp/sampling.py +31 -26
  110. {mcp_agent/mcp_server → fast_agent/mcp/server}/__init__.py +1 -1
  111. {mcp_agent/mcp_server → fast_agent/mcp/server}/agent_server.py +5 -6
  112. fast_agent/mcp/ui_agent.py +48 -0
  113. fast_agent/mcp/ui_mixin.py +209 -0
  114. fast_agent/mcp_server_registry.py +90 -0
  115. {mcp_agent → fast_agent}/resources/examples/data-analysis/analysis-campaign.py +5 -4
  116. {mcp_agent → fast_agent}/resources/examples/data-analysis/analysis.py +1 -1
  117. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/forms_demo.py +3 -3
  118. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/game_character.py +2 -2
  119. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/game_character_handler.py +1 -1
  120. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/tool_call.py +1 -1
  121. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/agent_one.py +1 -1
  122. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/agent_two.py +1 -1
  123. {mcp_agent → fast_agent}/resources/examples/researcher/researcher-eval.py +1 -1
  124. {mcp_agent → fast_agent}/resources/examples/researcher/researcher-imp.py +1 -1
  125. {mcp_agent → fast_agent}/resources/examples/researcher/researcher.py +1 -1
  126. {mcp_agent → fast_agent}/resources/examples/tensorzero/agent.py +2 -2
  127. {mcp_agent → fast_agent}/resources/examples/tensorzero/image_demo.py +3 -3
  128. {mcp_agent → fast_agent}/resources/examples/tensorzero/simple_agent.py +1 -1
  129. {mcp_agent → fast_agent}/resources/examples/workflows/chaining.py +1 -1
  130. {mcp_agent → fast_agent}/resources/examples/workflows/evaluator.py +3 -3
  131. {mcp_agent → fast_agent}/resources/examples/workflows/human_input.py +5 -3
  132. {mcp_agent → fast_agent}/resources/examples/workflows/orchestrator.py +1 -1
  133. {mcp_agent → fast_agent}/resources/examples/workflows/parallel.py +2 -2
  134. {mcp_agent → fast_agent}/resources/examples/workflows/router.py +5 -2
  135. fast_agent/resources/setup/.gitignore +24 -0
  136. fast_agent/resources/setup/agent.py +18 -0
  137. fast_agent/resources/setup/fastagent.config.yaml +44 -0
  138. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  139. fast_agent/tools/elicitation.py +369 -0
  140. fast_agent/types/__init__.py +32 -0
  141. fast_agent/types/llm_stop_reason.py +77 -0
  142. fast_agent/ui/__init__.py +38 -0
  143. fast_agent/ui/console_display.py +1005 -0
  144. {mcp_agent/human_input → fast_agent/ui}/elicitation_form.py +17 -12
  145. mcp_agent/human_input/elicitation_forms.py → fast_agent/ui/elicitation_style.py +1 -1
  146. {mcp_agent/core → fast_agent/ui}/enhanced_prompt.py +96 -25
  147. {mcp_agent/core → fast_agent/ui}/interactive_prompt.py +330 -125
  148. fast_agent/ui/mcp_ui_utils.py +224 -0
  149. {mcp_agent → fast_agent/ui}/progress_display.py +2 -2
  150. {mcp_agent/logging → fast_agent/ui}/rich_progress.py +4 -4
  151. {mcp_agent/core → fast_agent/ui}/usage_display.py +3 -8
  152. {fast_agent_mcp-0.2.58.dist-info → fast_agent_mcp-0.3.0.dist-info}/METADATA +7 -7
  153. fast_agent_mcp-0.3.0.dist-info/RECORD +202 -0
  154. fast_agent_mcp-0.3.0.dist-info/entry_points.txt +5 -0
  155. fast_agent_mcp-0.2.58.dist-info/RECORD +0 -193
  156. fast_agent_mcp-0.2.58.dist-info/entry_points.txt +0 -6
  157. mcp_agent/__init__.py +0 -114
  158. mcp_agent/agents/agent.py +0 -92
  159. mcp_agent/agents/workflow/__init__.py +0 -1
  160. mcp_agent/agents/workflow/orchestrator_agent.py +0 -597
  161. mcp_agent/app.py +0 -175
  162. mcp_agent/core/__init__.py +0 -26
  163. mcp_agent/core/prompt.py +0 -191
  164. mcp_agent/event_progress.py +0 -134
  165. mcp_agent/human_input/handler.py +0 -81
  166. mcp_agent/llm/__init__.py +0 -2
  167. mcp_agent/llm/augmented_llm_passthrough.py +0 -232
  168. mcp_agent/llm/augmented_llm_slow.py +0 -53
  169. mcp_agent/llm/providers/__init__.py +0 -8
  170. mcp_agent/llm/providers/augmented_llm_anthropic.py +0 -718
  171. mcp_agent/llm/providers/augmented_llm_google_native.py +0 -496
  172. mcp_agent/llm/providers/sampling_converter_anthropic.py +0 -57
  173. mcp_agent/llm/providers/sampling_converter_openai.py +0 -26
  174. mcp_agent/llm/sampling_format_converter.py +0 -37
  175. mcp_agent/logging/__init__.py +0 -0
  176. mcp_agent/mcp/__init__.py +0 -50
  177. mcp_agent/mcp/helpers/__init__.py +0 -25
  178. mcp_agent/mcp/helpers/content_helpers.py +0 -187
  179. mcp_agent/mcp/interfaces.py +0 -266
  180. mcp_agent/mcp/prompts/__init__.py +0 -0
  181. mcp_agent/mcp/prompts/__main__.py +0 -10
  182. mcp_agent/mcp_server_registry.py +0 -343
  183. mcp_agent/tools/tool_definition.py +0 -14
  184. mcp_agent/ui/console_display.py +0 -790
  185. mcp_agent/ui/console_display_legacy.py +0 -401
  186. {mcp_agent → fast_agent}/agents/workflow/orchestrator_prompts.py +0 -0
  187. {mcp_agent/agents → fast_agent/cli}/__init__.py +0 -0
  188. {mcp_agent → fast_agent}/cli/constants.py +0 -0
  189. {mcp_agent → fast_agent}/core/error_handling.py +0 -0
  190. {mcp_agent → fast_agent}/core/exceptions.py +0 -0
  191. {mcp_agent/cli → fast_agent/core/executor}/__init__.py +0 -0
  192. {mcp_agent → fast_agent/core}/executor/task_registry.py +0 -0
  193. {mcp_agent → fast_agent/core}/executor/workflow_signal.py +0 -0
  194. {mcp_agent → fast_agent}/human_input/form_fields.py +0 -0
  195. {mcp_agent → fast_agent}/llm/prompt_utils.py +0 -0
  196. {mcp_agent/core → fast_agent/llm}/request_params.py +0 -0
  197. {mcp_agent → fast_agent}/mcp/common.py +0 -0
  198. {mcp_agent/executor → fast_agent/mcp/prompts}/__init__.py +0 -0
  199. {mcp_agent → fast_agent}/mcp/prompts/prompt_constants.py +0 -0
  200. {mcp_agent → fast_agent}/py.typed +0 -0
  201. {mcp_agent → fast_agent}/resources/examples/data-analysis/fastagent.config.yaml +0 -0
  202. {mcp_agent → fast_agent}/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -0
  203. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_account_server.py +0 -0
  204. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_forms_server.py +0 -0
  205. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_game_server.py +0 -0
  206. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/fastagent.config.yaml +0 -0
  207. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +0 -0
  208. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/fastagent.config.yaml +0 -0
  209. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +0 -0
  210. {mcp_agent → fast_agent}/resources/examples/researcher/fastagent.config.yaml +0 -0
  211. {mcp_agent → fast_agent}/resources/examples/tensorzero/.env.sample +0 -0
  212. {mcp_agent → fast_agent}/resources/examples/tensorzero/Makefile +0 -0
  213. {mcp_agent → fast_agent}/resources/examples/tensorzero/README.md +0 -0
  214. {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  215. {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/crab.png +0 -0
  216. {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  217. {mcp_agent → fast_agent}/resources/examples/tensorzero/docker-compose.yml +0 -0
  218. {mcp_agent → fast_agent}/resources/examples/tensorzero/fastagent.config.yaml +0 -0
  219. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/Dockerfile +0 -0
  220. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/entrypoint.sh +0 -0
  221. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/mcp_server.py +0 -0
  222. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/pyproject.toml +0 -0
  223. {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/system_schema.json +0 -0
  224. {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +0 -0
  225. {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +0 -0
  226. {mcp_agent → fast_agent}/resources/examples/workflows/fastagent.config.yaml +0 -0
  227. {mcp_agent → fast_agent}/resources/examples/workflows/graded_report.md +0 -0
  228. {mcp_agent → fast_agent}/resources/examples/workflows/short_story.md +0 -0
  229. {mcp_agent → fast_agent}/resources/examples/workflows/short_story.txt +0 -0
  230. {mcp_agent → fast_agent/ui}/console.py +0 -0
  231. {mcp_agent/core → fast_agent/ui}/mermaid_utils.py +0 -0
  232. {fast_agent_mcp-0.2.58.dist-info → fast_agent_mcp-0.3.0.dist-info}/WHEEL +0 -0
  233. {fast_agent_mcp-0.2.58.dist-info → fast_agent_mcp-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1005 @@
1
+ from enum import Enum
2
+ from json import JSONDecodeError
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
4
+
5
+ from mcp.types import CallToolResult
6
+ from rich.panel import Panel
7
+ from rich.text import Text
8
+
9
+ from fast_agent.ui import console
10
+ from fast_agent.ui.mcp_ui_utils import UILink
11
+ from fast_agent.ui.mermaid_utils import (
12
+ MermaidDiagram,
13
+ create_mermaid_live_link,
14
+ detect_diagram_type,
15
+ extract_mermaid_diagrams,
16
+ )
17
+
18
+ if TYPE_CHECKING:
19
+ from fast_agent.mcp.prompt_message_extended import PromptMessageExtended
20
+
21
+ CODE_STYLE = "native"
22
+
23
+
24
+ class MessageType(Enum):
25
+ """Types of messages that can be displayed."""
26
+
27
+ USER = "user"
28
+ ASSISTANT = "assistant"
29
+ TOOL_CALL = "tool_call"
30
+ TOOL_RESULT = "tool_result"
31
+
32
+
33
+ # Configuration for each message type
34
+ MESSAGE_CONFIGS = {
35
+ MessageType.USER: {
36
+ "block_color": "blue",
37
+ "arrow": "▶",
38
+ "arrow_style": "dim blue",
39
+ "highlight_color": "blue",
40
+ },
41
+ MessageType.ASSISTANT: {
42
+ "block_color": "green",
43
+ "arrow": "◀",
44
+ "arrow_style": "dim green",
45
+ "highlight_color": "bright_green",
46
+ },
47
+ MessageType.TOOL_CALL: {
48
+ "block_color": "magenta",
49
+ "arrow": "◀",
50
+ "arrow_style": "dim magenta",
51
+ "highlight_color": "magenta",
52
+ },
53
+ MessageType.TOOL_RESULT: {
54
+ "block_color": "magenta", # Can be overridden to red if error
55
+ "arrow": "▶",
56
+ "arrow_style": "dim magenta",
57
+ "highlight_color": "magenta",
58
+ },
59
+ }
60
+
61
+ HTML_ESCAPE_CHARS = {
62
+ "&": "&",
63
+ "<": "&lt;",
64
+ ">": "&gt;",
65
+ '"': "&quot;",
66
+ "'": "&#39;",
67
+ }
68
+
69
+
70
+ def _prepare_markdown_content(content: str, escape_xml: bool = True) -> str:
71
+ """Prepare content for markdown rendering by escaping HTML/XML tags
72
+ while preserving code blocks and inline code.
73
+
74
+ This ensures XML/HTML tags are displayed as visible text rather than
75
+ being interpreted as markup by the markdown renderer.
76
+
77
+ Note: This method does not handle overlapping code blocks (e.g., if inline
78
+ code appears within a fenced code block range). In practice, this is not
79
+ an issue since markdown syntax doesn't support such overlapping.
80
+ """
81
+ if not escape_xml or not isinstance(content, str):
82
+ return content
83
+
84
+ protected_ranges = []
85
+ import re
86
+
87
+ # Protect fenced code blocks (don't escape anything inside these)
88
+ code_block_pattern = r"```[\s\S]*?```"
89
+ for match in re.finditer(code_block_pattern, content):
90
+ protected_ranges.append((match.start(), match.end()))
91
+
92
+ # Protect inline code (don't escape anything inside these)
93
+ inline_code_pattern = r"(?<!`)`(?!``)[^`\n]+`(?!`)"
94
+ for match in re.finditer(inline_code_pattern, content):
95
+ protected_ranges.append((match.start(), match.end()))
96
+
97
+ protected_ranges.sort(key=lambda x: x[0])
98
+
99
+ # Build the escaped content
100
+ result = []
101
+ last_end = 0
102
+
103
+ for start, end in protected_ranges:
104
+ # Escape everything outside protected ranges
105
+ unprotected_text = content[last_end:start]
106
+ for char, replacement in HTML_ESCAPE_CHARS.items():
107
+ unprotected_text = unprotected_text.replace(char, replacement)
108
+ result.append(unprotected_text)
109
+
110
+ # Keep protected ranges (code blocks) as-is
111
+ result.append(content[start:end])
112
+ last_end = end
113
+
114
+ # Escape any remaining content after the last protected range
115
+ remainder_text = content[last_end:]
116
+ for char, replacement in HTML_ESCAPE_CHARS.items():
117
+ remainder_text = remainder_text.replace(char, replacement)
118
+ result.append(remainder_text)
119
+
120
+ return "".join(result)
121
+
122
+
123
+ class ConsoleDisplay:
124
+ """
125
+ Handles displaying formatted messages, tool calls, and results to the console.
126
+ This centralizes the UI display logic used by LLM implementations.
127
+ """
128
+
129
+ def __init__(self, config=None) -> None:
130
+ """
131
+ Initialize the console display handler.
132
+
133
+ Args:
134
+ config: Configuration object containing display preferences
135
+ """
136
+ self.config = config
137
+ self._markup = config.logger.enable_markup if config else True
138
+ self._escape_xml = True
139
+
140
+ def display_message(
141
+ self,
142
+ content: Any,
143
+ message_type: MessageType,
144
+ name: str | None = None,
145
+ right_info: str = "",
146
+ bottom_metadata: List[str] | None = None,
147
+ highlight_items: str | List[str] | None = None,
148
+ max_item_length: int | None = None,
149
+ is_error: bool = False,
150
+ truncate_content: bool = True,
151
+ additional_message: Text | None = None,
152
+ ) -> None:
153
+ """
154
+ Unified method to display formatted messages to the console.
155
+
156
+ Args:
157
+ content: The main content to display (str, Text, JSON, etc.)
158
+ message_type: Type of message (USER, ASSISTANT, TOOL_CALL, TOOL_RESULT)
159
+ name: Optional name to display (agent name, user name, etc.)
160
+ right_info: Information to display on the right side of the header
161
+ bottom_metadata: Optional list of items for bottom separator
162
+ highlight_items: Item(s) to highlight in bottom metadata
163
+ max_item_length: Optional max length for bottom metadata items (with ellipsis)
164
+ is_error: For tool results, whether this is an error (uses red color)
165
+ truncate_content: Whether to truncate long content
166
+ """
167
+ # Get configuration for this message type
168
+ config = MESSAGE_CONFIGS[message_type]
169
+
170
+ # Override colors for error states
171
+ if is_error and message_type == MessageType.TOOL_RESULT:
172
+ block_color = "red"
173
+ else:
174
+ block_color = config["block_color"]
175
+
176
+ # Build the left side of the header
177
+ arrow = config["arrow"]
178
+ arrow_style = config["arrow_style"]
179
+ left = f"[{block_color}]▎[/{block_color}][{arrow_style}]{arrow}[/{arrow_style}]"
180
+ if name:
181
+ left += f" [{block_color if not is_error else 'red'}]{name}[/{block_color if not is_error else 'red'}]"
182
+
183
+ # Create combined separator and status line
184
+ self._create_combined_separator_status(left, right_info)
185
+
186
+ # Display the content
187
+ self._display_content(
188
+ content, truncate_content, is_error, message_type, check_markdown_markers=False
189
+ )
190
+ if additional_message:
191
+ console.console.print(additional_message, markup=self._markup)
192
+
193
+ # Handle bottom separator with optional metadata
194
+ console.console.print()
195
+
196
+ if bottom_metadata:
197
+ # Apply shortening if requested
198
+ display_items = bottom_metadata
199
+ if max_item_length:
200
+ display_items = self._shorten_items(bottom_metadata, max_item_length)
201
+
202
+ # Normalize highlight_items
203
+ if highlight_items is None:
204
+ highlight_items = []
205
+ elif isinstance(highlight_items, str):
206
+ highlight_items = [highlight_items]
207
+
208
+ # Shorten highlight items to match if we shortened display items
209
+ if max_item_length:
210
+ highlight_items = self._shorten_items(highlight_items, max_item_length)
211
+
212
+ # Format the metadata with highlighting, clipped to available width
213
+ # Compute available width for the metadata segment (excluding the fixed prefix/suffix)
214
+ total_width = console.console.size.width
215
+ prefix = Text("─| ")
216
+ prefix.stylize("dim")
217
+ suffix = Text(" |")
218
+ suffix.stylize("dim")
219
+ available = max(0, total_width - prefix.cell_len - suffix.cell_len)
220
+
221
+ metadata_text = self._format_bottom_metadata(
222
+ display_items,
223
+ highlight_items,
224
+ config["highlight_color"],
225
+ max_width=available,
226
+ )
227
+
228
+ # Create the separator line with metadata
229
+ line = Text()
230
+ line.append_text(prefix)
231
+ line.append_text(metadata_text)
232
+ line.append_text(suffix)
233
+ remaining = total_width - line.cell_len
234
+ if remaining > 0:
235
+ line.append("─" * remaining, style="dim")
236
+ console.console.print(line, markup=self._markup)
237
+ else:
238
+ # No metadata - continuous bar
239
+ console.console.print("─" * console.console.size.width, style="dim")
240
+
241
+ console.console.print()
242
+
243
+ def _display_content(
244
+ self,
245
+ content: Any,
246
+ truncate: bool = True,
247
+ is_error: bool = False,
248
+ message_type: Optional[MessageType] = None,
249
+ check_markdown_markers: bool = False,
250
+ ) -> None:
251
+ """
252
+ Display content in the appropriate format.
253
+
254
+ Args:
255
+ content: Content to display
256
+ truncate: Whether to truncate long content
257
+ is_error: Whether this is error content (affects styling)
258
+ message_type: Type of message to determine appropriate styling
259
+ check_markdown_markers: If True, only use markdown rendering when markers are present
260
+ """
261
+ import json
262
+ import re
263
+
264
+ from rich.markdown import Markdown
265
+ from rich.pretty import Pretty
266
+ from rich.syntax import Syntax
267
+
268
+ from fast_agent.mcp.helpers.content_helpers import get_text, is_text_content
269
+
270
+ # Determine the style based on message type
271
+ # USER and ASSISTANT messages should display in normal style
272
+ # TOOL_CALL and TOOL_RESULT should be dimmed
273
+ if is_error:
274
+ style = "dim red"
275
+ elif message_type in [MessageType.USER, MessageType.ASSISTANT]:
276
+ style = None # No style means default/normal white
277
+ else:
278
+ style = "dim"
279
+
280
+ # Handle different content types
281
+ if isinstance(content, str):
282
+ # Try to detect and handle different string formats
283
+ try:
284
+ # Try as JSON first
285
+ json_obj = json.loads(content)
286
+ if truncate and self.config and self.config.logger.truncate_tools:
287
+ pretty_obj = Pretty(json_obj, max_length=10, max_string=50)
288
+ else:
289
+ pretty_obj = Pretty(json_obj)
290
+ # Apply style only if specified
291
+ if style:
292
+ console.console.print(pretty_obj, style=style, markup=self._markup)
293
+ else:
294
+ console.console.print(pretty_obj, markup=self._markup)
295
+ except (JSONDecodeError, TypeError, ValueError):
296
+ # Check if content appears to be primarily XML
297
+ xml_pattern = r"^<[a-zA-Z_][a-zA-Z0-9_-]*[^>]*>"
298
+ is_xml_content = (
299
+ bool(re.match(xml_pattern, content.strip())) and content.count("<") > 5
300
+ )
301
+
302
+ if is_xml_content:
303
+ # Display XML content with syntax highlighting for better readability
304
+ syntax = Syntax(content, "xml", theme=CODE_STYLE, line_numbers=False)
305
+ console.console.print(syntax, markup=self._markup)
306
+ elif check_markdown_markers:
307
+ # Check for markdown markers before deciding to use markdown rendering
308
+ if any(marker in content for marker in ["##", "**", "*", "`", "---", "###"]):
309
+ # Has markdown markers - render as markdown with escaping
310
+ prepared_content = _prepare_markdown_content(content, self._escape_xml)
311
+ md = Markdown(prepared_content, code_theme=CODE_STYLE)
312
+ console.console.print(md, markup=self._markup)
313
+ else:
314
+ # Plain text - display as-is
315
+ if (
316
+ truncate
317
+ and self.config
318
+ and self.config.logger.truncate_tools
319
+ and len(content) > 360
320
+ ):
321
+ content = content[:360] + "..."
322
+ if style:
323
+ console.console.print(content, style=style, markup=self._markup)
324
+ else:
325
+ console.console.print(content, markup=self._markup)
326
+ else:
327
+ # Check if it looks like markdown
328
+ if any(marker in content for marker in ["##", "**", "*", "`", "---", "###"]):
329
+ # Escape HTML/XML tags while preserving code blocks
330
+ prepared_content = _prepare_markdown_content(content, self._escape_xml)
331
+ md = Markdown(prepared_content, code_theme=CODE_STYLE)
332
+ # Markdown handles its own styling, don't apply style
333
+ console.console.print(md, markup=self._markup)
334
+ else:
335
+ # Plain text
336
+ if (
337
+ truncate
338
+ and self.config
339
+ and self.config.logger.truncate_tools
340
+ and len(content) > 360
341
+ ):
342
+ content = content[:360] + "..."
343
+ # Apply style only if specified (None means default white)
344
+ if style:
345
+ console.console.print(content, style=style, markup=self._markup)
346
+ else:
347
+ console.console.print(content, markup=self._markup)
348
+ elif isinstance(content, Text):
349
+ # Rich Text object - check if it contains markdown
350
+ plain_text = content.plain
351
+
352
+ # Check if the plain text contains markdown markers
353
+ if any(marker in plain_text for marker in ["##", "**", "*", "`", "---", "###"]):
354
+ # Split the Text object into segments
355
+ # We need to handle the main content (which may have markdown)
356
+ # and any styled segments that were appended
357
+
358
+ # For now, we'll render the entire content with markdown support
359
+ # This means extracting each span and handling it appropriately
360
+ from rich.markdown import Markdown
361
+
362
+ # If the Text object has multiple spans with different styles,
363
+ # we need to be careful about how we render them
364
+ if len(content._spans) > 1:
365
+ # Complex case: Text has multiple styled segments
366
+ # We'll render the first part as markdown if it contains markers
367
+ # and append other styled parts separately
368
+
369
+ # Find where the markdown content ends (usually the first span)
370
+ markdown_end = content._spans[0].end if content._spans else len(plain_text)
371
+ markdown_part = plain_text[:markdown_end]
372
+
373
+ # Check if the first part has markdown
374
+ if any(
375
+ marker in markdown_part for marker in ["##", "**", "*", "`", "---", "###"]
376
+ ):
377
+ # Render markdown part
378
+ prepared_content = _prepare_markdown_content(
379
+ markdown_part, self._escape_xml
380
+ )
381
+ md = Markdown(prepared_content, code_theme=CODE_STYLE)
382
+ console.console.print(md, markup=self._markup)
383
+
384
+ # Then render any additional styled segments
385
+ if markdown_end < len(plain_text):
386
+ remaining_text = Text()
387
+ for span in content._spans:
388
+ if span.start >= markdown_end:
389
+ segment_text = plain_text[span.start : span.end]
390
+ remaining_text.append(segment_text, style=span.style)
391
+ if remaining_text.plain:
392
+ console.console.print(remaining_text, markup=self._markup)
393
+ else:
394
+ # No markdown in first part, just print the whole Text object
395
+ console.console.print(content, markup=self._markup)
396
+ else:
397
+ # Simple case: entire text should be rendered as markdown
398
+ prepared_content = _prepare_markdown_content(plain_text, self._escape_xml)
399
+ md = Markdown(prepared_content, code_theme=CODE_STYLE)
400
+ console.console.print(md, markup=self._markup)
401
+ else:
402
+ # No markdown markers, print as regular Rich Text
403
+ console.console.print(content, markup=self._markup)
404
+ elif isinstance(content, list):
405
+ # Handle content blocks (for tool results)
406
+ if len(content) == 1 and is_text_content(content[0]):
407
+ # Single text block - display directly
408
+ text_content = get_text(content[0])
409
+ if text_content:
410
+ if (
411
+ truncate
412
+ and self.config
413
+ and self.config.logger.truncate_tools
414
+ and len(text_content) > 360
415
+ ):
416
+ text_content = text_content[:360] + "..."
417
+ # Apply style only if specified
418
+ if style:
419
+ console.console.print(text_content, style=style, markup=self._markup)
420
+ else:
421
+ console.console.print(text_content, markup=self._markup)
422
+ else:
423
+ # Apply style only if specified
424
+ if style:
425
+ console.console.print("(empty text)", style=style, markup=self._markup)
426
+ else:
427
+ console.console.print("(empty text)", markup=self._markup)
428
+ else:
429
+ # Multiple blocks or non-text content
430
+ if truncate and self.config and self.config.logger.truncate_tools:
431
+ pretty_obj = Pretty(content, max_length=10, max_string=50)
432
+ else:
433
+ pretty_obj = Pretty(content)
434
+ # Apply style only if specified
435
+ if style:
436
+ console.console.print(pretty_obj, style=style, markup=self._markup)
437
+ else:
438
+ console.console.print(pretty_obj, markup=self._markup)
439
+ else:
440
+ # Any other type - use Pretty
441
+ if truncate and self.config and self.config.logger.truncate_tools:
442
+ pretty_obj = Pretty(content, max_length=10, max_string=50)
443
+ else:
444
+ pretty_obj = Pretty(content)
445
+ # Apply style only if specified
446
+ if style:
447
+ console.console.print(pretty_obj, style=style, markup=self._markup)
448
+ else:
449
+ console.console.print(pretty_obj, markup=self._markup)
450
+
451
+ def _shorten_items(self, items: List[str], max_length: int) -> List[str]:
452
+ """
453
+ Shorten items to max_length with ellipsis if needed.
454
+
455
+ Args:
456
+ items: List of strings to potentially shorten
457
+ max_length: Maximum length for each item
458
+
459
+ Returns:
460
+ List of shortened strings
461
+ """
462
+ return [item[: max_length - 1] + "…" if len(item) > max_length else item for item in items]
463
+
464
+ def _format_bottom_metadata(
465
+ self,
466
+ items: List[str],
467
+ highlight_items: List[str],
468
+ highlight_color: str,
469
+ max_width: int | None = None,
470
+ ) -> Text:
471
+ """
472
+ Format a list of items with pipe separators and highlighting.
473
+
474
+ Args:
475
+ items: List of items to display
476
+ highlight_items: List of items to highlight
477
+ highlight_color: Color to use for highlighting
478
+
479
+ Returns:
480
+ Formatted Text object with proper separators and highlighting
481
+ """
482
+ formatted = Text()
483
+
484
+ def will_fit(next_segment: Text) -> bool:
485
+ if max_width is None:
486
+ return True
487
+ # projected length if we append next_segment
488
+ return formatted.cell_len + next_segment.cell_len <= max_width
489
+
490
+ for i, item in enumerate(items):
491
+ sep = Text(" | ", style="dim") if i > 0 else Text("")
492
+
493
+ # Prepare item text with potential highlighting
494
+ should_highlight = False
495
+ if item in highlight_items:
496
+ should_highlight = True
497
+ else:
498
+ for highlight in highlight_items:
499
+ if item.startswith(highlight) or highlight.endswith(item):
500
+ should_highlight = True
501
+ break
502
+
503
+ item_text = Text(item, style=(highlight_color if should_highlight else "dim"))
504
+
505
+ # Check if separator + item fits in available width
506
+ if not will_fit(sep + item_text):
507
+ # If nothing has been added yet and the item itself is too long,
508
+ # leave space for an ellipsis and stop.
509
+ if formatted.cell_len == 0 and max_width is not None and max_width > 1:
510
+ # show truncated indicator only
511
+ formatted.append("…", style="dim")
512
+ else:
513
+ # Indicate there are more items but avoid wrapping
514
+ if max_width is None or formatted.cell_len < max_width:
515
+ formatted.append(" …", style="dim")
516
+ break
517
+
518
+ # Append separator and item
519
+ if sep.plain:
520
+ formatted.append_text(sep)
521
+ formatted.append_text(item_text)
522
+
523
+ return formatted
524
+
525
+ def show_tool_result(self, result: CallToolResult, name: str | None = None) -> None:
526
+ """Display a tool result in the new visual style."""
527
+ if not self.config or not self.config.logger.show_tools:
528
+ return
529
+
530
+ # Import content helpers
531
+ from fast_agent.mcp.helpers.content_helpers import get_text, is_text_content
532
+
533
+ # Analyze content to determine display format and status
534
+ content = result.content
535
+ if result.isError:
536
+ status = "ERROR"
537
+ else:
538
+ # Check if it's a list with content blocks
539
+ if len(content) == 0:
540
+ status = "No Content"
541
+ elif len(content) == 1 and is_text_content(content[0]):
542
+ text_content = get_text(content[0])
543
+ char_count = len(text_content) if text_content else 0
544
+ status = f"Text Only {char_count} chars"
545
+ else:
546
+ text_count = sum(1 for item in content if is_text_content(item))
547
+ if text_count == len(content):
548
+ status = f"{len(content)} Text Blocks" if len(content) > 1 else "1 Text Block"
549
+ else:
550
+ status = (
551
+ f"{len(content)} Content Blocks" if len(content) > 1 else "1 Content Block"
552
+ )
553
+
554
+ # Build right info
555
+ right_info = f"[dim]tool result - {status}[/dim]"
556
+
557
+ # Display using unified method
558
+ self.display_message(
559
+ content=content,
560
+ message_type=MessageType.TOOL_RESULT,
561
+ name=name,
562
+ right_info=right_info,
563
+ is_error=result.isError,
564
+ truncate_content=True,
565
+ )
566
+
567
+ def show_tool_call(
568
+ self,
569
+ tool_name: str,
570
+ tool_args: Dict[str, Any] | None,
571
+ bottom_items: List[str] | None = None,
572
+ highlight_items: str | List[str] | None = None,
573
+ max_item_length: int | None = None,
574
+ name: str | None = None,
575
+ ) -> None:
576
+ """Display a tool call in the new visual style.
577
+
578
+ Args:
579
+ tool_name: Name of the tool being called
580
+ tool_args: Arguments being passed to the tool
581
+ bottom_items: Optional list of items for bottom separator (e.g., available tools)
582
+ highlight_items: Item(s) to highlight in the bottom separator
583
+ max_item_length: Optional max length for bottom items (with ellipsis)
584
+ name: Optional agent name
585
+ """
586
+ if not self.config or not self.config.logger.show_tools:
587
+ return
588
+
589
+ # Build right info
590
+ right_info = f"[dim]tool request - {tool_name}[/dim]"
591
+
592
+ # Display using unified method
593
+ self.display_message(
594
+ content=tool_args,
595
+ message_type=MessageType.TOOL_CALL,
596
+ name=name,
597
+ right_info=right_info,
598
+ bottom_metadata=bottom_items,
599
+ highlight_items=tool_name,
600
+ max_item_length=max_item_length,
601
+ truncate_content=True,
602
+ )
603
+
604
+ async def show_tool_update(self, updated_server: str, agent_name: str | None = None) -> None:
605
+ """Show a tool update for a server in the new visual style.
606
+
607
+ Args:
608
+ updated_server: Name of the server being updated
609
+ agent_name: Optional agent name to display
610
+ """
611
+ if not self.config or not self.config.logger.show_tools:
612
+ return
613
+
614
+ # Combined separator and status line
615
+ if agent_name:
616
+ left = (
617
+ f"[magenta]▎[/magenta][dim magenta]▶[/dim magenta] [magenta]{agent_name}[/magenta]"
618
+ )
619
+ else:
620
+ left = "[magenta]▎[/magenta][dim magenta]▶[/dim magenta]"
621
+
622
+ right = f"[dim]{updated_server}[/dim]"
623
+ self._create_combined_separator_status(left, right)
624
+
625
+ # Display update message
626
+ message = f"Updating tools for server {updated_server}"
627
+ console.console.print(message, style="dim", markup=self._markup)
628
+
629
+ # Bottom separator
630
+ console.console.print()
631
+ console.console.print("─" * console.console.size.width, style="dim")
632
+ console.console.print()
633
+
634
+ # Force prompt_toolkit redraw if active
635
+ try:
636
+ from prompt_toolkit.application.current import get_app
637
+
638
+ get_app().invalidate() # Forces prompt_toolkit to redraw
639
+ except: # noqa: E722
640
+ pass # No active prompt_toolkit session
641
+
642
+ def _create_combined_separator_status(self, left_content: str, right_info: str = "") -> None:
643
+ """
644
+ Create a combined separator and status line.
645
+
646
+ Args:
647
+ left_content: The main content (block, arrow, name) - left justified with color
648
+ right_info: Supplementary information to show in brackets - right aligned
649
+ """
650
+ width = console.console.size.width
651
+
652
+ # Create left text
653
+ left_text = Text.from_markup(left_content)
654
+
655
+ # Create right text if we have info
656
+ if right_info and right_info.strip():
657
+ # Add dim brackets around the right info
658
+ right_text = Text()
659
+ right_text.append("[", style="dim")
660
+ right_text.append_text(Text.from_markup(right_info))
661
+ right_text.append("]", style="dim")
662
+ # Calculate separator count
663
+ separator_count = width - left_text.cell_len - right_text.cell_len
664
+ if separator_count < 1:
665
+ separator_count = 1 # Always at least 1 separator
666
+ else:
667
+ right_text = Text("")
668
+ separator_count = width - left_text.cell_len
669
+
670
+ # Build the combined line
671
+ combined = Text()
672
+ combined.append_text(left_text)
673
+ combined.append(" ", style="default")
674
+ combined.append("─" * (separator_count - 1), style="dim")
675
+ combined.append_text(right_text)
676
+
677
+ # Print with empty line before
678
+ console.console.print()
679
+ console.console.print(combined, markup=self._markup)
680
+ console.console.print()
681
+
682
+ async def show_assistant_message(
683
+ self,
684
+ message_text: Union[str, Text, "PromptMessageExtended"],
685
+ bottom_items: List[str] | None = None,
686
+ highlight_items: str | List[str] | None = None,
687
+ max_item_length: int | None = None,
688
+ name: str | None = None,
689
+ model: str | None = None,
690
+ additional_message: Optional[Text] = None,
691
+ ) -> None:
692
+ """Display an assistant message in a formatted panel.
693
+
694
+ Args:
695
+ message_text: The message content to display (str, Text, or PromptMessageExtended)
696
+ bottom_items: Optional list of items for bottom separator (e.g., servers, destinations)
697
+ highlight_items: Item(s) to highlight in the bottom separator
698
+ max_item_length: Optional max length for bottom items (with ellipsis)
699
+ title: Title for the message (default "ASSISTANT")
700
+ name: Optional agent name
701
+ model: Optional model name for right info
702
+ additional_message: Optional additional styled message to append
703
+ """
704
+ if not self.config or not self.config.logger.show_chat:
705
+ return
706
+
707
+ # Extract text from PromptMessageExtended if needed
708
+ from fast_agent.types import PromptMessageExtended
709
+
710
+ if isinstance(message_text, PromptMessageExtended):
711
+ display_text = message_text.last_text() or ""
712
+ else:
713
+ display_text = message_text
714
+
715
+ # Build right info
716
+ right_info = f"[dim]{model}[/dim]" if model else ""
717
+
718
+ # Display main message using unified method
719
+ self.display_message(
720
+ content=display_text,
721
+ message_type=MessageType.ASSISTANT,
722
+ name=name,
723
+ right_info=right_info,
724
+ bottom_metadata=bottom_items,
725
+ highlight_items=highlight_items,
726
+ max_item_length=max_item_length,
727
+ truncate_content=False, # Assistant messages shouldn't be truncated
728
+ additional_message=additional_message,
729
+ )
730
+
731
+ # Handle mermaid diagrams separately (after the main message)
732
+ # Extract plain text for mermaid detection
733
+ plain_text = display_text
734
+ if isinstance(display_text, Text):
735
+ plain_text = display_text.plain
736
+
737
+ if isinstance(plain_text, str):
738
+ diagrams = extract_mermaid_diagrams(plain_text)
739
+ if diagrams:
740
+ self._display_mermaid_diagrams(diagrams)
741
+
742
+ def _display_mermaid_diagrams(self, diagrams: List[MermaidDiagram]) -> None:
743
+ """Display mermaid diagram links."""
744
+ diagram_content = Text()
745
+ # Add bullet at the beginning
746
+ diagram_content.append("● ", style="dim")
747
+
748
+ for i, diagram in enumerate(diagrams, 1):
749
+ if i > 1:
750
+ diagram_content.append(" • ", style="dim")
751
+
752
+ # Generate URL
753
+ url = create_mermaid_live_link(diagram.content)
754
+
755
+ # Format: "1 - Title" or "1 - Flowchart" or "Diagram 1"
756
+ if diagram.title:
757
+ diagram_content.append(f"{i} - {diagram.title}", style=f"bright_blue link {url}")
758
+ else:
759
+ # Try to detect diagram type, fallback to "Diagram N"
760
+ diagram_type = detect_diagram_type(diagram.content)
761
+ if diagram_type != "Diagram":
762
+ diagram_content.append(f"{i} - {diagram_type}", style=f"bright_blue link {url}")
763
+ else:
764
+ diagram_content.append(f"Diagram {i}", style=f"bright_blue link {url}")
765
+
766
+ # Display diagrams on a simple new line (more space efficient)
767
+ console.console.print()
768
+ console.console.print(diagram_content, markup=self._markup)
769
+
770
+ async def show_mcp_ui_links(self, links: List[UILink]) -> None:
771
+ """Display MCP-UI links beneath the chat like mermaid links."""
772
+ if not self.config or not self.config.logger.show_chat:
773
+ return
774
+
775
+ if not links:
776
+ return
777
+
778
+ content = Text()
779
+ content.append("● mcp-ui ", style="dim")
780
+ for i, link in enumerate(links, 1):
781
+ if i > 1:
782
+ content.append(" • ", style="dim")
783
+ # Prefer a web-friendly URL (http(s) or data:) if available; fallback to local file
784
+ url = link.web_url if getattr(link, "web_url", None) else f"file://{link.file_path}"
785
+ label = f"{i} - {link.title}"
786
+ content.append(label, style=f"bright_blue link {url}")
787
+
788
+ console.console.print()
789
+ console.console.print(content, markup=self._markup)
790
+
791
+ def show_user_message(
792
+ self,
793
+ message: Union[str, Text],
794
+ model: str | None = None,
795
+ chat_turn: int = 0,
796
+ name: str | None = None,
797
+ ) -> None:
798
+ """Display a user message in the new visual style."""
799
+ if not self.config or not self.config.logger.show_chat:
800
+ return
801
+
802
+ # Build right side with model and turn
803
+ right_parts = []
804
+ if model:
805
+ right_parts.append(model)
806
+ if chat_turn > 0:
807
+ right_parts.append(f"turn {chat_turn}")
808
+
809
+ right_info = f"[dim]{' '.join(right_parts)}[/dim]" if right_parts else ""
810
+
811
+ self.display_message(
812
+ content=message,
813
+ message_type=MessageType.USER,
814
+ name=name,
815
+ right_info=right_info,
816
+ truncate_content=False, # User messages typically shouldn't be truncated
817
+ )
818
+
819
+ async def show_prompt_loaded(
820
+ self,
821
+ prompt_name: str,
822
+ description: Optional[str] = None,
823
+ message_count: int = 0,
824
+ agent_name: Optional[str] = None,
825
+ server_list: List[str] | None = None,
826
+ highlight_server: str | None = None,
827
+ arguments: Optional[dict[str, str]] = None,
828
+ ) -> None:
829
+ """
830
+ Display information about a loaded prompt template.
831
+
832
+ Args:
833
+ prompt_name: The name of the prompt that was loaded
834
+ description: Optional description of the prompt
835
+ message_count: Number of messages added to the conversation history
836
+ agent_name: Name of the agent using the prompt
837
+ server_list: Optional list of servers to display
838
+ highlight_server: Optional server name to highlight
839
+ arguments: Optional dictionary of arguments passed to the prompt template
840
+ """
841
+ if not self.config or not self.config.logger.show_tools:
842
+ return
843
+
844
+ # Build the server list with highlighting
845
+ display_server_list = Text()
846
+ if server_list:
847
+ for server_name in server_list:
848
+ style = "green" if server_name == highlight_server else "dim white"
849
+ display_server_list.append(f"[{server_name}] ", style)
850
+
851
+ # Create content text
852
+ content = Text()
853
+ messages_phrase = f"Loaded {message_count} message{'s' if message_count != 1 else ''}"
854
+ content.append(f"{messages_phrase} from template ", style="cyan italic")
855
+ content.append(f"'{prompt_name}'", style="cyan bold italic")
856
+
857
+ if agent_name:
858
+ content.append(f" for {agent_name}", style="cyan italic")
859
+
860
+ # Add template arguments if provided
861
+ if arguments:
862
+ content.append("\n\nArguments:", style="cyan")
863
+ for key, value in arguments.items():
864
+ content.append(f"\n {key}: ", style="cyan bold")
865
+ content.append(value, style="white")
866
+
867
+ if description:
868
+ content.append("\n\n", style="default")
869
+ content.append(description, style="dim white")
870
+
871
+ # Create panel
872
+ panel = Panel(
873
+ content,
874
+ title="[PROMPT LOADED]",
875
+ title_align="right",
876
+ style="cyan",
877
+ border_style="white",
878
+ padding=(1, 2),
879
+ subtitle=display_server_list,
880
+ subtitle_align="left",
881
+ )
882
+
883
+ console.console.print(panel, markup=self._markup)
884
+ console.console.print("\n")
885
+
886
+ def show_parallel_results(self, parallel_agent) -> None:
887
+ """Display parallel agent results in a clean, organized format.
888
+
889
+ Args:
890
+ parallel_agent: The parallel agent containing fan_out_agents with results
891
+ """
892
+
893
+ from rich.text import Text
894
+
895
+ if self.config and not self.config.logger.show_chat:
896
+ return
897
+
898
+ if not parallel_agent or not hasattr(parallel_agent, "fan_out_agents"):
899
+ return
900
+
901
+ # Collect results and agent information
902
+ agent_results = []
903
+
904
+ for agent in parallel_agent.fan_out_agents:
905
+ # Get the last response text from this agent
906
+ message_history = agent.message_history
907
+ if not message_history:
908
+ continue
909
+
910
+ last_message = message_history[-1]
911
+ content = last_message.last_text()
912
+
913
+ # Get model name
914
+ model = "unknown"
915
+ if (
916
+ hasattr(agent, "_llm")
917
+ and agent._llm
918
+ and hasattr(agent._llm, "default_request_params")
919
+ ):
920
+ model = getattr(agent._llm.default_request_params, "model", "unknown")
921
+
922
+ # Get usage information
923
+ tokens = 0
924
+ tool_calls = 0
925
+ if hasattr(agent, "usage_accumulator") and agent.usage_accumulator:
926
+ summary = agent.usage_accumulator.get_summary()
927
+ tokens = summary.get("cumulative_input_tokens", 0) + summary.get(
928
+ "cumulative_output_tokens", 0
929
+ )
930
+ tool_calls = summary.get("cumulative_tool_calls", 0)
931
+
932
+ agent_results.append(
933
+ {
934
+ "name": agent.name,
935
+ "model": model,
936
+ "content": content,
937
+ "tokens": tokens,
938
+ "tool_calls": tool_calls,
939
+ }
940
+ )
941
+
942
+ if not agent_results:
943
+ return
944
+
945
+ # Display header
946
+ console.console.print()
947
+ console.console.print("[dim]Parallel execution complete[/dim]")
948
+ console.console.print()
949
+
950
+ # Display results for each agent
951
+ for i, result in enumerate(agent_results):
952
+ if i > 0:
953
+ # Simple full-width separator
954
+ console.console.print()
955
+ console.console.print("─" * console.console.size.width, style="dim")
956
+ console.console.print()
957
+
958
+ # Two column header: model name (green) + usage info (dim)
959
+ left = f"[green]▎[/green] [bold green]{result['model']}[/bold green]"
960
+
961
+ # Build right side with tokens and tool calls if available
962
+ right_parts = []
963
+ if result["tokens"] > 0:
964
+ right_parts.append(f"{result['tokens']:,} tokens")
965
+ if result["tool_calls"] > 0:
966
+ right_parts.append(f"{result['tool_calls']} tools")
967
+
968
+ right = f"[dim]{' • '.join(right_parts) if right_parts else 'no usage data'}[/dim]"
969
+
970
+ # Calculate padding to right-align usage info
971
+ width = console.console.size.width
972
+ left_text = Text.from_markup(left)
973
+ right_text = Text.from_markup(right)
974
+ padding = max(1, width - left_text.cell_len - right_text.cell_len)
975
+
976
+ console.console.print(left + " " * padding + right, markup=self._markup)
977
+ console.console.print()
978
+
979
+ # Display content based on its type (check for markdown markers in parallel results)
980
+ content = result["content"]
981
+ # Use _display_content with assistant message type so content isn't dimmed
982
+ self._display_content(
983
+ content,
984
+ truncate=False,
985
+ is_error=False,
986
+ message_type=MessageType.ASSISTANT,
987
+ check_markdown_markers=True,
988
+ )
989
+
990
+ # Summary
991
+ console.console.print()
992
+ console.console.print("─" * console.console.size.width, style="dim")
993
+
994
+ total_tokens = sum(result["tokens"] for result in agent_results)
995
+ total_tools = sum(result["tool_calls"] for result in agent_results)
996
+
997
+ summary_parts = [f"{len(agent_results)} models"]
998
+ if total_tokens > 0:
999
+ summary_parts.append(f"{total_tokens:,} tokens")
1000
+ if total_tools > 0:
1001
+ summary_parts.append(f"{total_tools} tools")
1002
+
1003
+ summary_text = " • ".join(summary_parts)
1004
+ console.console.print(f"[dim]{summary_text}[/dim]")
1005
+ console.console.print()