fast-agent-mcp 0.4.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,422 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import TYPE_CHECKING, Any, Mapping
5
+
6
+ from rich.syntax import Syntax
7
+ from rich.text import Text
8
+
9
+ from fast_agent.ui import console
10
+ from fast_agent.ui.message_primitives import MESSAGE_CONFIGS, MessageType
11
+
12
+ if TYPE_CHECKING:
13
+ from mcp.types import CallToolResult
14
+
15
+ from fast_agent.mcp.skybridge import SkybridgeServerConfig
16
+ from fast_agent.ui.console_display import ConsoleDisplay
17
+
18
+
19
+ class ToolDisplay:
20
+ """Encapsulates rendering logic for tool calls and results."""
21
+
22
+ def __init__(self, display: "ConsoleDisplay") -> None:
23
+ self._display = display
24
+
25
+ @property
26
+ def _markup(self) -> bool:
27
+ return self._display._markup
28
+
29
+ def show_tool_result(
30
+ self,
31
+ result: CallToolResult,
32
+ *,
33
+ name: str | None = None,
34
+ tool_name: str | None = None,
35
+ skybridge_config: "SkybridgeServerConfig | None" = None,
36
+ timing_ms: float | None = None,
37
+ type_label: str = "tool result",
38
+ ) -> None:
39
+ """Display a tool result in the console."""
40
+ config = self._display.config
41
+ if config and not config.logger.show_tools:
42
+ return
43
+
44
+ from fast_agent.mcp.helpers.content_helpers import get_text, is_text_content
45
+
46
+ content = result.content
47
+ structured_content = getattr(result, "structuredContent", None)
48
+ has_structured = structured_content is not None
49
+
50
+ is_skybridge_tool = False
51
+ skybridge_resource_uri: str | None = None
52
+ if has_structured and tool_name and skybridge_config:
53
+ for tool_cfg in skybridge_config.tools:
54
+ if tool_cfg.tool_name == tool_name and tool_cfg.is_valid:
55
+ is_skybridge_tool = True
56
+ skybridge_resource_uri = tool_cfg.resource_uri
57
+ break
58
+
59
+ if result.isError:
60
+ status = "ERROR"
61
+ else:
62
+ if not content:
63
+ status = "No Content"
64
+ elif len(content) == 1 and is_text_content(content[0]):
65
+ text_content = get_text(content[0])
66
+ char_count = len(text_content) if text_content else 0
67
+ status = f"Text Only {char_count} chars"
68
+ else:
69
+ text_count = sum(1 for item in content if is_text_content(item))
70
+ if text_count == len(content):
71
+ status = f"{len(content)} Text Blocks" if len(content) > 1 else "1 Text Block"
72
+ else:
73
+ status = (
74
+ f"{len(content)} Content Blocks" if len(content) > 1 else "1 Content Block"
75
+ )
76
+
77
+ channel = getattr(result, "transport_channel", None)
78
+ bottom_metadata_items: list[str] = []
79
+ if channel:
80
+ if channel == "post-json":
81
+ transport_info = "HTTP (JSON-RPC)"
82
+ elif channel == "post-sse":
83
+ transport_info = "HTTP (SSE)"
84
+ elif channel == "get":
85
+ transport_info = "Legacy SSE"
86
+ elif channel == "resumption":
87
+ transport_info = "Resumption"
88
+ elif channel == "stdio":
89
+ transport_info = "STDIO"
90
+ else:
91
+ transport_info = channel.upper()
92
+
93
+ bottom_metadata_items.append(transport_info)
94
+
95
+ # Use timing from FAST_AGENT_TOOL_TIMING (passed as parameter)
96
+ if timing_ms is not None:
97
+ # Convert ms to seconds for display
98
+ timing_seconds = timing_ms / 1000.0
99
+ bottom_metadata_items.append(self._display._format_elapsed(timing_seconds))
100
+
101
+ if has_structured:
102
+ bottom_metadata_items.append("Structured ■")
103
+
104
+ bottom_metadata = bottom_metadata_items or None
105
+ right_info = f"[dim]{type_label} - {status}[/dim]"
106
+
107
+ if has_structured:
108
+ config_map = MESSAGE_CONFIGS[MessageType.TOOL_RESULT]
109
+ block_color = "red" if result.isError else config_map["block_color"]
110
+ arrow = config_map["arrow"]
111
+ arrow_style = config_map["arrow_style"]
112
+ left = f"[{block_color}]▎[/{block_color}][{arrow_style}]{arrow}[/{arrow_style}]"
113
+ if name:
114
+ name_color = block_color if not result.isError else "red"
115
+ left += f" [{name_color}]{name}[/{name_color}]"
116
+
117
+ self._display._create_combined_separator_status(left, right_info)
118
+
119
+ self._display._display_content(
120
+ content,
121
+ True,
122
+ result.isError,
123
+ MessageType.TOOL_RESULT,
124
+ check_markdown_markers=False,
125
+ )
126
+
127
+ console.console.print()
128
+ total_width = console.console.size.width
129
+
130
+ if is_skybridge_tool:
131
+ resource_label = (
132
+ f"skybridge resource: {skybridge_resource_uri}"
133
+ if skybridge_resource_uri
134
+ else "skybridge resource"
135
+ )
136
+ prefix = Text("─| ")
137
+ prefix.stylize("dim")
138
+ resource_text = Text(resource_label, style="magenta")
139
+ suffix = Text(" |")
140
+ suffix.stylize("dim")
141
+
142
+ separator_line = Text()
143
+ separator_line.append_text(prefix)
144
+ separator_line.append_text(resource_text)
145
+ separator_line.append_text(suffix)
146
+ remaining = total_width - separator_line.cell_len
147
+ if remaining > 0:
148
+ separator_line.append("─" * remaining, style="dim")
149
+ console.console.print(separator_line, markup=self._markup)
150
+ console.console.print()
151
+
152
+ json_str = json.dumps(structured_content, indent=2)
153
+ syntax_obj = Syntax(
154
+ json_str,
155
+ "json",
156
+ theme=self._display.code_style,
157
+ background_color="default",
158
+ )
159
+ console.console.print(syntax_obj, markup=self._markup)
160
+ else:
161
+ prefix = Text("─| ")
162
+ prefix.stylize("dim")
163
+ suffix = Text(" |")
164
+ suffix.stylize("dim")
165
+ available = max(0, total_width - prefix.cell_len - suffix.cell_len)
166
+
167
+ metadata_text = self._display._format_bottom_metadata(
168
+ bottom_metadata_items,
169
+ None,
170
+ config_map["highlight_color"],
171
+ max_width=available,
172
+ )
173
+
174
+ line = Text()
175
+ line.append_text(prefix)
176
+ line.append_text(metadata_text)
177
+ line.append_text(suffix)
178
+ remaining = total_width - line.cell_len
179
+ if remaining > 0:
180
+ line.append("─" * remaining, style="dim")
181
+ console.console.print(line, markup=self._markup)
182
+ console.console.print()
183
+ else:
184
+ self._display.display_message(
185
+ content=content,
186
+ message_type=MessageType.TOOL_RESULT,
187
+ name=name,
188
+ right_info=right_info,
189
+ bottom_metadata=bottom_metadata,
190
+ is_error=result.isError,
191
+ truncate_content=True,
192
+ )
193
+
194
+ def show_tool_call(
195
+ self,
196
+ tool_name: str,
197
+ tool_args: dict[str, Any] | None,
198
+ *,
199
+ bottom_items: list[str] | None = None,
200
+ highlight_index: int | None = None,
201
+ max_item_length: int | None = None,
202
+ name: str | None = None,
203
+ metadata: dict[str, Any] | None = None,
204
+ type_label: str = "tool call",
205
+ ) -> None:
206
+ """Display a tool call header and body."""
207
+ config = self._display.config
208
+ if config and not config.logger.show_tools:
209
+ return
210
+
211
+ tool_args = tool_args or {}
212
+ metadata = metadata or {}
213
+
214
+ right_info = f"[dim]{type_label} - {tool_name}[/dim]"
215
+ content: Any = tool_args
216
+ pre_content: Text | None = None
217
+ truncate_content = True
218
+
219
+ if metadata.get("variant") == "shell":
220
+ bottom_items = list()
221
+ max_item_length = 50
222
+ command = metadata.get("command") or tool_args.get("command")
223
+
224
+ command_text = Text()
225
+ if command and isinstance(command, str):
226
+ command_text.append("$ ", style="magenta")
227
+ command_text.append(command, style="white")
228
+ else:
229
+ command_text.append("$ ", style="magenta")
230
+ command_text.append("(no shell command provided)", style="dim")
231
+
232
+ content = command_text
233
+
234
+ shell_name = metadata.get("shell_name") or "shell"
235
+ shell_path = metadata.get("shell_path")
236
+ if shell_path:
237
+ bottom_items.append(str(shell_path))
238
+
239
+ right_parts: list[str] = []
240
+ if shell_path and shell_path != shell_name:
241
+ right_parts.append(f"{shell_name} ({shell_path})")
242
+ elif shell_name:
243
+ right_parts.append(shell_name)
244
+
245
+ right_info = f"[dim]{' | '.join(right_parts)}[/dim]" if right_parts else ""
246
+ truncate_content = False
247
+
248
+ working_dir_display = metadata.get("working_dir_display") or metadata.get("working_dir")
249
+ if working_dir_display:
250
+ bottom_items.append(f"cwd: {working_dir_display}")
251
+
252
+ timeout_seconds = metadata.get("timeout_seconds")
253
+ warning_interval = metadata.get("warning_interval_seconds")
254
+
255
+ if timeout_seconds and warning_interval:
256
+ bottom_items.append(
257
+ f"timeout: {timeout_seconds}s, warning every {warning_interval}s"
258
+ )
259
+
260
+ self._display.display_message(
261
+ content=content,
262
+ message_type=MessageType.TOOL_CALL,
263
+ name=name,
264
+ pre_content=pre_content,
265
+ right_info=right_info,
266
+ bottom_metadata=bottom_items,
267
+ highlight_index=highlight_index,
268
+ max_item_length=max_item_length,
269
+ truncate_content=truncate_content,
270
+ )
271
+
272
+ async def show_tool_update(self, updated_server: str, *, agent_name: str | None = None) -> None:
273
+ """Show a background tool update notification."""
274
+ config = self._display.config
275
+ if config and not config.logger.show_tools:
276
+ return
277
+
278
+ try:
279
+ from prompt_toolkit.application.current import get_app
280
+
281
+ app = get_app()
282
+ from fast_agent.ui import notification_tracker
283
+
284
+ notification_tracker.add_tool_update(updated_server)
285
+ app.invalidate()
286
+ except Exception: # noqa: BLE001
287
+ if agent_name:
288
+ left = f"[magenta]▎[/magenta][dim magenta]▶[/dim magenta] [magenta]{agent_name}[/magenta]"
289
+ else:
290
+ left = "[magenta]▎[/magenta][dim magenta]▶[/dim magenta]"
291
+
292
+ right = f"[dim]{updated_server}[/dim]"
293
+ self._display._create_combined_separator_status(left, right)
294
+
295
+ message = f"Updating tools for server {updated_server}"
296
+ console.console.print(message, style="dim", markup=self._markup)
297
+
298
+ console.console.print()
299
+ console.console.print("─" * console.console.size.width, style="dim")
300
+ console.console.print()
301
+
302
+ @staticmethod
303
+ def summarize_skybridge_configs(
304
+ configs: Mapping[str, "SkybridgeServerConfig"] | None,
305
+ ) -> tuple[list[dict[str, Any]], list[str]]:
306
+ """Convert Skybridge configs into display-ready structures."""
307
+ server_rows: list[dict[str, Any]] = []
308
+ warnings: list[str] = []
309
+ warning_seen: set[str] = set()
310
+
311
+ if not configs:
312
+ return server_rows, warnings
313
+
314
+ def add_warning(message: str) -> None:
315
+ formatted = message.strip()
316
+ if not formatted:
317
+ return
318
+ if formatted not in warning_seen:
319
+ warnings.append(formatted)
320
+ warning_seen.add(formatted)
321
+
322
+ for server_name in sorted(configs.keys()):
323
+ config = configs.get(server_name)
324
+ if not config:
325
+ continue
326
+ resources = list(config.ui_resources or [])
327
+ has_skybridge_signal = bool(
328
+ config.enabled or resources or config.tools or config.warnings
329
+ )
330
+ if not has_skybridge_signal:
331
+ continue
332
+
333
+ valid_resource_count = sum(1 for resource in resources if resource.is_skybridge)
334
+
335
+ server_rows.append(
336
+ {
337
+ "server_name": server_name,
338
+ "config": config,
339
+ "resources": resources,
340
+ "valid_resource_count": valid_resource_count,
341
+ "total_resource_count": len(resources),
342
+ "active_tools": [
343
+ {
344
+ "name": tool.display_name,
345
+ "template": str(tool.template_uri) if tool.template_uri else None,
346
+ }
347
+ for tool in config.tools
348
+ if tool.is_valid
349
+ ],
350
+ "enabled": config.enabled,
351
+ }
352
+ )
353
+
354
+ for warning in config.warnings:
355
+ message = warning.strip()
356
+ if not message:
357
+ continue
358
+ if not message.startswith(server_name):
359
+ message = f"{server_name} {message}"
360
+ add_warning(message)
361
+
362
+ return server_rows, warnings
363
+
364
+ def show_skybridge_summary(
365
+ self,
366
+ agent_name: str,
367
+ configs: Mapping[str, "SkybridgeServerConfig"] | None,
368
+ ) -> None:
369
+ """Display aggregated Skybridge status."""
370
+ server_rows, warnings = self.summarize_skybridge_configs(configs)
371
+
372
+ if not server_rows and not warnings:
373
+ return
374
+
375
+ heading = "[dim]OpenAI Apps SDK ([/dim][cyan]skybridge[/cyan][dim]) detected:[/dim]"
376
+ console.console.print()
377
+ console.console.print(heading, markup=self._markup)
378
+
379
+ if not server_rows:
380
+ console.console.print("[dim] ● none detected[/dim]", markup=self._markup)
381
+ else:
382
+ for row in server_rows:
383
+ server_name = row["server_name"]
384
+ resource_count = row["valid_resource_count"]
385
+ tool_infos = row["active_tools"]
386
+ enabled = row["enabled"]
387
+
388
+ tool_count = len(tool_infos)
389
+ tool_word = "tool" if tool_count == 1 else "tools"
390
+ resource_word = (
391
+ "skybridge resource" if resource_count == 1 else "skybridge resources"
392
+ )
393
+ tool_segment = f"[cyan]{tool_count}[/cyan][dim] {tool_word}[/dim]"
394
+ resource_segment = f"[cyan]{resource_count}[/cyan][dim] {resource_word}[/dim]"
395
+ name_style = "cyan" if enabled else "yellow"
396
+ status_suffix = "" if enabled else "[dim] (issues detected)[/dim]"
397
+
398
+ console.console.print(
399
+ f"[dim] ● [/dim][{name_style}]{server_name}[/{name_style}]{status_suffix}"
400
+ f"[dim] — [/dim]{tool_segment}[dim], [/dim]{resource_segment}",
401
+ markup=self._markup,
402
+ )
403
+
404
+ if tool_infos:
405
+ for tool in tool_infos:
406
+ template_info = (
407
+ f" [dim]({tool['template']})[/dim]" if tool["template"] else ""
408
+ )
409
+ console.console.print(
410
+ f"[dim] · [/dim]{tool['name']}{template_info}", markup=self._markup
411
+ )
412
+ else:
413
+ console.console.print("[dim] · no active tools[/dim]", markup=self._markup)
414
+
415
+ if warnings:
416
+ console.console.print()
417
+ console.console.print(
418
+ "[yellow]Warnings[/yellow]",
419
+ markup=self._markup,
420
+ )
421
+ for warning in warnings:
422
+ console.console.print(f"[yellow]- {warning}[/yellow]", markup=self._markup)
@@ -0,0 +1,204 @@
1
+ """
2
+ Utility module for displaying usage statistics in a consistent format.
3
+ Consolidates the usage display logic that was duplicated between fastagent.py and interactive_prompt.py.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from rich.console import Console
9
+
10
+
11
+ def display_usage_report(
12
+ agents: dict[str, Any], show_if_progress_disabled: bool = False, subdued_colors: bool = False
13
+ ) -> None:
14
+ """
15
+ Display a formatted table of token usage for all agents.
16
+
17
+ Args:
18
+ agents: Dictionary of agent name -> agent object
19
+ show_if_progress_disabled: If True, show even when progress display is disabled
20
+ subdued_colors: If True, use dim styling for a more subdued appearance
21
+ """
22
+ # Check if progress display is enabled (only relevant for fastagent context)
23
+ if not show_if_progress_disabled:
24
+ try:
25
+ from fast_agent import config
26
+
27
+ settings = config.get_settings()
28
+ if not settings.logger.progress_display:
29
+ return
30
+ except (ImportError, AttributeError):
31
+ # If we can't check settings, assume we should display
32
+ pass
33
+
34
+ # Collect usage data from all agents
35
+ usage_data = []
36
+ total_input = 0
37
+ total_output = 0
38
+ total_tokens = 0
39
+ total_tool_calls = 0
40
+
41
+ for agent_name, agent in agents.items():
42
+ if agent.usage_accumulator:
43
+ summary = agent.usage_accumulator.get_summary()
44
+ if summary["turn_count"] > 0:
45
+ input_tokens = summary["cumulative_input_tokens"]
46
+ output_tokens = summary["cumulative_output_tokens"]
47
+ billing_tokens = summary["cumulative_billing_tokens"]
48
+ turns = summary["turn_count"]
49
+ tool_calls = summary["cumulative_tool_calls"]
50
+
51
+ # Get context percentage for this agent
52
+ context_percentage = agent.usage_accumulator.context_usage_percentage
53
+
54
+ # Get model name via typed property when available
55
+ model = "unknown"
56
+ if agent.llm:
57
+ model = agent.llm.model_name or "unknown"
58
+
59
+ # Standardize model name truncation - use consistent 25 char width with 22+... truncation
60
+ if len(model) > 25:
61
+ model = model[:22] + "..."
62
+
63
+ usage_data.append(
64
+ {
65
+ "name": agent_name,
66
+ "model": model,
67
+ "input": input_tokens,
68
+ "output": output_tokens,
69
+ "total": billing_tokens,
70
+ "turns": turns,
71
+ "tool_calls": tool_calls,
72
+ "context": context_percentage,
73
+ }
74
+ )
75
+
76
+ total_input += input_tokens
77
+ total_output += output_tokens
78
+ total_tokens += billing_tokens
79
+ total_tool_calls += tool_calls
80
+
81
+ if not usage_data:
82
+ return
83
+
84
+ # Calculate dynamic agent column width (max 15)
85
+ max_agent_width = min(15, max(len(data["name"]) for data in usage_data) if usage_data else 8)
86
+ agent_width = max(max_agent_width, 5) # Minimum of 5 for "Agent" header
87
+
88
+ # Display the table with new visual style
89
+ console = Console()
90
+
91
+ # Top separator
92
+ console.print()
93
+ console.print("─" * console.size.width, style="dim")
94
+ console.print()
95
+
96
+ # Header with block character
97
+ console.print("[dim]▎[/dim] [bold dim]Usage Summary[/bold dim]")
98
+ console.print()
99
+
100
+ # Table header with proper spacing
101
+ console.print(
102
+ f"[dim]{'Agent':<{agent_width}} {'Input':>9} {'Output':>9} {'Total':>9} {'Turns':>6} {'Tools':>6} {'Context%':>9} {'Model':<25}[/dim]"
103
+ )
104
+
105
+ # Print agent rows - use styling based on subdued_colors flag
106
+ for data in usage_data:
107
+ input_str = f"{data['input']:,}"
108
+ output_str = f"{data['output']:,}"
109
+ total_str = f"{data['total']:,}"
110
+ turns_str = str(data["turns"])
111
+ tools_str = str(data["tool_calls"])
112
+ context_str = f"{data['context']:.1f}%" if data["context"] is not None else "-"
113
+
114
+ # Truncate agent name if needed
115
+ agent_name = data["name"]
116
+ if len(agent_name) > agent_width:
117
+ agent_name = agent_name[: agent_width - 3] + "..."
118
+
119
+ if subdued_colors:
120
+ # Original fastagent.py style with dim wrapper
121
+ console.print(
122
+ f"[dim]{agent_name:<{agent_width}} "
123
+ f"{input_str:>9} "
124
+ f"{output_str:>9} "
125
+ f"[bold]{total_str:>9}[/bold] "
126
+ f"{turns_str:>6} "
127
+ f"{tools_str:>6} "
128
+ f"{context_str:>9} "
129
+ f"{data['model']:<25}[/dim]"
130
+ )
131
+ else:
132
+ # Original interactive_prompt.py style
133
+ console.print(
134
+ f"{agent_name:<{agent_width}} "
135
+ f"{input_str:>9} "
136
+ f"{output_str:>9} "
137
+ f"[bold]{total_str:>9}[/bold] "
138
+ f"{turns_str:>6} "
139
+ f"{tools_str:>6} "
140
+ f"{context_str:>9} "
141
+ f"[dim]{data['model']:<25}[/dim]"
142
+ )
143
+
144
+ # Add total row if multiple agents
145
+ if len(usage_data) > 1:
146
+ console.print()
147
+ total_input_str = f"{total_input:,}"
148
+ total_output_str = f"{total_output:,}"
149
+ total_tokens_str = f"{total_tokens:,}"
150
+ total_tools_str = str(total_tool_calls)
151
+
152
+ if subdued_colors:
153
+ # Original fastagent.py style with dim wrapper on bold
154
+ console.print(
155
+ f"[bold dim]{'TOTAL':<{agent_width}} "
156
+ f"{total_input_str:>9} "
157
+ f"{total_output_str:>9} "
158
+ f"[bold]{total_tokens_str:>9}[/bold] "
159
+ f"{'':<6} "
160
+ f"{total_tools_str:>6} "
161
+ f"{'':<9} "
162
+ f"{'':<25}[/bold dim]"
163
+ )
164
+ else:
165
+ # Original interactive_prompt.py style
166
+ console.print(
167
+ f"[bold]{'TOTAL':<{agent_width}}[/bold] "
168
+ f"[bold]{total_input_str:>9}[/bold] "
169
+ f"[bold]{total_output_str:>9}[/bold] "
170
+ f"[bold]{total_tokens_str:>9}[/bold] "
171
+ f"{'':<6} "
172
+ f"[bold]{total_tools_str:>6}[/bold] "
173
+ f"{'':<9} "
174
+ f"{'':<25}"
175
+ )
176
+
177
+ console.print()
178
+
179
+
180
+ def collect_agents_from_provider(
181
+ prompt_provider: Any, agent_name: str | None = None
182
+ ) -> dict[str, Any]:
183
+ """
184
+ Collect agents from a prompt provider for usage display.
185
+
186
+ Args:
187
+ prompt_provider: Provider that has access to agents
188
+ agent_name: Name of the current agent (for context)
189
+
190
+ Returns:
191
+ Dictionary of agent name -> agent object
192
+ """
193
+ agents_to_show = {}
194
+
195
+ if hasattr(prompt_provider, "_agents"):
196
+ # Multi-agent app - show all agents
197
+ agents_to_show = prompt_provider._agents
198
+ elif hasattr(prompt_provider, "agent"):
199
+ # Single agent
200
+ agent = prompt_provider.agent
201
+ if hasattr(agent, "name"):
202
+ agents_to_show = {agent.name: agent}
203
+
204
+ return agents_to_show
@@ -0,0 +1,5 @@
1
+ """Utility helpers for fast-agent."""
2
+
3
+ from .time import format_duration
4
+
5
+ __all__ = ["format_duration"]