fast-agent-mcp 0.2.58__py3-none-any.whl → 0.3.1__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 (234) 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 +75 -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 +52 -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 +54 -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/resources/setup/pyproject.toml.tmpl +17 -0
  140. fast_agent/tools/elicitation.py +369 -0
  141. fast_agent/types/__init__.py +32 -0
  142. fast_agent/types/llm_stop_reason.py +77 -0
  143. fast_agent/ui/__init__.py +38 -0
  144. fast_agent/ui/console_display.py +1005 -0
  145. {mcp_agent/human_input → fast_agent/ui}/elicitation_form.py +17 -12
  146. mcp_agent/human_input/elicitation_forms.py → fast_agent/ui/elicitation_style.py +1 -1
  147. {mcp_agent/core → fast_agent/ui}/enhanced_prompt.py +96 -25
  148. {mcp_agent/core → fast_agent/ui}/interactive_prompt.py +330 -125
  149. fast_agent/ui/mcp_ui_utils.py +224 -0
  150. {mcp_agent → fast_agent/ui}/progress_display.py +2 -2
  151. {mcp_agent/logging → fast_agent/ui}/rich_progress.py +4 -4
  152. {mcp_agent/core → fast_agent/ui}/usage_display.py +3 -8
  153. {fast_agent_mcp-0.2.58.dist-info → fast_agent_mcp-0.3.1.dist-info}/METADATA +7 -7
  154. fast_agent_mcp-0.3.1.dist-info/RECORD +203 -0
  155. fast_agent_mcp-0.3.1.dist-info/entry_points.txt +5 -0
  156. fast_agent_mcp-0.2.58.dist-info/RECORD +0 -193
  157. fast_agent_mcp-0.2.58.dist-info/entry_points.txt +0 -6
  158. mcp_agent/__init__.py +0 -114
  159. mcp_agent/agents/agent.py +0 -92
  160. mcp_agent/agents/workflow/__init__.py +0 -1
  161. mcp_agent/agents/workflow/orchestrator_agent.py +0 -597
  162. mcp_agent/app.py +0 -175
  163. mcp_agent/core/__init__.py +0 -26
  164. mcp_agent/core/prompt.py +0 -191
  165. mcp_agent/event_progress.py +0 -134
  166. mcp_agent/human_input/handler.py +0 -81
  167. mcp_agent/llm/__init__.py +0 -2
  168. mcp_agent/llm/augmented_llm_passthrough.py +0 -232
  169. mcp_agent/llm/augmented_llm_slow.py +0 -53
  170. mcp_agent/llm/providers/__init__.py +0 -8
  171. mcp_agent/llm/providers/augmented_llm_anthropic.py +0 -718
  172. mcp_agent/llm/providers/augmented_llm_google_native.py +0 -496
  173. mcp_agent/llm/providers/sampling_converter_anthropic.py +0 -57
  174. mcp_agent/llm/providers/sampling_converter_openai.py +0 -26
  175. mcp_agent/llm/sampling_format_converter.py +0 -37
  176. mcp_agent/logging/__init__.py +0 -0
  177. mcp_agent/mcp/__init__.py +0 -50
  178. mcp_agent/mcp/helpers/__init__.py +0 -25
  179. mcp_agent/mcp/helpers/content_helpers.py +0 -187
  180. mcp_agent/mcp/interfaces.py +0 -266
  181. mcp_agent/mcp/prompts/__init__.py +0 -0
  182. mcp_agent/mcp/prompts/__main__.py +0 -10
  183. mcp_agent/mcp_server_registry.py +0 -343
  184. mcp_agent/tools/tool_definition.py +0 -14
  185. mcp_agent/ui/console_display.py +0 -790
  186. mcp_agent/ui/console_display_legacy.py +0 -401
  187. {mcp_agent → fast_agent}/agents/workflow/orchestrator_prompts.py +0 -0
  188. {mcp_agent/agents → fast_agent/cli}/__init__.py +0 -0
  189. {mcp_agent → fast_agent}/cli/constants.py +0 -0
  190. {mcp_agent → fast_agent}/core/error_handling.py +0 -0
  191. {mcp_agent → fast_agent}/core/exceptions.py +0 -0
  192. {mcp_agent/cli → fast_agent/core/executor}/__init__.py +0 -0
  193. {mcp_agent → fast_agent/core}/executor/task_registry.py +0 -0
  194. {mcp_agent → fast_agent/core}/executor/workflow_signal.py +0 -0
  195. {mcp_agent → fast_agent}/human_input/form_fields.py +0 -0
  196. {mcp_agent → fast_agent}/llm/prompt_utils.py +0 -0
  197. {mcp_agent/core → fast_agent/llm}/request_params.py +0 -0
  198. {mcp_agent → fast_agent}/mcp/common.py +0 -0
  199. {mcp_agent/executor → fast_agent/mcp/prompts}/__init__.py +0 -0
  200. {mcp_agent → fast_agent}/mcp/prompts/prompt_constants.py +0 -0
  201. {mcp_agent → fast_agent}/py.typed +0 -0
  202. {mcp_agent → fast_agent}/resources/examples/data-analysis/fastagent.config.yaml +0 -0
  203. {mcp_agent → fast_agent}/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +0 -0
  204. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_account_server.py +0 -0
  205. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_forms_server.py +0 -0
  206. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/elicitation_game_server.py +0 -0
  207. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/fastagent.config.yaml +0 -0
  208. {mcp_agent → fast_agent}/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +0 -0
  209. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/fastagent.config.yaml +0 -0
  210. {mcp_agent → fast_agent}/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +0 -0
  211. {mcp_agent → fast_agent}/resources/examples/researcher/fastagent.config.yaml +0 -0
  212. {mcp_agent → fast_agent}/resources/examples/tensorzero/.env.sample +0 -0
  213. {mcp_agent → fast_agent}/resources/examples/tensorzero/Makefile +0 -0
  214. {mcp_agent → fast_agent}/resources/examples/tensorzero/README.md +0 -0
  215. {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  216. {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/crab.png +0 -0
  217. {mcp_agent → fast_agent}/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  218. {mcp_agent → fast_agent}/resources/examples/tensorzero/docker-compose.yml +0 -0
  219. {mcp_agent → fast_agent}/resources/examples/tensorzero/fastagent.config.yaml +0 -0
  220. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/Dockerfile +0 -0
  221. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/entrypoint.sh +0 -0
  222. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/mcp_server.py +0 -0
  223. {mcp_agent → fast_agent}/resources/examples/tensorzero/mcp_server/pyproject.toml +0 -0
  224. {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/system_schema.json +0 -0
  225. {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +0 -0
  226. {mcp_agent → fast_agent}/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +0 -0
  227. {mcp_agent → fast_agent}/resources/examples/workflows/fastagent.config.yaml +0 -0
  228. {mcp_agent → fast_agent}/resources/examples/workflows/graded_report.md +0 -0
  229. {mcp_agent → fast_agent}/resources/examples/workflows/short_story.md +0 -0
  230. {mcp_agent → fast_agent}/resources/examples/workflows/short_story.txt +0 -0
  231. {mcp_agent → fast_agent/ui}/console.py +0 -0
  232. {mcp_agent/core → fast_agent/ui}/mermaid_utils.py +0 -0
  233. {fast_agent_mcp-0.2.58.dist-info → fast_agent_mcp-0.3.1.dist-info}/WHEEL +0 -0
  234. {fast_agent_mcp-0.2.58.dist-info → fast_agent_mcp-0.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,790 +0,0 @@
1
- from json import JSONDecodeError
2
- from typing import Optional, Union
3
-
4
- from mcp.types import CallToolResult
5
- from rich.json import JSON
6
- from rich.panel import Panel
7
- from rich.text import Text
8
-
9
- from mcp_agent import console
10
- from mcp_agent.core.mermaid_utils import (
11
- create_mermaid_live_link,
12
- detect_diagram_type,
13
- extract_mermaid_diagrams,
14
- )
15
- from mcp_agent.mcp.common import SEP
16
- from mcp_agent.mcp.mcp_aggregator import MCPAggregator
17
-
18
- # Constants
19
- HUMAN_INPUT_TOOL_NAME = "__human_input__"
20
- CODE_STYLE = "native"
21
-
22
- HTML_ESCAPE_CHARS = {"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"}
23
-
24
-
25
- def _prepare_markdown_content(content: str, escape_xml: bool = True) -> str:
26
- """Prepare content for markdown rendering by escaping HTML/XML tags
27
- while preserving code blocks and inline code.
28
-
29
- This ensures XML/HTML tags are displayed as visible text rather than
30
- being interpreted as markup by the markdown renderer.
31
-
32
- Note: This method does not handle overlapping code blocks (e.g., if inline
33
- code appears within a fenced code block range). In practice, this is not
34
- an issue since markdown syntax doesn't support such overlapping.
35
- """
36
- if not escape_xml or not isinstance(content, str):
37
- return content
38
-
39
- protected_ranges = []
40
- import re
41
-
42
- # Protect fenced code blocks (don't escape anything inside these)
43
- code_block_pattern = r"```[\s\S]*?```"
44
- for match in re.finditer(code_block_pattern, content):
45
- protected_ranges.append((match.start(), match.end()))
46
-
47
- # Protect inline code (don't escape anything inside these)
48
- inline_code_pattern = r"(?<!`)`(?!``)[^`\n]+`(?!`)"
49
- for match in re.finditer(inline_code_pattern, content):
50
- protected_ranges.append((match.start(), match.end()))
51
-
52
- protected_ranges.sort(key=lambda x: x[0])
53
-
54
- # Build the escaped content
55
- result = []
56
- last_end = 0
57
-
58
- for start, end in protected_ranges:
59
- # Escape everything outside protected ranges
60
- unprotected_text = content[last_end:start]
61
- for char, replacement in HTML_ESCAPE_CHARS.items():
62
- unprotected_text = unprotected_text.replace(char, replacement)
63
- result.append(unprotected_text)
64
-
65
- # Keep protected ranges (code blocks) as-is
66
- result.append(content[start:end])
67
- last_end = end
68
-
69
- # Escape any remaining content after the last protected range
70
- remainder_text = content[last_end:]
71
- for char, replacement in HTML_ESCAPE_CHARS.items():
72
- remainder_text = remainder_text.replace(char, replacement)
73
- result.append(remainder_text)
74
-
75
- return "".join(result)
76
-
77
-
78
- class ConsoleDisplay:
79
- """
80
- Handles displaying formatted messages, tool calls, and results to the console.
81
- This centralizes the UI display logic used by LLM implementations.
82
- """
83
-
84
- def __init__(self, config=None) -> None:
85
- """
86
- Initialize the console display handler.
87
-
88
- Args:
89
- config: Configuration object containing display preferences
90
- """
91
- self.config = config
92
- self._markup = config.logger.enable_markup if config else True
93
- self._escape_xml = True
94
-
95
- def _render_content_smartly(self, content: str, check_markdown_markers: bool = False) -> None:
96
- """
97
- Helper method to intelligently render content based on its type.
98
-
99
- - Pure XML: Use syntax highlighting for readability
100
- - Markdown (with markers): Use markdown rendering with proper escaping
101
- - Plain text: Display as-is (when check_markdown_markers=True and no markers found)
102
-
103
- Args:
104
- content: The text content to render
105
- check_markdown_markers: If True, only use markdown rendering when markers are present
106
- """
107
- import re
108
-
109
- from rich.markdown import Markdown
110
-
111
- # Check if content appears to be primarily XML
112
- xml_pattern = r"^<[a-zA-Z_][a-zA-Z0-9_-]*[^>]*>"
113
- is_xml_content = bool(re.match(xml_pattern, content.strip())) and content.count("<") > 5
114
-
115
- if is_xml_content:
116
- # Display XML content with syntax highlighting for better readability
117
- from rich.syntax import Syntax
118
-
119
- syntax = Syntax(content, "xml", theme=CODE_STYLE, line_numbers=False)
120
- console.console.print(syntax, markup=self._markup)
121
- elif check_markdown_markers:
122
- # Check for markdown markers before deciding to use markdown rendering
123
- if any(marker in content for marker in ["##", "**", "*", "`", "---", "###"]):
124
- # Has markdown markers - render as markdown with escaping
125
- prepared_content = _prepare_markdown_content(content, self._escape_xml)
126
- md = Markdown(prepared_content, code_theme=CODE_STYLE)
127
- console.console.print(md, markup=self._markup)
128
- else:
129
- # Plain text - display as-is
130
- console.console.print(content, markup=self._markup)
131
- else:
132
- # Always treat as markdown with proper escaping
133
- prepared_content = _prepare_markdown_content(content, self._escape_xml)
134
- md = Markdown(prepared_content, code_theme=CODE_STYLE)
135
- console.console.print(md, markup=self._markup)
136
-
137
- def show_tool_result(self, result: CallToolResult, name: str | None = None) -> None:
138
- """Display a tool result in the new visual style."""
139
- if not self.config or not self.config.logger.show_tools:
140
- return
141
-
142
- # Import content helpers
143
- from mcp_agent.mcp.helpers.content_helpers import get_text, is_text_content
144
-
145
- # Use red for errors, magenta for success (keep block bright)
146
- block_color = "red" if result.isError else "magenta"
147
- text_color = "dim red" if result.isError else "dim magenta"
148
-
149
- # Analyze content to determine display format and status
150
- content = result.content
151
- if result.isError:
152
- status = "ERROR"
153
- else:
154
- # Check if it's a list with content blocks
155
- if len(content) == 0:
156
- status = "No Content"
157
- elif len(content) == 1 and is_text_content(content[0]):
158
- text_content = get_text(content[0])
159
- char_count = len(text_content) if text_content else 0
160
- status = f"Text Only {char_count} chars"
161
- else:
162
- text_count = sum(1 for item in content if is_text_content(item))
163
- if text_count == len(content):
164
- status = f"{len(content)} Text Blocks" if len(content) > 1 else "1 Text Block"
165
- else:
166
- status = (
167
- f"{len(content)} Content Blocks" if len(content) > 1 else "1 Content Block"
168
- )
169
-
170
- # Combined separator and status line
171
- left = f"[{block_color}]▎[/{block_color}][{text_color}]▶[/{text_color}]{f' [{block_color}]{name}[/{block_color}]' if name else ''}"
172
- right = f"[dim]tool result - {status}[/dim]"
173
- self._create_combined_separator_status(left, right)
174
-
175
- # Display tool result content
176
- content = result.content
177
-
178
- # Handle special case: single text content block
179
- if isinstance(content, list) and len(content) == 1 and is_text_content(content[0]):
180
- # Display just the text content directly
181
- text_content = get_text(content[0])
182
- if text_content:
183
- if self.config and self.config.logger.truncate_tools and len(text_content) > 360:
184
- text_content = text_content[:360] + "..."
185
- console.console.print(text_content, style="dim", markup=self._markup)
186
- else:
187
- console.console.print("(empty text)", style="dim", markup=self._markup)
188
- else:
189
- # Use Rich pretty printing for everything else
190
- try:
191
- import json
192
-
193
- from rich.pretty import Pretty
194
-
195
- # Try to parse as JSON for pretty printing
196
- if isinstance(content, str):
197
- json_obj = json.loads(content)
198
- else:
199
- json_obj = content
200
-
201
- # Use Rich's built-in truncation with dimmed styling
202
- if self.config and self.config.logger.truncate_tools:
203
- pretty_obj = Pretty(json_obj, max_length=10, max_string=50)
204
- else:
205
- pretty_obj = Pretty(json_obj)
206
-
207
- # Print with dim styling
208
- console.console.print(pretty_obj, style="dim", markup=self._markup)
209
-
210
- except (json.JSONDecodeError, TypeError, ValueError, AttributeError):
211
- # Fall back to string representation if not valid JSON
212
- content_str = str(content)
213
- if self.config and self.config.logger.truncate_tools and len(content_str) > 360:
214
- content_str = content_str[:360] + "..."
215
- console.console.print(content_str, style="dim", markup=self._markup)
216
-
217
- # Bottom separator (no additional info for tool results)
218
- console.console.print()
219
- console.console.print("─" * console.console.size.width, style="dim")
220
- console.console.print()
221
-
222
- def show_tool_call(
223
- self, available_tools, tool_name, tool_args, name: str | None = None
224
- ) -> None:
225
- """Display a tool call in the new visual style."""
226
- if not self.config or not self.config.logger.show_tools:
227
- return
228
-
229
- display_tool_list = self._format_tool_list(available_tools, tool_name)
230
-
231
- # Combined separator and status line
232
- left = f"[magenta]▎[/magenta][dim magenta]◀[/dim magenta]{f' [magenta]{name}[/magenta]' if name else ''}"
233
- right = f"[dim]tool request - {tool_name}[/dim]"
234
- self._create_combined_separator_status(left, right)
235
-
236
- # Display tool arguments using Rich JSON pretty printing (dimmed)
237
- try:
238
- import json
239
-
240
- from rich.pretty import Pretty
241
-
242
- # Try to parse as JSON for pretty printing
243
- if isinstance(tool_args, str):
244
- json_obj = json.loads(tool_args)
245
- else:
246
- json_obj = tool_args
247
-
248
- # Use Rich's built-in truncation with dimmed styling
249
- if self.config and self.config.logger.truncate_tools:
250
- pretty_obj = Pretty(json_obj, max_length=10, max_string=50)
251
- else:
252
- pretty_obj = Pretty(json_obj)
253
-
254
- # Print with dim styling
255
- console.console.print(pretty_obj, style="dim", markup=self._markup)
256
-
257
- except (json.JSONDecodeError, TypeError, ValueError):
258
- # Fall back to string representation if not valid JSON
259
- content = str(tool_args)
260
- if self.config and self.config.logger.truncate_tools and len(content) > 360:
261
- content = content[:360] + "..."
262
- console.console.print(content, style="dim", markup=self._markup)
263
-
264
- # Bottom separator with tool list using pipe separators (matching server style)
265
- console.console.print()
266
-
267
- # Use existing tool list formatting with pipe separators
268
- if display_tool_list and len(display_tool_list) > 0:
269
- # Truncate tool list if needed (leave space for "─| " prefix and " |" suffix)
270
- max_tool_width = console.console.size.width - 10 # Reserve space for separators
271
- truncated_tool_list = self._truncate_list_if_needed(display_tool_list, max_tool_width)
272
-
273
- # Create the separator line: ─| [tools] |──────
274
- line1 = Text()
275
- line1.append("─| ", style="dim")
276
- line1.append_text(truncated_tool_list)
277
- line1.append(" |", style="dim")
278
- remaining = console.console.size.width - line1.cell_len
279
- if remaining > 0:
280
- line1.append("─" * remaining, style="dim")
281
- else:
282
- # No tools - continuous bar
283
- line1 = Text()
284
- line1.append("─" * console.console.size.width, style="dim")
285
-
286
- console.console.print(line1, markup=self._markup)
287
- console.console.print()
288
-
289
- async def show_tool_update(self, aggregator: MCPAggregator | None, updated_server: str) -> None:
290
- """Show a tool update for a server in the new visual style."""
291
- if not self.config or not self.config.logger.show_tools:
292
- return
293
-
294
- # Check if aggregator is actually an agent (has name attribute)
295
- agent_name = None
296
- if aggregator and hasattr(aggregator, "name") and aggregator.name:
297
- agent_name = aggregator.name
298
-
299
- # Combined separator and status line
300
- if agent_name:
301
- left = (
302
- f"[magenta]▎[/magenta][dim magenta]▶[/dim magenta] [magenta]{agent_name}[/magenta]"
303
- )
304
- else:
305
- left = "[magenta]▎[/magenta][dim magenta]▶[/dim magenta]"
306
-
307
- right = f"[dim]{updated_server}[/dim]"
308
- self._create_combined_separator_status(left, right)
309
-
310
- # Display update message
311
- message = f"Updating tools for server {updated_server}"
312
- console.console.print(message, style="dim", markup=self._markup)
313
-
314
- # Bottom separator
315
- console.console.print()
316
- console.console.print("─" * console.console.size.width, style="dim")
317
- console.console.print()
318
-
319
- # Force prompt_toolkit redraw if active
320
- try:
321
- from prompt_toolkit.application.current import get_app
322
-
323
- get_app().invalidate() # Forces prompt_toolkit to redraw
324
- except: # noqa: E722
325
- pass # No active prompt_toolkit session
326
-
327
- def _format_tool_list(self, available_tools, selected_tool_name):
328
- """Format the list of available tools, highlighting the selected one."""
329
- display_tool_list = Text()
330
- matching_tools = []
331
-
332
- for display_tool in available_tools:
333
- # Handle both OpenAI and Anthropic tool formats
334
- if isinstance(display_tool, dict):
335
- if "function" in display_tool:
336
- # OpenAI format
337
- tool_call_name = display_tool["function"]["name"]
338
- else:
339
- # Anthropic format
340
- tool_call_name = display_tool["name"]
341
- else:
342
- # Handle potential object format (e.g., Pydantic models)
343
- tool_call_name = (
344
- display_tool.function.name
345
- if hasattr(display_tool, "function")
346
- else display_tool.name
347
- )
348
-
349
- parts = (
350
- tool_call_name.split(SEP)
351
- if SEP in tool_call_name
352
- else [tool_call_name, tool_call_name]
353
- )
354
-
355
- if selected_tool_name.split(SEP)[0] == parts[0]:
356
- shortened_name = parts[1] if len(parts[1]) <= 12 else parts[1][:11] + "…"
357
- matching_tools.append((shortened_name, tool_call_name))
358
-
359
- # Format with pipe separators instead of brackets
360
- for i, (shortened_name, tool_call_name) in enumerate(matching_tools):
361
- if i > 0:
362
- display_tool_list.append(" | ", style="dim")
363
- style = "magenta" if tool_call_name == selected_tool_name else "dim"
364
- display_tool_list.append(shortened_name, style)
365
-
366
- return display_tool_list
367
-
368
- def _truncate_list_if_needed(self, text_list: Text, max_width: int) -> Text:
369
- """Truncate a Text list if it exceeds the maximum width."""
370
- if text_list.cell_len <= max_width:
371
- return text_list
372
-
373
- # Create a new truncated version
374
- truncated = Text()
375
- current_width = 0
376
-
377
- for span in text_list._spans:
378
- text_part = text_list.plain[span.start : span.end]
379
- if current_width + len(text_part) <= max_width - 1: # -1 for ellipsis
380
- truncated.append(text_part, style=span.style)
381
- current_width += len(text_part)
382
- else:
383
- # Add what we can fit and ellipsis
384
- remaining = max_width - current_width - 1
385
- if remaining > 0:
386
- truncated.append(text_part[:remaining], style=span.style)
387
- truncated.append("…", style="dim")
388
- break
389
-
390
- return truncated
391
-
392
- def _create_combined_separator_status(self, left_content: str, right_info: str = "") -> None:
393
- """
394
- Create a combined separator and status line.
395
-
396
- Args:
397
- left_content: The main content (block, arrow, name) - left justified with color
398
- right_info: Supplementary information to show in brackets - right aligned
399
- """
400
- width = console.console.size.width
401
-
402
- # Create left text
403
- left_text = Text.from_markup(left_content)
404
-
405
- # Create right text if we have info
406
- if right_info and right_info.strip():
407
- # Add dim brackets around the right info
408
- right_text = Text()
409
- right_text.append("[", style="dim")
410
- right_text.append_text(Text.from_markup(right_info))
411
- right_text.append("]", style="dim")
412
- # Calculate separator count
413
- separator_count = width - left_text.cell_len - right_text.cell_len
414
- if separator_count < 1:
415
- separator_count = 1 # Always at least 1 separator
416
- else:
417
- right_text = Text("")
418
- separator_count = width - left_text.cell_len
419
-
420
- # Build the combined line
421
- combined = Text()
422
- combined.append_text(left_text)
423
- combined.append(" ", style="default")
424
- combined.append("─" * (separator_count - 1), style="dim")
425
- combined.append_text(right_text)
426
-
427
- # Print with empty line before
428
- console.console.print()
429
- console.console.print(combined, markup=self._markup)
430
- console.console.print()
431
-
432
- async def show_assistant_message(
433
- self,
434
- message_text: Union[str, Text],
435
- aggregator=None,
436
- highlight_namespaced_tool: str = "",
437
- title: str = "ASSISTANT",
438
- name: str | None = None,
439
- model: str | None = None,
440
- ) -> None:
441
- """Display an assistant message in a formatted panel."""
442
-
443
- if not self.config or not self.config.logger.show_chat:
444
- return
445
-
446
- # Build server list for bottom separator (using same logic as legacy)
447
- display_server_list = Text()
448
-
449
- if aggregator:
450
- # Add human input tool if available
451
- tools = await aggregator.list_tools()
452
- if any(tool.name == HUMAN_INPUT_TOOL_NAME for tool in tools.tools):
453
- style = (
454
- "green" if highlight_namespaced_tool == HUMAN_INPUT_TOOL_NAME else "dim white"
455
- )
456
- display_server_list.append("[human] ", style)
457
-
458
- # Add all available servers
459
- mcp_server_name = (
460
- highlight_namespaced_tool.split(SEP)[0]
461
- if SEP in highlight_namespaced_tool
462
- else highlight_namespaced_tool
463
- )
464
-
465
- for server_name in await aggregator.list_servers():
466
- style = "green" if server_name == mcp_server_name else "dim white"
467
- display_server_list.append(f"[{server_name}] ", style)
468
-
469
- # Combined separator and status line
470
- left = f"[green]▎[/green][dim green]◀[/dim green]{f' [bold green]{name}[/bold green]' if name else ''}"
471
- right = f"[dim]{model}[/dim]" if model else ""
472
- self._create_combined_separator_status(left, right)
473
-
474
- if isinstance(message_text, str):
475
- content = message_text
476
-
477
- # Try to detect and pretty print JSON
478
- try:
479
- import json
480
-
481
- json.loads(content)
482
- json = JSON(message_text)
483
- console.console.print(json, markup=self._markup)
484
- except (JSONDecodeError, TypeError, ValueError):
485
- # Use the smart rendering helper to handle XML vs Markdown
486
- self._render_content_smartly(content)
487
- else:
488
- # Handle Rich Text objects directly
489
- console.console.print(message_text, markup=self._markup)
490
-
491
- # Bottom separator with server list and diagrams
492
- console.console.print()
493
-
494
- # Check for mermaid diagrams in the message content
495
- diagrams = []
496
- if isinstance(message_text, str):
497
- diagrams = extract_mermaid_diagrams(message_text)
498
-
499
- # Create server list with pipe separators (no "mcp:" prefix)
500
- server_content = Text()
501
- if display_server_list and len(display_server_list) > 0:
502
- # Convert the existing server list to pipe-separated format
503
- servers = []
504
- if aggregator:
505
- for server_name in await aggregator.list_servers():
506
- servers.append(server_name)
507
-
508
- # Create pipe-separated server list
509
- for i, server_name in enumerate(servers):
510
- if i > 0:
511
- server_content.append(" | ", style="dim")
512
- # Highlight active server, dim inactive ones
513
- mcp_server_name = (
514
- highlight_namespaced_tool.split(SEP)[0]
515
- if SEP in highlight_namespaced_tool
516
- else highlight_namespaced_tool
517
- )
518
- style = "bright_green" if server_name == mcp_server_name else "dim"
519
- server_content.append(server_name, style)
520
-
521
- # Create main separator line
522
- line1 = Text()
523
- if server_content.cell_len > 0:
524
- line1.append("─| ", style="dim")
525
- line1.append_text(server_content)
526
- line1.append(" |", style="dim")
527
- remaining = console.console.size.width - line1.cell_len
528
- if remaining > 0:
529
- line1.append("─" * remaining, style="dim")
530
- else:
531
- # No servers - continuous bar (no break)
532
- line1.append("─" * console.console.size.width, style="dim")
533
-
534
- console.console.print(line1, markup=self._markup)
535
-
536
- # Add diagram links in panel if any diagrams found
537
- if diagrams:
538
- diagram_content = Text()
539
- # Add bullet at the beginning
540
- diagram_content.append("● ", style="dim")
541
-
542
- for i, diagram in enumerate(diagrams, 1):
543
- if i > 1:
544
- diagram_content.append(" • ", style="dim")
545
-
546
- # Generate URL
547
- url = create_mermaid_live_link(diagram.content)
548
-
549
- # Format: "1 - Title" or "1 - Flowchart" or "Diagram 1"
550
- if diagram.title:
551
- diagram_content.append(
552
- f"{i} - {diagram.title}", style=f"bright_blue link {url}"
553
- )
554
- else:
555
- # Try to detect diagram type, fallback to "Diagram N"
556
- diagram_type = detect_diagram_type(diagram.content)
557
- if diagram_type != "Diagram":
558
- diagram_content.append(
559
- f"{i} - {diagram_type}", style=f"bright_blue link {url}"
560
- )
561
- else:
562
- diagram_content.append(f"Diagram {i}", style=f"bright_blue link {url}")
563
-
564
- # Display diagrams on a simple new line (more space efficient)
565
- console.console.print()
566
- console.console.print(diagram_content, markup=self._markup)
567
-
568
- console.console.print()
569
-
570
- def show_user_message(
571
- self, message, model: str | None = None, chat_turn: int = 0, name: str | None = None
572
- ) -> None:
573
- """Display a user message in the new visual style."""
574
-
575
- if not self.config or not self.config.logger.show_chat:
576
- return
577
-
578
- # Combined separator and status line
579
- left = f"[blue]▎[/blue][dim blue]▶[/dim blue]{f' [bold blue]{name}[/bold blue]' if name else ''}"
580
-
581
- # Build right side with model and turn
582
- right_parts = []
583
- if model:
584
- right_parts.append(model)
585
- if chat_turn > 0:
586
- right_parts.append(f"turn {chat_turn}")
587
-
588
- right = f"[dim]{' '.join(right_parts)}[/dim]" if right_parts else ""
589
- self._create_combined_separator_status(left, right)
590
-
591
- # Display content as markdown if it looks like markdown, otherwise as text
592
- if isinstance(message, str):
593
- # Use the smart rendering helper to handle XML vs Markdown
594
- self._render_content_smartly(message)
595
- else:
596
- # Handle Text objects directly
597
- console.console.print(message, markup=self._markup)
598
-
599
- # Bottom separator (no server list for user messages)
600
- console.console.print()
601
- console.console.print("─" * console.console.size.width, style="dim")
602
- console.console.print()
603
-
604
- async def show_prompt_loaded(
605
- self,
606
- prompt_name: str,
607
- description: Optional[str] = None,
608
- message_count: int = 0,
609
- agent_name: Optional[str] = None,
610
- aggregator=None,
611
- arguments: Optional[dict[str, str]] = None,
612
- ) -> None:
613
- """
614
- Display information about a loaded prompt template.
615
-
616
- Args:
617
- prompt_name: The name of the prompt that was loaded (should be namespaced)
618
- description: Optional description of the prompt
619
- message_count: Number of messages added to the conversation history
620
- agent_name: Name of the agent using the prompt
621
- aggregator: Optional aggregator instance to use for server highlighting
622
- arguments: Optional dictionary of arguments passed to the prompt template
623
- """
624
- if not self.config or not self.config.logger.show_tools:
625
- return
626
-
627
- # Get server name from the namespaced prompt_name
628
- mcp_server_name = None
629
- if SEP in prompt_name:
630
- # Extract the server from the namespaced prompt name
631
- mcp_server_name = prompt_name.split(SEP)[0]
632
- elif aggregator and aggregator.server_names:
633
- # Fallback to first server if not namespaced
634
- mcp_server_name = aggregator.server_names[0]
635
-
636
- # Build the server list with highlighting
637
- display_server_list = Text()
638
- if aggregator:
639
- for server_name in await aggregator.list_servers():
640
- style = "green" if server_name == mcp_server_name else "dim white"
641
- display_server_list.append(f"[{server_name}] ", style)
642
-
643
- # Create content text
644
- content = Text()
645
- messages_phrase = f"Loaded {message_count} message{'s' if message_count != 1 else ''}"
646
- content.append(f"{messages_phrase} from template ", style="cyan italic")
647
- content.append(f"'{prompt_name}'", style="cyan bold italic")
648
-
649
- if agent_name:
650
- content.append(f" for {agent_name}", style="cyan italic")
651
-
652
- # Add template arguments if provided
653
- if arguments:
654
- content.append("\n\nArguments:", style="cyan")
655
- for key, value in arguments.items():
656
- content.append(f"\n {key}: ", style="cyan bold")
657
- content.append(value, style="white")
658
-
659
- if description:
660
- content.append("\n\n", style="default")
661
- content.append(description, style="dim white")
662
-
663
- # Create panel
664
- panel = Panel(
665
- content,
666
- title="[PROMPT LOADED]",
667
- title_align="right",
668
- style="cyan",
669
- border_style="white",
670
- padding=(1, 2),
671
- subtitle=display_server_list,
672
- subtitle_align="left",
673
- )
674
-
675
- console.console.print(panel, markup=self._markup)
676
- console.console.print("\n")
677
-
678
- def show_parallel_results(self, parallel_agent) -> None:
679
- """Display parallel agent results in a clean, organized format.
680
-
681
- Args:
682
- parallel_agent: The parallel agent containing fan_out_agents with results
683
- """
684
-
685
- from rich.text import Text
686
-
687
- if self.config and not self.config.logger.show_chat:
688
- return
689
-
690
- if not parallel_agent or not hasattr(parallel_agent, "fan_out_agents"):
691
- return
692
-
693
- # Collect results and agent information
694
- agent_results = []
695
-
696
- for agent in parallel_agent.fan_out_agents:
697
- # Get the last response text from this agent
698
- message_history = agent.message_history
699
- if not message_history:
700
- continue
701
-
702
- last_message = message_history[-1]
703
- content = last_message.last_text()
704
-
705
- # Get model name
706
- model = "unknown"
707
- if (
708
- hasattr(agent, "_llm")
709
- and agent._llm
710
- and hasattr(agent._llm, "default_request_params")
711
- ):
712
- model = getattr(agent._llm.default_request_params, "model", "unknown")
713
-
714
- # Get usage information
715
- tokens = 0
716
- tool_calls = 0
717
- if hasattr(agent, "usage_accumulator") and agent.usage_accumulator:
718
- summary = agent.usage_accumulator.get_summary()
719
- tokens = summary.get("cumulative_input_tokens", 0) + summary.get(
720
- "cumulative_output_tokens", 0
721
- )
722
- tool_calls = summary.get("cumulative_tool_calls", 0)
723
-
724
- agent_results.append(
725
- {
726
- "name": agent.name,
727
- "model": model,
728
- "content": content,
729
- "tokens": tokens,
730
- "tool_calls": tool_calls,
731
- }
732
- )
733
-
734
- if not agent_results:
735
- return
736
-
737
- # Display header
738
- console.console.print()
739
- console.console.print("[dim]Parallel execution complete[/dim]")
740
- console.console.print()
741
-
742
- # Display results for each agent
743
- for i, result in enumerate(agent_results):
744
- if i > 0:
745
- # Simple full-width separator
746
- console.console.print()
747
- console.console.print("─" * console.console.size.width, style="dim")
748
- console.console.print()
749
-
750
- # Two column header: model name (green) + usage info (dim)
751
- left = f"[green]▎[/green] [bold green]{result['model']}[/bold green]"
752
-
753
- # Build right side with tokens and tool calls if available
754
- right_parts = []
755
- if result["tokens"] > 0:
756
- right_parts.append(f"{result['tokens']:,} tokens")
757
- if result["tool_calls"] > 0:
758
- right_parts.append(f"{result['tool_calls']} tools")
759
-
760
- right = f"[dim]{' • '.join(right_parts) if right_parts else 'no usage data'}[/dim]"
761
-
762
- # Calculate padding to right-align usage info
763
- width = console.console.size.width
764
- left_text = Text.from_markup(left)
765
- right_text = Text.from_markup(right)
766
- padding = max(1, width - left_text.cell_len - right_text.cell_len)
767
-
768
- console.console.print(left + " " * padding + right, markup=self._markup)
769
- console.console.print()
770
-
771
- # Display content based on its type (check for markdown markers in parallel results)
772
- content = result["content"]
773
- self._render_content_smartly(content, check_markdown_markers=True)
774
-
775
- # Summary
776
- console.console.print()
777
- console.console.print("─" * console.console.size.width, style="dim")
778
-
779
- total_tokens = sum(result["tokens"] for result in agent_results)
780
- total_tools = sum(result["tool_calls"] for result in agent_results)
781
-
782
- summary_parts = [f"{len(agent_results)} models"]
783
- if total_tokens > 0:
784
- summary_parts.append(f"{total_tokens:,} tokens")
785
- if total_tools > 0:
786
- summary_parts.append(f"{total_tools} tools")
787
-
788
- summary_text = " • ".join(summary_parts)
789
- console.console.print(f"[dim]{summary_text}[/dim]")
790
- console.console.print()