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,1050 @@
1
+ """
2
+ Slash Commands for ACP
3
+
4
+ Provides slash command support for the ACP server, allowing clients to
5
+ discover and invoke special commands with the /command syntax.
6
+
7
+ Session commands (status, tools, save, clear, load) are always available.
8
+ Agent-specific commands are queried from the current agent if it implements
9
+ ACPAwareProtocol.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import textwrap
15
+ import time
16
+ from importlib.metadata import version as get_version
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING
19
+
20
+ from acp.schema import (
21
+ AvailableCommand,
22
+ AvailableCommandInput,
23
+ UnstructuredCommandInput,
24
+ )
25
+
26
+ from fast_agent.agents.agent_types import AgentType
27
+ from fast_agent.constants import FAST_AGENT_ERROR_CHANNEL
28
+ from fast_agent.history.history_exporter import HistoryExporter
29
+ from fast_agent.interfaces import ACPAwareProtocol, AgentProtocol
30
+ from fast_agent.llm.model_info import ModelInfo
31
+ from fast_agent.mcp.helpers.content_helpers import get_text
32
+ from fast_agent.mcp.prompts.prompt_load import load_history_into_agent
33
+ from fast_agent.types.conversation_summary import ConversationSummary
34
+ from fast_agent.utils.time import format_duration
35
+
36
+ if TYPE_CHECKING:
37
+ from mcp.types import ListToolsResult, Tool
38
+
39
+ from fast_agent.core.fastagent import AgentInstance
40
+
41
+
42
+ class SlashCommandHandler:
43
+ """Handles slash command execution for ACP sessions."""
44
+
45
+ def __init__(
46
+ self,
47
+ session_id: str,
48
+ instance: AgentInstance,
49
+ primary_agent_name: str,
50
+ *,
51
+ history_exporter: type[HistoryExporter] | HistoryExporter | None = None,
52
+ client_info: dict | None = None,
53
+ client_capabilities: dict | None = None,
54
+ protocol_version: int | None = None,
55
+ session_instructions: dict[str, str] | None = None,
56
+ ):
57
+ """
58
+ Initialize the slash command handler.
59
+
60
+ Args:
61
+ session_id: The ACP session ID
62
+ instance: The agent instance for this session
63
+ primary_agent_name: Name of the primary agent
64
+ history_exporter: Optional history exporter
65
+ client_info: Client information from ACP initialize
66
+ client_capabilities: Client capabilities from ACP initialize
67
+ protocol_version: ACP protocol version
68
+ """
69
+ self.session_id = session_id
70
+ self.instance = instance
71
+ self.primary_agent_name = primary_agent_name
72
+ # Track current agent (can change via setSessionMode). Ensure it exists.
73
+ if primary_agent_name in instance.agents:
74
+ self.current_agent_name = primary_agent_name
75
+ else:
76
+ # Fallback: pick the first registered agent to enable agent-specific commands.
77
+ self.current_agent_name = next(iter(instance.agents.keys()), primary_agent_name)
78
+ self.history_exporter = history_exporter or HistoryExporter
79
+ self._created_at = time.time()
80
+ self.client_info = client_info
81
+ self.client_capabilities = client_capabilities
82
+ self.protocol_version = protocol_version
83
+ self._session_instructions = session_instructions or {}
84
+
85
+ # Session-level commands (always available, operate on current agent)
86
+ self._session_commands: dict[str, AvailableCommand] = {
87
+ "status": AvailableCommand(
88
+ name="status",
89
+ description="Show fast-agent diagnostics",
90
+ input=AvailableCommandInput(
91
+ root=UnstructuredCommandInput(hint="[system|auth|authreset]")
92
+ ),
93
+ ),
94
+ "tools": AvailableCommand(
95
+ name="tools",
96
+ description="List available tools",
97
+ input=None,
98
+ ),
99
+ "save": AvailableCommand(
100
+ name="save",
101
+ description="Save conversation history",
102
+ input=None,
103
+ ),
104
+ "clear": AvailableCommand(
105
+ name="clear",
106
+ description="Clear history (`last` for prev. turn)",
107
+ input=AvailableCommandInput(root=UnstructuredCommandInput(hint="[last]")),
108
+ ),
109
+ "load": AvailableCommand(
110
+ name="load",
111
+ description="Load conversation history from file",
112
+ input=AvailableCommandInput(root=UnstructuredCommandInput(hint="<filename>")),
113
+ ),
114
+ }
115
+
116
+ def get_available_commands(self) -> list[AvailableCommand]:
117
+ """Get combined session commands and current agent's commands."""
118
+ commands = list(self._get_allowed_session_commands().values())
119
+
120
+ # Add agent-specific commands if current agent is ACP-aware
121
+ agent = self._get_current_agent()
122
+ if isinstance(agent, ACPAwareProtocol):
123
+ for name, cmd in agent.acp_commands.items():
124
+ # Convert ACPCommand to AvailableCommand
125
+ cmd_input = None
126
+ if cmd.input_hint:
127
+ cmd_input = AvailableCommandInput(
128
+ root=UnstructuredCommandInput(hint=cmd.input_hint)
129
+ )
130
+ commands.append(
131
+ AvailableCommand(name=name, description=cmd.description, input=cmd_input)
132
+ )
133
+
134
+ return commands
135
+
136
+ def _get_allowed_session_commands(self) -> dict[str, AvailableCommand]:
137
+ """
138
+ Return session-level commands filtered by the current agent's policy.
139
+
140
+ By default, all session commands are available. ACP-aware agents can restrict
141
+ session commands (e.g. Setup/wizard flows) by defining either:
142
+ - `acp_session_commands_allowlist: set[str] | None` attribute, or
143
+ - `acp_session_commands_allowlist() -> set[str] | None` method
144
+ """
145
+ agent = self._get_current_agent()
146
+ if not isinstance(agent, ACPAwareProtocol):
147
+ return self._session_commands
148
+
149
+ allowlist = getattr(agent, "acp_session_commands_allowlist", None)
150
+ if callable(allowlist):
151
+ try:
152
+ allowlist = allowlist()
153
+ except Exception:
154
+ allowlist = None
155
+
156
+ if allowlist is None:
157
+ return self._session_commands
158
+
159
+ try:
160
+ allowset = {str(name) for name in allowlist}
161
+ except Exception:
162
+ return self._session_commands
163
+
164
+ return {name: cmd for name, cmd in self._session_commands.items() if name in allowset}
165
+
166
+ def set_current_agent(self, agent_name: str) -> None:
167
+ """
168
+ Update the current agent for this session.
169
+
170
+ This is called when the user switches modes via setSessionMode.
171
+
172
+ Args:
173
+ agent_name: Name of the agent to use for slash commands
174
+ """
175
+ self.current_agent_name = agent_name
176
+
177
+ def _get_current_agent(self) -> AgentProtocol | None:
178
+ """Return the current agent or None if it does not exist."""
179
+ return self.instance.agents.get(self.current_agent_name)
180
+
181
+ def _get_current_agent_or_error(
182
+ self,
183
+ heading: str,
184
+ missing_template: str | None = None,
185
+ ) -> tuple[AgentProtocol | None, str | None]:
186
+ """
187
+ Return the current agent or an error response string if it is missing.
188
+
189
+ Args:
190
+ heading: Heading for the error message.
191
+ missing_template: Optional custom missing-agent message.
192
+ """
193
+ agent = self._get_current_agent()
194
+ if agent:
195
+ return agent, None
196
+
197
+ message = (
198
+ missing_template or f"Agent '{self.current_agent_name}' not found for this session."
199
+ )
200
+ return None, "\n".join([heading, "", message])
201
+
202
+ def is_slash_command(self, prompt_text: str) -> bool:
203
+ """Check if the prompt text is a slash command."""
204
+ return prompt_text.strip().startswith("/")
205
+
206
+ def parse_command(self, prompt_text: str) -> tuple[str, str]:
207
+ """
208
+ Parse a slash command into command name and arguments.
209
+
210
+ Args:
211
+ prompt_text: The full prompt text starting with /
212
+
213
+ Returns:
214
+ Tuple of (command_name, arguments)
215
+ """
216
+ text = prompt_text.strip()
217
+ if not text.startswith("/"):
218
+ return "", text
219
+
220
+ # Remove leading slash
221
+ text = text[1:]
222
+
223
+ # Split on first whitespace
224
+ command_name, _, arguments = text.partition(" ")
225
+ arguments = arguments.lstrip()
226
+
227
+ return command_name, arguments
228
+
229
+ async def execute_command(self, command_name: str, arguments: str) -> str:
230
+ """
231
+ Execute a slash command and return the response.
232
+
233
+ Args:
234
+ command_name: Name of the command to execute
235
+ arguments: Arguments passed to the command
236
+
237
+ Returns:
238
+ The command response as a string
239
+ """
240
+ # Check session-level commands first (filtered by agent policy)
241
+ allowed_session_commands = self._get_allowed_session_commands()
242
+ if command_name in allowed_session_commands:
243
+ if command_name == "status":
244
+ return await self._handle_status(arguments)
245
+ if command_name == "tools":
246
+ return await self._handle_tools()
247
+ if command_name == "save":
248
+ return await self._handle_save(arguments)
249
+ if command_name == "clear":
250
+ return await self._handle_clear(arguments)
251
+ if command_name == "load":
252
+ return await self._handle_load(arguments)
253
+
254
+ # Check agent-specific commands
255
+ agent = self._get_current_agent()
256
+ if isinstance(agent, ACPAwareProtocol):
257
+ agent_commands = agent.acp_commands
258
+ if command_name in agent_commands:
259
+ return await agent_commands[command_name].handler(arguments)
260
+
261
+ # Unknown command
262
+ available = self.get_available_commands()
263
+ return f"Unknown command: /{command_name}\n\nAvailable commands:\n" + "\n".join(
264
+ f" /{cmd.name} - {cmd.description}" for cmd in available
265
+ )
266
+
267
+ async def _handle_status(self, arguments: str | None = None) -> str:
268
+ """Handle the /status command."""
269
+ # Check for subcommands
270
+ normalized = (arguments or "").strip().lower()
271
+ if normalized == "system":
272
+ return self._handle_status_system()
273
+ if normalized == "auth":
274
+ return self._handle_status_auth()
275
+ if normalized == "authreset":
276
+ return self._handle_status_authreset()
277
+
278
+ # Get fast-agent version
279
+ try:
280
+ fa_version = get_version("fast-agent-mcp")
281
+ except Exception:
282
+ fa_version = "unknown"
283
+
284
+ # Get model information from current agent (not primary)
285
+ agent = self._get_current_agent()
286
+
287
+ # Check if this is a PARALLEL agent
288
+ is_parallel_agent = (
289
+ agent and hasattr(agent, "agent_type") and agent.agent_type == AgentType.PARALLEL
290
+ )
291
+
292
+ # For non-parallel agents, extract standard model info
293
+ model_name = "unknown"
294
+ model_provider = "unknown"
295
+ model_provider_display = "unknown"
296
+ context_window = "unknown"
297
+ capabilities_line = "Capabilities: unknown"
298
+
299
+ if agent and not is_parallel_agent and agent.llm:
300
+ model_info = ModelInfo.from_llm(agent.llm)
301
+ if model_info:
302
+ model_name = model_info.name
303
+ model_provider = str(model_info.provider.value)
304
+ model_provider_display = getattr(
305
+ model_info.provider, "display_name", model_provider
306
+ )
307
+ if model_info.context_window:
308
+ context_window = f"{model_info.context_window} tokens"
309
+ capability_parts = []
310
+ if model_info.supports_text:
311
+ capability_parts.append("Text")
312
+ if model_info.supports_document:
313
+ capability_parts.append("Document")
314
+ if model_info.supports_vision:
315
+ capability_parts.append("Vision")
316
+ if capability_parts:
317
+ capabilities_line = f"Capabilities: {', '.join(capability_parts)}"
318
+
319
+ # Get conversation statistics
320
+ summary_stats = self._get_conversation_stats(agent)
321
+
322
+ # Format the status response
323
+ status_lines = [
324
+ "# fast-agent ACP status",
325
+ "",
326
+ "## Version",
327
+ f"fast-agent-mcp: {fa_version} - https://fast-agent.ai/",
328
+ "",
329
+ ]
330
+
331
+ # Add client information if available
332
+ if self.client_info or self.client_capabilities:
333
+ status_lines.extend(["## Client Information", ""])
334
+
335
+ if self.client_info:
336
+ client_name = self.client_info.get("name", "unknown")
337
+ client_version = self.client_info.get("version", "unknown")
338
+ client_title = self.client_info.get("title")
339
+
340
+ if client_title:
341
+ status_lines.append(f"Client: {client_title} ({client_name})")
342
+ else:
343
+ status_lines.append(f"Client: {client_name}")
344
+ status_lines.append(f"Client Version: {client_version}")
345
+
346
+ if self.protocol_version:
347
+ status_lines.append(f"ACP Protocol Version: {self.protocol_version}")
348
+
349
+ if self.client_capabilities:
350
+ # Filesystem capabilities
351
+ if "fs" in self.client_capabilities:
352
+ fs_caps = self.client_capabilities["fs"]
353
+ if fs_caps:
354
+ for key, value in fs_caps.items():
355
+ status_lines.append(f" - {key}: {value}")
356
+
357
+ # Terminal capability
358
+ if "terminal" in self.client_capabilities:
359
+ status_lines.append(f" - Terminal: {self.client_capabilities['terminal']}")
360
+
361
+ # Meta capabilities
362
+ if "_meta" in self.client_capabilities:
363
+ meta_caps = self.client_capabilities["_meta"]
364
+ if meta_caps:
365
+ status_lines.append("Meta:")
366
+ for key, value in meta_caps.items():
367
+ status_lines.append(f" - {key}: {value}")
368
+
369
+ status_lines.append("")
370
+
371
+ # Build model section based on agent type
372
+ if is_parallel_agent:
373
+ # Special handling for PARALLEL agents
374
+ status_lines.append("## Active Models (Parallel Mode)")
375
+ status_lines.append("")
376
+
377
+ # Display fan-out agents
378
+ if hasattr(agent, "fan_out_agents") and agent.fan_out_agents:
379
+ status_lines.append(f"### Fan-Out Agents ({len(agent.fan_out_agents)})")
380
+ for idx, fan_out_agent in enumerate(agent.fan_out_agents, 1):
381
+ agent_name = getattr(fan_out_agent, "name", f"agent-{idx}")
382
+ status_lines.append(f"**{idx}. {agent_name}**")
383
+
384
+ # Get model info for this fan-out agent
385
+ if fan_out_agent.llm:
386
+ model_info = ModelInfo.from_llm(fan_out_agent.llm)
387
+ if model_info:
388
+ provider_display = getattr(
389
+ model_info.provider, "display_name", str(model_info.provider.value)
390
+ )
391
+ status_lines.append(f" - Provider: {provider_display}")
392
+ status_lines.append(f" - Model: {model_info.name}")
393
+ if model_info.context_window:
394
+ status_lines.append(
395
+ f" - Context Window: {model_info.context_window} tokens"
396
+ )
397
+ else:
398
+ status_lines.append(" - Model: unknown")
399
+
400
+ status_lines.append("")
401
+ else:
402
+ status_lines.append("Fan-Out Agents: none configured")
403
+ status_lines.append("")
404
+
405
+ # Display fan-in agent
406
+ if hasattr(agent, "fan_in_agent") and agent.fan_in_agent:
407
+ fan_in_agent = agent.fan_in_agent
408
+ fan_in_name = getattr(fan_in_agent, "name", "aggregator")
409
+ status_lines.append(f"### Fan-In Agent: {fan_in_name}")
410
+
411
+ # Get model info for fan-in agent
412
+ if fan_in_agent.llm:
413
+ model_info = ModelInfo.from_llm(fan_in_agent.llm)
414
+ if model_info:
415
+ provider_display = getattr(
416
+ model_info.provider, "display_name", str(model_info.provider.value)
417
+ )
418
+ status_lines.append(f" - Provider: {provider_display}")
419
+ status_lines.append(f" - Model: {model_info.name}")
420
+ if model_info.context_window:
421
+ status_lines.append(
422
+ f" - Context Window: {model_info.context_window} tokens"
423
+ )
424
+ else:
425
+ status_lines.append(" - Model: unknown")
426
+
427
+ status_lines.append("")
428
+ else:
429
+ status_lines.append("Fan-In Agent: none configured")
430
+ status_lines.append("")
431
+
432
+ else:
433
+ # Standard single-model display
434
+ provider_line = f"{model_provider}"
435
+ if model_provider_display != "unknown":
436
+ provider_line = f"{model_provider_display} ({model_provider})"
437
+
438
+ # For HuggingFace, add the routing provider info
439
+ if agent and agent.llm:
440
+ get_hf_info = getattr(agent.llm, "get_hf_display_info", None)
441
+ if callable(get_hf_info):
442
+ hf_info = get_hf_info()
443
+ hf_provider = hf_info.get("provider", "auto-routing")
444
+ provider_line = f"{model_provider_display} ({model_provider}) / {hf_provider}"
445
+
446
+ status_lines.extend(
447
+ [
448
+ "## Active Model",
449
+ f"- Provider: {provider_line}",
450
+ f"- Model: {model_name}",
451
+ f"- Context Window: {context_window}",
452
+ f"- {capabilities_line}",
453
+ "",
454
+ ]
455
+ )
456
+
457
+ # Add conversation statistics
458
+ status_lines.append(
459
+ f"## Conversation Statistics ({getattr(agent, 'name', self.current_agent_name) if agent else 'Unknown'})"
460
+ )
461
+
462
+ uptime_seconds = max(time.time() - self._created_at, 0.0)
463
+ status_lines.extend(summary_stats)
464
+ status_lines.extend(["", f"ACP Agent Uptime: {format_duration(uptime_seconds)}"])
465
+ status_lines.extend(["", "## Error Handling"])
466
+ status_lines.extend(self._get_error_handling_report(agent))
467
+
468
+ return "\n".join(status_lines)
469
+
470
+ def _handle_status_system(self) -> str:
471
+ """Handle the /status system command to show the system prompt."""
472
+ heading = "# system prompt"
473
+
474
+ agent, error = self._get_current_agent_or_error(heading)
475
+ if error:
476
+ return error
477
+
478
+ # Get the system prompt from the agent's instruction attribute
479
+ system_prompt = self._session_instructions.get(
480
+ getattr(agent, "name", self.current_agent_name), getattr(agent, "instruction", None)
481
+ )
482
+ if not system_prompt:
483
+ return "\n".join(
484
+ [
485
+ heading,
486
+ "",
487
+ "No system prompt available for this agent.",
488
+ ]
489
+ )
490
+
491
+ # Format the response
492
+ agent_name = getattr(agent, "name", self.current_agent_name)
493
+ lines = [
494
+ heading,
495
+ "",
496
+ f"**Agent:** {agent_name}",
497
+ "",
498
+ system_prompt,
499
+ ]
500
+
501
+ return "\n".join(lines)
502
+
503
+ def _handle_status_auth(self) -> str:
504
+ """Handle the /status auth command to show permissions from auths.md."""
505
+ heading = "# permissions"
506
+ auths_path = Path("./.fast-agent/auths.md")
507
+ resolved_path = auths_path.resolve()
508
+
509
+ if not auths_path.exists():
510
+ return "\n".join(
511
+ [
512
+ heading,
513
+ "",
514
+ "No permissions set",
515
+ "",
516
+ f"Path: `{resolved_path}`",
517
+ ]
518
+ )
519
+
520
+ try:
521
+ content = auths_path.read_text(encoding="utf-8")
522
+ return "\n".join(
523
+ [
524
+ heading,
525
+ "",
526
+ content.strip() if content.strip() else "No permissions set",
527
+ "",
528
+ f"Path: `{resolved_path}`",
529
+ ]
530
+ )
531
+ except Exception as exc:
532
+ return "\n".join(
533
+ [
534
+ heading,
535
+ "",
536
+ f"Failed to read permissions file: {exc}",
537
+ "",
538
+ f"Path: `{resolved_path}`",
539
+ ]
540
+ )
541
+
542
+ def _handle_status_authreset(self) -> str:
543
+ """Handle the /status authreset command to remove the auths.md file."""
544
+ heading = "# reset permissions"
545
+ auths_path = Path("./.fast-agent/auths.md")
546
+ resolved_path = auths_path.resolve()
547
+
548
+ if not auths_path.exists():
549
+ return "\n".join(
550
+ [
551
+ heading,
552
+ "",
553
+ "No permissions file exists.",
554
+ "",
555
+ f"Path: `{resolved_path}`",
556
+ ]
557
+ )
558
+
559
+ try:
560
+ auths_path.unlink()
561
+ return "\n".join(
562
+ [
563
+ heading,
564
+ "",
565
+ "Permissions file removed successfully.",
566
+ "",
567
+ f"Path: `{resolved_path}`",
568
+ ]
569
+ )
570
+ except Exception as exc:
571
+ return "\n".join(
572
+ [
573
+ heading,
574
+ "",
575
+ f"Failed to remove permissions file: {exc}",
576
+ "",
577
+ f"Path: `{resolved_path}`",
578
+ ]
579
+ )
580
+
581
+ async def _handle_tools(self) -> str:
582
+ """List available tools for the current agent."""
583
+ heading = "# tools"
584
+
585
+ agent, error = self._get_current_agent_or_error(heading)
586
+ if error:
587
+ return error
588
+
589
+ if not isinstance(agent, AgentProtocol):
590
+ return "\n".join(
591
+ [
592
+ heading,
593
+ "",
594
+ "This agent does not support tool listing.",
595
+ ]
596
+ )
597
+
598
+ try:
599
+ tools_result: "ListToolsResult" = await agent.list_tools()
600
+ except Exception as exc:
601
+ return "\n".join(
602
+ [
603
+ heading,
604
+ "",
605
+ "Failed to fetch tools from the agent.",
606
+ f"Details: {exc}",
607
+ ]
608
+ )
609
+
610
+ tools = tools_result.tools if tools_result else None
611
+ if not tools:
612
+ return "\n".join(
613
+ [
614
+ heading,
615
+ "",
616
+ "No MCP tools available for this agent.",
617
+ ]
618
+ )
619
+
620
+ lines = [heading, ""]
621
+ for index, tool in enumerate(tools, start=1):
622
+ lines.extend(self._format_tool_lines(tool, index))
623
+ lines.append("")
624
+
625
+ return "\n".join(lines).strip()
626
+
627
+ def _format_tool_lines(self, tool: "Tool", index: int) -> list[str]:
628
+ """
629
+ Convert a Tool into markdown-friendly lines.
630
+
631
+ We avoid fragile getattr usage by relying on the typed attributes
632
+ provided by mcp.types.Tool. Additional guards are added for optional fields.
633
+ """
634
+ lines: list[str] = []
635
+
636
+ meta = tool.meta or {}
637
+ name = tool.name or "unnamed"
638
+ title = (tool.title or "").strip()
639
+
640
+ header = f"{index}. **{name}**"
641
+ if title:
642
+ header = f"{header} - {title}"
643
+ if meta.get("openai/skybridgeEnabled"):
644
+ header = f"{header} _(skybridge)_"
645
+ lines.append(header)
646
+
647
+ description = (tool.description or "").strip()
648
+ if description:
649
+ wrapped = textwrap.wrap(description, width=92)
650
+ if wrapped:
651
+ indent = " "
652
+ lines.extend(f"{indent}{desc_line}" for desc_line in wrapped[:6])
653
+ if len(wrapped) > 6:
654
+ lines.append(f"{indent}...")
655
+
656
+ args_line = self._format_tool_arguments(tool)
657
+ if args_line:
658
+ lines.append(f" - Args: {args_line}")
659
+
660
+ template = meta.get("openai/skybridgeTemplate")
661
+ if template:
662
+ lines.append(f" - Template: `{template}`")
663
+
664
+ return lines
665
+
666
+ def _format_tool_arguments(self, tool: "Tool") -> str | None:
667
+ """Render tool input schema fields as inline-code argument list."""
668
+ schema = tool.inputSchema if isinstance(tool.inputSchema, dict) else None
669
+ if not schema:
670
+ return None
671
+
672
+ properties = schema.get("properties")
673
+ if not isinstance(properties, dict) or not properties:
674
+ return None
675
+
676
+ required_raw = schema.get("required", [])
677
+ required = set(required_raw) if isinstance(required_raw, list) else set()
678
+
679
+ args: list[str] = []
680
+ for prop_name in properties.keys():
681
+ suffix = "*" if prop_name in required else ""
682
+ args.append(f"`{prop_name}{suffix}`")
683
+
684
+ return ", ".join(args) if args else None
685
+
686
+ async def _handle_save(self, arguments: str | None = None) -> str:
687
+ """Handle the /save command by persisting conversation history."""
688
+ heading = "# save conversation"
689
+
690
+ agent, error = self._get_current_agent_or_error(
691
+ heading,
692
+ missing_template=f"Unable to locate agent '{self.current_agent_name}' for this session.",
693
+ )
694
+ if error:
695
+ return error
696
+
697
+ filename = arguments.strip() if arguments and arguments.strip() else None
698
+
699
+ try:
700
+ saved_path = await self.history_exporter.save(agent, filename)
701
+ except Exception as exc:
702
+ return "\n".join(
703
+ [
704
+ heading,
705
+ "",
706
+ "Failed to save conversation history.",
707
+ f"Details: {exc}",
708
+ ]
709
+ )
710
+
711
+ return "\n".join(
712
+ [
713
+ heading,
714
+ "",
715
+ "Conversation history saved successfully.",
716
+ f"Filename: `{saved_path}`",
717
+ ]
718
+ )
719
+
720
+ async def _handle_load(self, arguments: str | None = None) -> str:
721
+ """Handle the /load command by loading conversation history from a file."""
722
+ heading = "# load conversation"
723
+
724
+ agent, error = self._get_current_agent_or_error(
725
+ heading,
726
+ missing_template=f"Unable to locate agent '{self.current_agent_name}' for this session.",
727
+ )
728
+ if error:
729
+ return error
730
+
731
+ filename = arguments.strip() if arguments and arguments.strip() else None
732
+
733
+ if not filename:
734
+ return "\n".join(
735
+ [
736
+ heading,
737
+ "",
738
+ "Filename required for /load command.",
739
+ "Usage: /load <filename>",
740
+ ]
741
+ )
742
+
743
+ file_path = Path(filename)
744
+ if not file_path.exists():
745
+ return "\n".join(
746
+ [
747
+ heading,
748
+ "",
749
+ f"File not found: `{filename}`",
750
+ ]
751
+ )
752
+
753
+ try:
754
+ load_history_into_agent(agent, file_path)
755
+ except Exception as exc:
756
+ return "\n".join(
757
+ [
758
+ heading,
759
+ "",
760
+ "Failed to load conversation history.",
761
+ f"Details: {exc}",
762
+ ]
763
+ )
764
+
765
+ message_count = len(agent.message_history) if hasattr(agent, "message_history") else 0
766
+
767
+ return "\n".join(
768
+ [
769
+ heading,
770
+ "",
771
+ "Conversation history loaded successfully.",
772
+ f"Filename: `{filename}`",
773
+ f"Messages: {message_count}",
774
+ ]
775
+ )
776
+
777
+ async def _handle_clear(self, arguments: str | None = None) -> str:
778
+ """Handle /clear and /clear last commands."""
779
+ normalized = (arguments or "").strip().lower()
780
+ if normalized == "last":
781
+ return self._handle_clear_last()
782
+ return self._handle_clear_all()
783
+
784
+ def _handle_clear_all(self) -> str:
785
+ """Clear the entire conversation history."""
786
+ heading = "# clear conversation"
787
+ agent, error = self._get_current_agent_or_error(
788
+ heading,
789
+ missing_template=f"Unable to locate agent '{self.current_agent_name}' for this session.",
790
+ )
791
+ if error:
792
+ return error
793
+
794
+ try:
795
+ history = getattr(agent, "message_history", None)
796
+ original_count = len(history) if isinstance(history, list) else None
797
+
798
+ cleared = False
799
+ clear_method = getattr(agent, "clear", None)
800
+ if callable(clear_method):
801
+ clear_method()
802
+ cleared = True
803
+ elif isinstance(history, list):
804
+ history.clear()
805
+ cleared = True
806
+ except Exception as exc:
807
+ return "\n".join(
808
+ [
809
+ heading,
810
+ "",
811
+ "Failed to clear conversation history.",
812
+ f"Details: {exc}",
813
+ ]
814
+ )
815
+
816
+ if not cleared:
817
+ return "\n".join(
818
+ [
819
+ heading,
820
+ "",
821
+ "Agent does not expose a clear() method or message history list.",
822
+ ]
823
+ )
824
+
825
+ removed_text = (
826
+ f"Removed {original_count} message(s)." if isinstance(original_count, int) else ""
827
+ )
828
+
829
+ response_lines = [
830
+ heading,
831
+ "",
832
+ "Conversation history cleared.",
833
+ ]
834
+
835
+ if removed_text:
836
+ response_lines.append(removed_text)
837
+
838
+ return "\n".join(response_lines)
839
+
840
+ def _handle_clear_last(self) -> str:
841
+ """Remove the most recent conversation message."""
842
+ heading = "# clear last conversation turn"
843
+ agent, error = self._get_current_agent_or_error(
844
+ heading,
845
+ missing_template=f"Unable to locate agent '{self.current_agent_name}' for this session.",
846
+ )
847
+ if error:
848
+ return error
849
+
850
+ try:
851
+ removed = None
852
+ pop_method = getattr(agent, "pop_last_message", None)
853
+ if callable(pop_method):
854
+ removed = pop_method()
855
+ else:
856
+ history = getattr(agent, "message_history", None)
857
+ if isinstance(history, list) and history:
858
+ removed = history.pop()
859
+ except Exception as exc:
860
+ return "\n".join(
861
+ [
862
+ heading,
863
+ "",
864
+ "Failed to remove the last message.",
865
+ f"Details: {exc}",
866
+ ]
867
+ )
868
+
869
+ if removed is None:
870
+ return "\n".join(
871
+ [
872
+ heading,
873
+ "",
874
+ "No messages available to remove.",
875
+ ]
876
+ )
877
+
878
+ role = getattr(removed, "role", "message")
879
+ return "\n".join(
880
+ [
881
+ heading,
882
+ "",
883
+ f"Removed last {role} message.",
884
+ ]
885
+ )
886
+
887
+ def _get_conversation_stats(self, agent) -> list[str]:
888
+ """Get conversation statistics from the agent's message history."""
889
+ if not agent or not hasattr(agent, "message_history"):
890
+ return [
891
+ "- Turns: 0",
892
+ "- Tool Calls: 0",
893
+ "- Context Used: 0%",
894
+ ]
895
+
896
+ try:
897
+ # Create a conversation summary from message history
898
+ summary = ConversationSummary(messages=agent.message_history)
899
+
900
+ # Calculate turns (user + assistant message pairs)
901
+ turns = min(summary.user_message_count, summary.assistant_message_count)
902
+
903
+ # Get tool call statistics
904
+ tool_calls = summary.tool_calls
905
+ tool_errors = summary.tool_errors
906
+ tool_successes = summary.tool_successes
907
+ context_usage_line = self._context_usage_line(summary, agent)
908
+
909
+ stats = [
910
+ f"- Turns: {turns}",
911
+ f"- Messages: {summary.message_count} (user: {summary.user_message_count}, assistant: {summary.assistant_message_count})",
912
+ f"- Tool Calls: {tool_calls} (successes: {tool_successes}, errors: {tool_errors})",
913
+ context_usage_line,
914
+ ]
915
+
916
+ # Add timing information if available
917
+ if summary.total_elapsed_time_ms > 0:
918
+ stats.append(
919
+ f"- Total LLM Time: {format_duration(summary.total_elapsed_time_ms / 1000)}"
920
+ )
921
+
922
+ if summary.conversation_span_ms > 0:
923
+ span_seconds = summary.conversation_span_ms / 1000
924
+ stats.append(
925
+ f"- Conversation Runtime (LLM + tools): {format_duration(span_seconds)}"
926
+ )
927
+
928
+ # Add tool breakdown if there were tool calls
929
+ if tool_calls > 0 and summary.tool_call_map:
930
+ stats.append("")
931
+ stats.append("### Tool Usage Breakdown")
932
+ for tool_name, count in sorted(
933
+ summary.tool_call_map.items(), key=lambda x: x[1], reverse=True
934
+ ):
935
+ stats.append(f" - {tool_name}: {count}")
936
+
937
+ return stats
938
+
939
+ except Exception as e:
940
+ return [
941
+ "- Turns: error",
942
+ "- Tool Calls: error",
943
+ f"- Context Used: error ({e})",
944
+ ]
945
+
946
+ def _get_error_handling_report(self, agent, max_entries: int = 3) -> list[str]:
947
+ """Summarize error channel availability and recent entries."""
948
+ channel_label = f"Error Channel: {FAST_AGENT_ERROR_CHANNEL}"
949
+ if not agent or not hasattr(agent, "message_history"):
950
+ return ["_No errors recorded_"]
951
+
952
+ recent_entries: list[str] = []
953
+ history = getattr(agent, "message_history", []) or []
954
+
955
+ for message in reversed(history):
956
+ channels = getattr(message, "channels", None) or {}
957
+ channel_blocks = channels.get(FAST_AGENT_ERROR_CHANNEL)
958
+ if not channel_blocks:
959
+ continue
960
+
961
+ for block in channel_blocks:
962
+ text = get_text(block)
963
+ if text:
964
+ cleaned = text.replace("\n", " ").strip()
965
+ if cleaned:
966
+ recent_entries.append(cleaned)
967
+ else:
968
+ # Truncate long content (e.g., base64 image data)
969
+ block_str = str(block)
970
+ if len(block_str) > 60:
971
+ recent_entries.append(f"{block_str[:60]}... ({len(block_str)} characters)")
972
+ else:
973
+ recent_entries.append(block_str)
974
+ if len(recent_entries) >= max_entries:
975
+ break
976
+ if len(recent_entries) >= max_entries:
977
+ break
978
+
979
+ if recent_entries:
980
+ lines = [channel_label, "Recent Entries:"]
981
+ lines.extend(f"- {entry}" for entry in recent_entries)
982
+ return lines
983
+
984
+ return ["_No errors recorded_"]
985
+
986
+ def _context_usage_line(self, summary: ConversationSummary, agent) -> str:
987
+ """Generate a context usage line with token estimation and fallbacks."""
988
+ # Prefer usage accumulator when available (matches enhanced/interactive prompt display)
989
+ usage = getattr(agent, "usage_accumulator", None)
990
+ if usage:
991
+ window = usage.context_window_size
992
+ tokens = usage.current_context_tokens
993
+ pct = usage.context_usage_percentage
994
+ if window and pct is not None:
995
+ return f"- Context Used: {min(pct, 100.0):.1f}% (~{tokens:,} tokens of {window:,})"
996
+ if tokens:
997
+ return f"- Context Used: ~{tokens:,} tokens (window unknown)"
998
+
999
+ # Fallback to tokenizing the actual conversation text
1000
+ token_count, char_count = self._estimate_tokens(summary, agent)
1001
+
1002
+ model_info = ModelInfo.from_llm(agent.llm) if getattr(agent, "llm", None) else None
1003
+ if model_info and model_info.context_window:
1004
+ percentage = (
1005
+ (token_count / model_info.context_window) * 100
1006
+ if model_info.context_window
1007
+ else 0.0
1008
+ )
1009
+ percentage = min(percentage, 100.0)
1010
+ return f"- Context Used: {percentage:.1f}% (~{token_count:,} tokens of {model_info.context_window:,})"
1011
+
1012
+ token_text = f"~{token_count:,} tokens" if token_count else "~0 tokens"
1013
+ return f"- Context Used: {char_count:,} chars ({token_text} est.)"
1014
+
1015
+ def _estimate_tokens(self, summary: ConversationSummary, agent) -> tuple[int, int]:
1016
+ """Estimate tokens and return (tokens, characters) for the conversation history."""
1017
+ text_parts: list[str] = []
1018
+ for message in summary.messages:
1019
+ for content in getattr(message, "content", []) or []:
1020
+ text = get_text(content)
1021
+ if text:
1022
+ text_parts.append(text)
1023
+
1024
+ combined = "\n".join(text_parts)
1025
+ char_count = len(combined)
1026
+ if not combined:
1027
+ return 0, 0
1028
+
1029
+ model_name = None
1030
+ llm = getattr(agent, "llm", None)
1031
+ if llm:
1032
+ model_name = getattr(llm, "model_name", None)
1033
+
1034
+ token_count = self._count_tokens_with_tiktoken(combined, model_name)
1035
+ return token_count, char_count
1036
+
1037
+ def _count_tokens_with_tiktoken(self, text: str, model_name: str | None) -> int:
1038
+ """Try to count tokens with tiktoken; fall back to a rough chars/4 estimate."""
1039
+ try:
1040
+ import tiktoken
1041
+
1042
+ if model_name:
1043
+ encoding = tiktoken.encoding_for_model(model_name)
1044
+ else:
1045
+ encoding = tiktoken.get_encoding("cl100k_base")
1046
+
1047
+ return len(encoding.encode(text))
1048
+ except Exception:
1049
+ # Rough heuristic: ~4 characters per token (matches default bytes/token constant)
1050
+ return max(1, (len(text) + 3) // 4)