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,1400 @@
1
+ """
2
+ Enhanced prompt functionality with advanced prompt_toolkit features.
3
+ """
4
+
5
+ import asyncio
6
+ import json
7
+ import os
8
+ import shlex
9
+ import subprocess
10
+ import tempfile
11
+ from importlib.metadata import version
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ from prompt_toolkit import PromptSession
16
+ from prompt_toolkit.completion import Completer, Completion, WordCompleter
17
+ from prompt_toolkit.filters import Condition
18
+ from prompt_toolkit.formatted_text import HTML
19
+ from prompt_toolkit.history import InMemoryHistory
20
+ from prompt_toolkit.key_binding import KeyBindings
21
+ from prompt_toolkit.styles import Style
22
+ from rich import print as rich_print
23
+
24
+ from fast_agent.agents.agent_types import AgentType
25
+ from fast_agent.constants import FAST_AGENT_ERROR_CHANNEL, FAST_AGENT_REMOVED_METADATA_CHANNEL
26
+ from fast_agent.core.exceptions import PromptExitError
27
+ from fast_agent.llm.model_info import ModelInfo
28
+ from fast_agent.mcp.types import McpAgentProtocol
29
+ from fast_agent.ui.mcp_display import render_mcp_status
30
+
31
+ if TYPE_CHECKING:
32
+ from fast_agent.core.agent_app import AgentApp
33
+
34
+ # Get the application version
35
+ try:
36
+ app_version = version("fast-agent-mcp")
37
+ except: # noqa: E722
38
+ app_version = "unknown"
39
+
40
+ # Map of agent names to their history
41
+ agent_histories = {}
42
+
43
+ # Store available agents for auto-completion
44
+ available_agents = set()
45
+
46
+ # Keep track of multi-line mode state
47
+ in_multiline_mode = False
48
+
49
+
50
+ def _extract_alert_flags_from_meta(blocks) -> set[str]:
51
+ flags: set[str] = set()
52
+ for block in blocks or []:
53
+ text = getattr(block, "text", None)
54
+ if not text:
55
+ continue
56
+ try:
57
+ payload = json.loads(text)
58
+ except (TypeError, ValueError):
59
+ continue
60
+ if payload.get("type") != "fast-agent-removed":
61
+ continue
62
+ category = payload.get("category")
63
+ match category:
64
+ case "text":
65
+ flags.add("T")
66
+ case "document":
67
+ flags.add("D")
68
+ case "vision":
69
+ flags.add("V")
70
+ return flags
71
+
72
+
73
+ # Track whether help text has been shown globally
74
+ help_message_shown = False
75
+
76
+ # Track which agents have shown their info
77
+ _agent_info_shown = set()
78
+
79
+
80
+ async def show_mcp_status(agent_name: str, agent_provider: "AgentApp | None") -> None:
81
+ if agent_provider is None:
82
+ rich_print("[red]No agent provider available[/red]")
83
+ return
84
+
85
+ try:
86
+ agent = agent_provider._agent(agent_name)
87
+ except Exception as exc:
88
+ rich_print(f"[red]Unable to load agent '{agent_name}': {exc}[/red]")
89
+ return
90
+
91
+ await render_mcp_status(agent)
92
+
93
+
94
+ async def _display_agent_info_helper(agent_name: str, agent_provider: "AgentApp | None") -> None:
95
+ """Helper function to display agent information."""
96
+ # Only show once per agent
97
+ if agent_name in _agent_info_shown:
98
+ return
99
+
100
+ try:
101
+ # Get agent info from AgentApp
102
+ if agent_provider is None:
103
+ return
104
+ agent = agent_provider._agent(agent_name)
105
+
106
+ # Get counts TODO -- add this to the type library or adjust the way aggregator/reporting works
107
+ server_count = 0
108
+ if isinstance(agent, McpAgentProtocol):
109
+ server_names = agent.aggregator.server_names
110
+ server_count = len(server_names) if server_names else 0
111
+
112
+ tools_result = await agent.list_tools()
113
+ tool_count = (
114
+ len(tools_result.tools) if tools_result and hasattr(tools_result, "tools") else 0
115
+ )
116
+
117
+ resources_dict = await agent.list_resources()
118
+ resource_count = (
119
+ sum(len(resources) for resources in resources_dict.values()) if resources_dict else 0
120
+ )
121
+
122
+ prompts_dict = await agent.list_prompts()
123
+ prompt_count = sum(len(prompts) for prompts in prompts_dict.values()) if prompts_dict else 0
124
+
125
+ skill_count = 0
126
+ skill_manifests = getattr(agent, "_skill_manifests", None)
127
+ if skill_manifests:
128
+ try:
129
+ skill_count = len(list(skill_manifests))
130
+ except TypeError:
131
+ skill_count = 0
132
+
133
+ # Handle different agent types
134
+ if agent.agent_type == AgentType.PARALLEL:
135
+ # Count child agents for parallel agents
136
+ child_count = 0
137
+ if hasattr(agent, "fan_out_agents") and agent.fan_out_agents:
138
+ child_count += len(agent.fan_out_agents)
139
+ if hasattr(agent, "fan_in_agent") and agent.fan_in_agent:
140
+ child_count += 1
141
+
142
+ if child_count > 0:
143
+ child_word = "child agent" if child_count == 1 else "child agents"
144
+ rich_print(
145
+ f"[dim]Agent [/dim][blue]{agent_name}[/blue][dim]:[/dim] {child_count:,}[dim] {child_word}[/dim]"
146
+ )
147
+ elif agent.agent_type == AgentType.ROUTER:
148
+ # Count child agents for router agents
149
+ child_count = 0
150
+ if hasattr(agent, "routing_agents") and agent.routing_agents:
151
+ child_count = len(agent.routing_agents)
152
+ elif hasattr(agent, "agents") and agent.agents:
153
+ child_count = len(agent.agents)
154
+
155
+ if child_count > 0:
156
+ child_word = "child agent" if child_count == 1 else "child agents"
157
+ rich_print(
158
+ f"[dim]Agent [/dim][blue]{agent_name}[/blue][dim]:[/dim] {child_count:,}[dim] {child_word}[/dim]"
159
+ )
160
+ else:
161
+ content_parts = []
162
+
163
+ if server_count > 0:
164
+ sub_parts = []
165
+ if tool_count > 0:
166
+ tool_word = "tool" if tool_count == 1 else "tools"
167
+ sub_parts.append(f"{tool_count:,}[dim] {tool_word}[/dim]")
168
+ if prompt_count > 0:
169
+ prompt_word = "prompt" if prompt_count == 1 else "prompts"
170
+ sub_parts.append(f"{prompt_count:,}[dim] {prompt_word}[/dim]")
171
+ if resource_count > 0:
172
+ resource_word = "resource" if resource_count == 1 else "resources"
173
+ sub_parts.append(f"{resource_count:,}[dim] {resource_word}[/dim]")
174
+
175
+ server_word = "Server" if server_count == 1 else "Servers"
176
+ server_text = f"{server_count:,}[dim] MCP {server_word}[/dim]"
177
+ if sub_parts:
178
+ server_text = (
179
+ f"{server_text}[dim] ([/dim]"
180
+ + "[dim], [/dim]".join(sub_parts)
181
+ + "[dim])[/dim]"
182
+ )
183
+ content_parts.append(server_text)
184
+
185
+ if skill_count > 0:
186
+ skill_word = "skill" if skill_count == 1 else "skills"
187
+ content_parts.append(
188
+ f"{skill_count:,}[dim] {skill_word}[/dim][dim] available[/dim]"
189
+ )
190
+
191
+ if content_parts:
192
+ content = "[dim]. [/dim]".join(content_parts)
193
+ rich_print(f"[dim]Agent [/dim][blue]{agent_name}[/blue][dim]:[/dim] {content}")
194
+ # await _render_mcp_status(agent)
195
+
196
+ # Display Skybridge status (if aggregator discovered any)
197
+ try:
198
+ aggregator = agent.aggregator if isinstance(agent, McpAgentProtocol) else None
199
+ display = getattr(agent, "display", None)
200
+ if aggregator and display and hasattr(display, "show_skybridge_summary"):
201
+ skybridge_configs = await aggregator.get_skybridge_configs()
202
+ display.show_skybridge_summary(agent_name, skybridge_configs)
203
+ except Exception:
204
+ # Ignore Skybridge rendering issues to avoid interfering with startup
205
+ pass
206
+
207
+ # Mark as shown
208
+ _agent_info_shown.add(agent_name)
209
+
210
+ except Exception:
211
+ # Silently ignore errors to not disrupt the user experience
212
+ pass
213
+
214
+
215
+ async def _display_all_agents_with_hierarchy(
216
+ available_agents: list[str], agent_provider: "AgentApp | None"
217
+ ) -> None:
218
+ """Display all agents with tree structure for workflow agents."""
219
+ # Track which agents are children to avoid displaying them twice
220
+ child_agents = set()
221
+
222
+ # First pass: identify all child agents
223
+ for agent_name in available_agents:
224
+ try:
225
+ if agent_provider is None:
226
+ continue
227
+ agent = agent_provider._agent(agent_name)
228
+
229
+ if agent.agent_type == AgentType.PARALLEL:
230
+ if hasattr(agent, "fan_out_agents") and agent.fan_out_agents:
231
+ for child_agent in agent.fan_out_agents:
232
+ child_agents.add(child_agent.name)
233
+ if hasattr(agent, "fan_in_agent") and agent.fan_in_agent:
234
+ child_agents.add(agent.fan_in_agent.name)
235
+ elif agent.agent_type == AgentType.ROUTER:
236
+ if hasattr(agent, "routing_agents") and agent.routing_agents:
237
+ for child_agent in agent.routing_agents:
238
+ child_agents.add(child_agent.name)
239
+ elif hasattr(agent, "agents") and agent.agents:
240
+ for child_agent in agent.agents:
241
+ child_agents.add(child_agent.name)
242
+ except Exception:
243
+ continue
244
+
245
+ # Second pass: display agents (parents with children, standalone agents without children)
246
+ for agent_name in sorted(available_agents):
247
+ # Skip if this agent is a child of another agent
248
+ if agent_name in child_agents:
249
+ continue
250
+
251
+ try:
252
+ if agent_provider is None:
253
+ continue
254
+ agent = agent_provider._agent(agent_name)
255
+
256
+ # Display parent agent
257
+ await _display_agent_info_helper(agent_name, agent_provider)
258
+
259
+ # If it's a workflow agent, display its children
260
+ if agent.agent_type == AgentType.PARALLEL:
261
+ await _display_parallel_children(agent, agent_provider)
262
+ elif agent.agent_type == AgentType.ROUTER:
263
+ await _display_router_children(agent, agent_provider)
264
+
265
+ except Exception:
266
+ continue
267
+
268
+
269
+ async def _display_parallel_children(parallel_agent, agent_provider: "AgentApp | None") -> None:
270
+ """Display child agents of a parallel agent in tree format."""
271
+ children = []
272
+
273
+ # Collect fan-out agents
274
+ if hasattr(parallel_agent, "fan_out_agents") and parallel_agent.fan_out_agents:
275
+ for child_agent in parallel_agent.fan_out_agents:
276
+ children.append(child_agent)
277
+
278
+ # Collect fan-in agent
279
+ if hasattr(parallel_agent, "fan_in_agent") and parallel_agent.fan_in_agent:
280
+ children.append(parallel_agent.fan_in_agent)
281
+
282
+ # Display children with tree formatting
283
+ for i, child_agent in enumerate(children):
284
+ is_last = i == len(children) - 1
285
+ prefix = "└─" if is_last else "├─"
286
+ await _display_child_agent_info(child_agent, prefix, agent_provider)
287
+
288
+
289
+ async def _display_router_children(router_agent, agent_provider: "AgentApp | None") -> None:
290
+ """Display child agents of a router agent in tree format."""
291
+ children = []
292
+
293
+ # Collect routing agents
294
+ if hasattr(router_agent, "routing_agents") and router_agent.routing_agents:
295
+ children = router_agent.routing_agents
296
+ elif hasattr(router_agent, "agents") and router_agent.agents:
297
+ children = router_agent.agents
298
+
299
+ # Display children with tree formatting
300
+ for i, child_agent in enumerate(children):
301
+ is_last = i == len(children) - 1
302
+ prefix = "└─" if is_last else "├─"
303
+ await _display_child_agent_info(child_agent, prefix, agent_provider)
304
+
305
+
306
+ async def _display_child_agent_info(
307
+ child_agent, prefix: str, agent_provider: "AgentApp | None"
308
+ ) -> None:
309
+ """Display info for a child agent with tree prefix."""
310
+ try:
311
+ # Get counts for child agent
312
+ servers = await child_agent.list_servers()
313
+ server_count = len(servers) if servers else 0
314
+
315
+ tools_result = await child_agent.list_tools()
316
+ tool_count = (
317
+ len(tools_result.tools) if tools_result and hasattr(tools_result, "tools") else 0
318
+ )
319
+
320
+ resources_dict = await child_agent.list_resources()
321
+ resource_count = (
322
+ sum(len(resources) for resources in resources_dict.values()) if resources_dict else 0
323
+ )
324
+
325
+ prompts_dict = await child_agent.list_prompts()
326
+ prompt_count = sum(len(prompts) for prompts in prompts_dict.values()) if prompts_dict else 0
327
+
328
+ # Only display if child has MCP servers
329
+ if server_count > 0:
330
+ # Pluralization helpers
331
+ server_word = "Server" if server_count == 1 else "Servers"
332
+ tool_word = "tool" if tool_count == 1 else "tools"
333
+ resource_word = "resource" if resource_count == 1 else "resources"
334
+ prompt_word = "prompt" if prompt_count == 1 else "prompts"
335
+
336
+ rich_print(
337
+ f"[dim] {prefix} [/dim][blue]{child_agent.name}[/blue][dim]:[/dim] {server_count:,}[dim] MCP {server_word}, [/dim]{tool_count:,}[dim] {tool_word}, [/dim]{resource_count:,}[dim] {resource_word}, [/dim]{prompt_count:,}[dim] {prompt_word} available[/dim]"
338
+ )
339
+ else:
340
+ # Show child even without MCP servers for context
341
+ rich_print(
342
+ f"[dim] {prefix} [/dim][blue]{child_agent.name}[/blue][dim]: No MCP Servers[/dim]"
343
+ )
344
+
345
+ except Exception:
346
+ # Fallback: just show the name
347
+ rich_print(f"[dim] {prefix} [/dim][blue]{child_agent.name}[/blue]")
348
+
349
+
350
+ class AgentCompleter(Completer):
351
+ """Provide completion for agent names and common commands."""
352
+
353
+ def __init__(
354
+ self,
355
+ agents: list[str],
356
+ commands: list[str] = None,
357
+ agent_types: dict = None,
358
+ is_human_input: bool = False,
359
+ ) -> None:
360
+ self.agents = agents
361
+ # Map commands to their descriptions for better completion hints
362
+ self.commands = {
363
+ "mcp": "Show MCP server status",
364
+ "history": "Show conversation history overview (optionally another agent)",
365
+ "tools": "List available MCP Tools",
366
+ "skills": "List available Agent Skills",
367
+ "prompt": "List and choose MCP prompts, or apply specific prompt (/prompt <name>)",
368
+ "clear": "Clear history",
369
+ "clear last": "Remove the most recent message from history",
370
+ "agents": "List available agents",
371
+ "system": "Show the current system prompt",
372
+ "usage": "Show current usage statistics",
373
+ "markdown": "Show last assistant message without markdown formatting",
374
+ "save_history": "Save history; .json = MCP JSON, others = Markdown",
375
+ "load_history": "Load history from a file",
376
+ "help": "Show commands and shortcuts",
377
+ "EXIT": "Exit fast-agent, terminating any running workflows",
378
+ "STOP": "Stop this prompting session and move to next workflow step",
379
+ **(commands or {}), # Allow custom commands to be passed in
380
+ }
381
+ if is_human_input:
382
+ self.commands.pop("agents")
383
+ self.commands.pop("prompt", None) # Remove prompt command in human input mode
384
+ self.commands.pop("tools", None) # Remove tools command in human input mode
385
+ self.commands.pop("usage", None) # Remove usage command in human input mode
386
+ self.agent_types = agent_types or {}
387
+
388
+ def _complete_history_files(self, partial: str):
389
+ """Generate completions for history files (.json and .md)."""
390
+ from pathlib import Path
391
+
392
+ # Determine directory and prefix to search
393
+ if partial:
394
+ partial_path = Path(partial)
395
+ if partial.endswith("/") or partial.endswith(os.sep):
396
+ search_dir = partial_path
397
+ prefix = ""
398
+ else:
399
+ search_dir = partial_path.parent if partial_path.parent != partial_path else Path(".")
400
+ prefix = partial_path.name
401
+ else:
402
+ search_dir = Path(".")
403
+ prefix = ""
404
+
405
+ # Ensure search_dir exists
406
+ if not search_dir.exists():
407
+ return
408
+
409
+ try:
410
+ # List directory contents
411
+ for entry in sorted(search_dir.iterdir()):
412
+ name = entry.name
413
+
414
+ # Skip hidden files
415
+ if name.startswith("."):
416
+ continue
417
+
418
+ # Check if name matches prefix
419
+ if not name.lower().startswith(prefix.lower()):
420
+ continue
421
+
422
+ # Build the completion text
423
+ if search_dir == Path("."):
424
+ completion_text = name
425
+ else:
426
+ completion_text = str(search_dir / name)
427
+
428
+ # Handle directories - add trailing slash
429
+ if entry.is_dir():
430
+ yield Completion(
431
+ completion_text + "/",
432
+ start_position=-len(partial),
433
+ display=name + "/",
434
+ display_meta="directory",
435
+ )
436
+ # Handle .json and .md files
437
+ elif entry.is_file() and (name.endswith(".json") or name.endswith(".md")):
438
+ file_type = "JSON history" if name.endswith(".json") else "Markdown"
439
+ yield Completion(
440
+ completion_text,
441
+ start_position=-len(partial),
442
+ display=name,
443
+ display_meta=file_type,
444
+ )
445
+ except PermissionError:
446
+ pass # Skip directories we can't read
447
+
448
+ def get_completions(self, document, complete_event):
449
+ """Synchronous completions method - this is what prompt_toolkit expects by default"""
450
+ text = document.text_before_cursor
451
+ text_lower = text.lower()
452
+
453
+ # Sub-completion for /load_history - show .json and .md files
454
+ if text_lower.startswith("/load_history ") or text_lower.startswith("/load "):
455
+ # Extract the partial path after the command
456
+ if text_lower.startswith("/load_history "):
457
+ partial = text[len("/load_history "):]
458
+ else:
459
+ partial = text[len("/load "):]
460
+
461
+ yield from self._complete_history_files(partial)
462
+ return
463
+
464
+ # Complete commands
465
+ if text_lower.startswith("/"):
466
+ cmd = text_lower[1:]
467
+ # Simple command completion - match beginning of command
468
+ for command, description in self.commands.items():
469
+ if command.lower().startswith(cmd):
470
+ yield Completion(
471
+ command,
472
+ start_position=-len(cmd),
473
+ display=command,
474
+ display_meta=description,
475
+ )
476
+
477
+ # Complete agent names for agent-related commands
478
+ elif text.startswith("@"):
479
+ agent_name = text[1:]
480
+ for agent in self.agents:
481
+ if agent.lower().startswith(agent_name.lower()):
482
+ # Get agent type or default to "Agent"
483
+ agent_type = self.agent_types.get(agent, AgentType.BASIC).value
484
+ yield Completion(
485
+ agent,
486
+ start_position=-len(agent_name),
487
+ display=agent,
488
+ display_meta=agent_type,
489
+ )
490
+
491
+
492
+ # Helper function to open text in an external editor
493
+ def get_text_from_editor(initial_text: str = "") -> str:
494
+ """
495
+ Opens the user\'s configured editor ($VISUAL or $EDITOR) to edit the initial_text.
496
+ Falls back to \'nano\' (Unix) or \'notepad\' (Windows) if neither is set.
497
+ Returns the edited text, or the original text if an error occurs.
498
+ """
499
+ editor_cmd_str = os.environ.get("VISUAL") or os.environ.get("EDITOR")
500
+
501
+ if not editor_cmd_str:
502
+ if os.name == "nt": # Windows
503
+ editor_cmd_str = "notepad"
504
+ else: # Unix-like (Linux, macOS)
505
+ editor_cmd_str = "nano" # A common, usually available, simple editor
506
+
507
+ # Use shlex.split to handle editors with arguments (e.g., "code --wait")
508
+ try:
509
+ editor_cmd_list = shlex.split(editor_cmd_str)
510
+ if not editor_cmd_list: # Handle empty string from shlex.split
511
+ raise ValueError("Editor command string is empty or invalid.")
512
+ except ValueError as e:
513
+ rich_print(f"[red]Error: Invalid editor command string ('{editor_cmd_str}'): {e}[/red]")
514
+ return initial_text
515
+
516
+ # Create a temporary file for the editor to use.
517
+ # Using a suffix can help some editors with syntax highlighting or mode.
518
+ try:
519
+ with tempfile.NamedTemporaryFile(
520
+ mode="w+", delete=False, suffix=".txt", encoding="utf-8"
521
+ ) as tmp_file:
522
+ if initial_text:
523
+ tmp_file.write(initial_text)
524
+ tmp_file.flush() # Ensure content is written to disk before editor opens it
525
+ temp_file_path = tmp_file.name
526
+ except Exception as e:
527
+ rich_print(f"[red]Error: Could not create temporary file for editor: {e}[/red]")
528
+ return initial_text
529
+
530
+ try:
531
+ # Construct the full command: editor_parts + [temp_file_path]
532
+ # e.g., [\'vim\', \'/tmp/somefile.txt\'] or [\'code\', \'--wait\', \'/tmp/somefile.txt\']
533
+ full_cmd = editor_cmd_list + [temp_file_path]
534
+
535
+ # Run the editor. This is a blocking call.
536
+ subprocess.run(full_cmd, check=True)
537
+
538
+ # Read the content back from the temporary file.
539
+ with open(temp_file_path, "r", encoding="utf-8") as f:
540
+ edited_text = f.read()
541
+
542
+ except FileNotFoundError:
543
+ rich_print(
544
+ f"[red]Error: Editor command '{editor_cmd_list[0]}' not found. "
545
+ f"Please set $VISUAL or $EDITOR correctly, or install '{editor_cmd_list[0]}'.[/red]"
546
+ )
547
+ return initial_text
548
+ except subprocess.CalledProcessError as e:
549
+ rich_print(
550
+ f"[red]Error: Editor '{editor_cmd_list[0]}' closed with an error (code {e.returncode}).[/red]"
551
+ )
552
+ return initial_text
553
+ except Exception as e:
554
+ rich_print(
555
+ f"[red]An unexpected error occurred while launching or using the editor: {e}[/red]"
556
+ )
557
+ return initial_text
558
+ finally:
559
+ # Always attempt to clean up the temporary file.
560
+ if "temp_file_path" in locals() and os.path.exists(temp_file_path):
561
+ try:
562
+ os.remove(temp_file_path)
563
+ except Exception as e:
564
+ rich_print(
565
+ f"[yellow]Warning: Could not remove temporary file {temp_file_path}: {e}[/yellow]"
566
+ )
567
+
568
+ return edited_text.strip() # Added strip() to remove trailing newlines often added by editors
569
+
570
+
571
+ def create_keybindings(
572
+ on_toggle_multiline=None, app=None, agent_provider: "AgentApp | None" = None, agent_name=None
573
+ ):
574
+ """Create custom key bindings."""
575
+ kb = KeyBindings()
576
+
577
+ @kb.add("c-m", filter=Condition(lambda: not in_multiline_mode))
578
+ def _(event) -> None:
579
+ """Enter: accept input when not in multiline mode."""
580
+ event.current_buffer.validate_and_handle()
581
+
582
+ @kb.add("c-j", filter=Condition(lambda: not in_multiline_mode))
583
+ def _(event) -> None:
584
+ """Ctrl+J: Insert newline when in normal mode."""
585
+ event.current_buffer.insert_text("\n")
586
+
587
+ @kb.add("c-m", filter=Condition(lambda: in_multiline_mode))
588
+ def _(event) -> None:
589
+ """Enter: Insert newline when in multiline mode."""
590
+ event.current_buffer.insert_text("\n")
591
+
592
+ # Use c-j (Ctrl+J) as an alternative to represent Ctrl+Enter in multiline mode
593
+ @kb.add("c-j", filter=Condition(lambda: in_multiline_mode))
594
+ def _(event) -> None:
595
+ """Ctrl+J (equivalent to Ctrl+Enter): Submit in multiline mode."""
596
+ event.current_buffer.validate_and_handle()
597
+
598
+ @kb.add("c-t")
599
+ def _(event) -> None:
600
+ """Ctrl+T: Toggle multiline mode."""
601
+ global in_multiline_mode
602
+ in_multiline_mode = not in_multiline_mode
603
+
604
+ # Force redraw the app to update toolbar
605
+ if event.app:
606
+ event.app.invalidate()
607
+ elif app:
608
+ app.invalidate()
609
+
610
+ # Call the toggle callback if provided
611
+ if on_toggle_multiline:
612
+ on_toggle_multiline(in_multiline_mode)
613
+
614
+ # Instead of printing, we'll just update the toolbar
615
+ # The toolbar will show the current mode
616
+
617
+ @kb.add("c-l")
618
+ def _(event) -> None:
619
+ """Ctrl+L: Clear and redraw the terminal screen."""
620
+ app_ref = event.app or app
621
+ if app_ref and getattr(app_ref, "renderer", None):
622
+ app_ref.renderer.clear()
623
+ app_ref.invalidate()
624
+
625
+ @kb.add("c-u")
626
+ def _(event) -> None:
627
+ """Ctrl+U: Clear the input buffer."""
628
+ event.current_buffer.text = ""
629
+
630
+ @kb.add("c-e")
631
+ async def _(event) -> None:
632
+ """Ctrl+E: Edit current buffer in $EDITOR."""
633
+ current_text = event.app.current_buffer.text
634
+ try:
635
+ # Run the synchronous editor function in a thread
636
+ edited_text = await event.app.loop.run_in_executor(
637
+ None, get_text_from_editor, current_text
638
+ )
639
+ event.app.current_buffer.text = edited_text
640
+ # Optionally, move cursor to the end of the edited text
641
+ event.app.current_buffer.cursor_position = len(edited_text)
642
+ except asyncio.CancelledError:
643
+ rich_print("[yellow]Editor interaction cancelled.[/yellow]")
644
+ except Exception as e:
645
+ rich_print(f"[red]Error during editor interaction: {e}[/red]")
646
+ finally:
647
+ # Ensure the UI is updated
648
+ if event.app:
649
+ event.app.invalidate()
650
+
651
+ # Store reference to agent provider and agent name for clipboard functionality
652
+ kb.agent_provider = agent_provider
653
+ kb.current_agent_name = agent_name
654
+
655
+ @kb.add("c-y")
656
+ async def _(event) -> None:
657
+ """Ctrl+Y: Copy last assistant response to clipboard."""
658
+ if kb.agent_provider and kb.current_agent_name:
659
+ try:
660
+ # Get the agent from AgentApp
661
+ agent = kb.agent_provider._agent(kb.current_agent_name)
662
+
663
+ # Find last assistant message
664
+ for msg in reversed(agent.message_history):
665
+ if msg.role == "assistant":
666
+ content = msg.last_text()
667
+ import pyperclip
668
+
669
+ pyperclip.copy(content)
670
+ rich_print("\n[green]✓ Copied to clipboard[/green]")
671
+ return
672
+ except Exception:
673
+ pass
674
+
675
+ return kb
676
+
677
+
678
+ async def get_enhanced_input(
679
+ agent_name: str,
680
+ default: str = "",
681
+ show_default: bool = False,
682
+ show_stop_hint: bool = False,
683
+ multiline: bool = False,
684
+ available_agent_names: list[str] = None,
685
+ agent_types: dict[str, AgentType] = None,
686
+ is_human_input: bool = False,
687
+ toolbar_color: str = "ansiblue",
688
+ agent_provider: "AgentApp | None" = None,
689
+ ) -> str:
690
+ """
691
+ Enhanced input with advanced prompt_toolkit features.
692
+
693
+ Args:
694
+ agent_name: Name of the agent (used for prompt and history)
695
+ default: Default value if user presses enter
696
+ show_default: Whether to show the default value in the prompt
697
+ show_stop_hint: Whether to show the STOP hint
698
+ multiline: Start in multiline mode
699
+ available_agent_names: List of agent names for auto-completion
700
+ agent_types: Dictionary mapping agent names to their types for display
701
+ is_human_input: Whether this is a human input request (disables agent selection features)
702
+ toolbar_color: Color to use for the agent name in the toolbar (default: "ansiblue")
703
+ agent_provider: Optional AgentApp for displaying agent info
704
+
705
+ Returns:
706
+ User input string
707
+ """
708
+ global in_multiline_mode, available_agents, help_message_shown
709
+
710
+ # Update global state
711
+ in_multiline_mode = multiline
712
+ if available_agent_names:
713
+ available_agents = set(available_agent_names)
714
+
715
+ # Get or create history object for this agent
716
+ if agent_name not in agent_histories:
717
+ agent_histories[agent_name] = InMemoryHistory()
718
+
719
+ # Define callback for multiline toggle
720
+ def on_multiline_toggle(enabled) -> None:
721
+ nonlocal session
722
+ if hasattr(session, "app") and session.app:
723
+ session.app.invalidate()
724
+
725
+ # Define toolbar function that will update dynamically
726
+ def get_toolbar():
727
+ if in_multiline_mode:
728
+ mode_style = "ansired" # More noticeable for multiline mode
729
+ mode_text = "MULTILINE"
730
+ # toggle_text = "Normal"
731
+ else:
732
+ mode_style = "ansigreen"
733
+ mode_text = "NORMAL"
734
+ # toggle_text = "Multiline"
735
+
736
+ # No shortcut hints in the toolbar for now
737
+ shortcuts = []
738
+
739
+ # Only show relevant shortcuts based on mode
740
+ shortcuts = [(k, v) for k, v in shortcuts if v]
741
+
742
+ shortcut_text = " | ".join(f"{key}:{action}" for key, action in shortcuts)
743
+
744
+ # Resolve model name, turn counter, and TDV from the current agent if available
745
+ model_display = None
746
+ tdv_segment = None
747
+ turn_count = 0
748
+ agent = None
749
+ if agent_provider:
750
+ try:
751
+ agent = agent_provider._agent(agent_name)
752
+ except Exception as exc:
753
+ print(f"[toolbar debug] unable to resolve agent '{agent_name}': {exc}")
754
+
755
+ if agent:
756
+ for message in agent.message_history:
757
+ if message.role == "user":
758
+ turn_count += 1
759
+
760
+ # Resolve LLM reference safely (avoid assertion when unattached)
761
+ llm = None
762
+ try:
763
+ llm = agent.llm
764
+ except AssertionError:
765
+ llm = getattr(agent, "_llm", None)
766
+ except Exception as exc:
767
+ print(f"[toolbar debug] agent.llm access failed for '{agent_name}': {exc}")
768
+
769
+ model_name = None
770
+ if llm:
771
+ model_name = getattr(llm, "model_name", None)
772
+ if not model_name:
773
+ model_name = getattr(
774
+ getattr(llm, "default_request_params", None), "model", None
775
+ )
776
+
777
+ if not model_name:
778
+ model_name = getattr(agent.config, "model", None)
779
+ if not model_name and getattr(agent.config, "default_request_params", None):
780
+ model_name = getattr(agent.config.default_request_params, "model", None)
781
+ if not model_name:
782
+ context = getattr(agent, "context", None) or getattr(
783
+ agent_provider, "context", None
784
+ )
785
+ config_obj = getattr(context, "config", None) if context else None
786
+ model_name = getattr(config_obj, "default_model", None)
787
+
788
+ if model_name:
789
+ max_len = 25
790
+ model_display = (
791
+ model_name[: max_len - 1] + "…" if len(model_name) > max_len else model_name
792
+ )
793
+ else:
794
+ print(f"[toolbar debug] no model resolved for agent '{agent_name}'")
795
+ model_display = "unknown"
796
+
797
+ # Build TDV capability segment based on model database
798
+ info = None
799
+ if llm:
800
+ info = ModelInfo.from_llm(llm)
801
+ if not info and model_name:
802
+ info = ModelInfo.from_name(model_name)
803
+
804
+ # Default to text-only if info resolution fails for any reason
805
+ t, d, v = (True, False, False)
806
+ if info:
807
+ t, d, v = info.tdv_flags
808
+
809
+ # Check for alert flags in user messages
810
+ alert_flags: set[str] = set()
811
+ error_seen = False
812
+ for message in agent.message_history:
813
+ if message.channels:
814
+ if message.channels.get(FAST_AGENT_ERROR_CHANNEL):
815
+ error_seen = True
816
+ if message.role == "user" and message.channels:
817
+ meta_blocks = message.channels.get(FAST_AGENT_REMOVED_METADATA_CHANNEL, [])
818
+ alert_flags.update(_extract_alert_flags_from_meta(meta_blocks))
819
+
820
+ if error_seen and not alert_flags:
821
+ alert_flags.add("T")
822
+
823
+ def _style_flag(letter: str, supported: bool) -> str:
824
+ # Enabled uses the same color as NORMAL mode (ansigreen), disabled is dim
825
+ if letter in alert_flags:
826
+ return f"<style fg='ansired' bg='ansiblack'>{letter}</style>"
827
+
828
+ enabled_color = "ansigreen"
829
+ if supported:
830
+ return f"<style fg='{enabled_color}' bg='ansiblack'>{letter}</style>"
831
+ return f"<style fg='ansiblack' bg='ansiwhite'>{letter}</style>"
832
+
833
+ tdv_segment = f"{_style_flag('T', t)}{_style_flag('D', d)}{_style_flag('V', v)}"
834
+ else:
835
+ model_display = None
836
+ tdv_segment = None
837
+
838
+ # Build dynamic middle segments: model (in green), turn counter, and optional shortcuts
839
+ middle_segments = []
840
+ if model_display:
841
+ # Model chip + inline TDV flags
842
+ if tdv_segment:
843
+ middle_segments.append(
844
+ f"{tdv_segment} <style bg='ansigreen'>{model_display}</style>"
845
+ )
846
+ else:
847
+ middle_segments.append(f"<style bg='ansigreen'>{model_display}</style>")
848
+
849
+ # Add turn counter (formatted as 3 digits)
850
+ middle_segments.append(f"{turn_count:03d}")
851
+
852
+ if shortcut_text:
853
+ middle_segments.append(shortcut_text)
854
+ middle = " | ".join(middle_segments)
855
+
856
+ # Version/app label in green (dynamic version)
857
+ version_segment = f"fast-agent {app_version}"
858
+
859
+ # Add notifications - prioritize active events over completed ones
860
+ from fast_agent.ui import notification_tracker
861
+
862
+ notification_segment = ""
863
+
864
+ # Check for active events first (highest priority)
865
+ active_status = notification_tracker.get_active_status()
866
+ if active_status:
867
+ event_type = active_status["type"].upper()
868
+ server = active_status["server"]
869
+ notification_segment = (
870
+ f" | <style fg='ansired' bg='ansiblack'>◀ {event_type} ({server})</style>"
871
+ )
872
+ elif notification_tracker.get_count() > 0:
873
+ # Show completed events summary when no active events
874
+ counts_by_type = notification_tracker.get_counts_by_type()
875
+ total_events = sum(counts_by_type.values()) if counts_by_type else 0
876
+
877
+ if len(counts_by_type) == 1:
878
+ event_type, count = next(iter(counts_by_type.items()))
879
+ label_text = notification_tracker.format_event_label(event_type, count)
880
+ notification_segment = f" | ◀ {label_text}"
881
+ else:
882
+ summary = notification_tracker.get_summary(compact=True)
883
+ heading = "event" if total_events == 1 else "events"
884
+ notification_segment = f" | ◀ {total_events} {heading} ({summary})"
885
+
886
+ if middle:
887
+ return HTML(
888
+ f" <style fg='{toolbar_color}' bg='ansiblack'> {agent_name} </style> "
889
+ f" {middle} | <style fg='{mode_style}' bg='ansiblack'> {mode_text} </style> | "
890
+ f"{version_segment}{notification_segment}"
891
+ )
892
+ else:
893
+ return HTML(
894
+ f" <style fg='{toolbar_color}' bg='ansiblack'> {agent_name} </style> "
895
+ f"Mode: <style fg='{mode_style}' bg='ansiblack'> {mode_text} </style> | "
896
+ f"{version_segment}{notification_segment}"
897
+ )
898
+
899
+ # A more terminal-agnostic style that should work across themes
900
+ custom_style = Style.from_dict(
901
+ {
902
+ "completion-menu.completion": "bg:#ansiblack #ansigreen",
903
+ "completion-menu.completion.current": "bg:#ansiblack bold #ansigreen",
904
+ "completion-menu.meta.completion": "bg:#ansiblack #ansiblue",
905
+ "completion-menu.meta.completion.current": "bg:#ansibrightblack #ansiblue",
906
+ "bottom-toolbar": "#ansiblack bg:#ansigray",
907
+ }
908
+ )
909
+ # Create session with history and completions
910
+ session = PromptSession(
911
+ history=agent_histories[agent_name],
912
+ completer=AgentCompleter(
913
+ agents=list(available_agents) if available_agents else [],
914
+ agent_types=agent_types or {},
915
+ is_human_input=is_human_input,
916
+ ),
917
+ complete_while_typing=True,
918
+ multiline=Condition(lambda: in_multiline_mode),
919
+ complete_in_thread=True,
920
+ mouse_support=False,
921
+ bottom_toolbar=get_toolbar,
922
+ style=custom_style,
923
+ )
924
+
925
+ # Create key bindings with a reference to the app
926
+ bindings = create_keybindings(
927
+ on_toggle_multiline=on_multiline_toggle,
928
+ app=session.app,
929
+ agent_provider=agent_provider,
930
+ agent_name=agent_name,
931
+ )
932
+ session.app.key_bindings = bindings
933
+
934
+ shell_agent = None
935
+ shell_enabled = False
936
+ shell_access_modes: tuple[str, ...] = ()
937
+ shell_name: str | None = None
938
+ if agent_provider:
939
+ try:
940
+ shell_agent = agent_provider._agent(agent_name)
941
+ except Exception:
942
+ shell_agent = None
943
+
944
+ if shell_agent:
945
+ shell_enabled = bool(getattr(shell_agent, "_shell_runtime_enabled", False))
946
+ modes_attr = getattr(shell_agent, "_shell_access_modes", ())
947
+ if isinstance(modes_attr, (list, tuple)):
948
+ shell_access_modes = tuple(str(mode) for mode in modes_attr)
949
+ elif modes_attr:
950
+ shell_access_modes = (str(modes_attr),)
951
+
952
+ # Get the detected shell name from the runtime
953
+ if shell_enabled:
954
+ shell_runtime = getattr(shell_agent, "_shell_runtime", None)
955
+ if shell_runtime:
956
+ runtime_info = shell_runtime.runtime_info()
957
+ shell_name = runtime_info.get("name")
958
+
959
+ # Create formatted prompt text
960
+ arrow_segment = "<ansibrightyellow>❯</ansibrightyellow>" if shell_enabled else "❯"
961
+ prompt_text = f"<ansibrightblue>{agent_name}</ansibrightblue> {arrow_segment} "
962
+
963
+ # Add default value display if requested
964
+ if show_default and default and default != "STOP":
965
+ prompt_text = f"{prompt_text} [<ansigreen>{default}</ansigreen>] "
966
+
967
+ # Only show hints at startup if requested
968
+ if show_stop_hint:
969
+ if default == "STOP":
970
+ rich_print("Enter a prompt, [red]STOP[/red] to finish")
971
+ if default:
972
+ rich_print(f"Press <ENTER> to use the default prompt:\n[cyan]{default}[/cyan]")
973
+
974
+ # Mention available features but only on first usage globally
975
+ if not help_message_shown:
976
+ if is_human_input:
977
+ rich_print("[dim]Type /help for commands. Ctrl+T toggles multiline mode.[/dim]")
978
+ else:
979
+ rich_print(
980
+ "[dim]Type '/' for commands, '@' to switch agent. Ctrl+T multiline, CTRL+E external editor.[/dim]\n"
981
+ )
982
+
983
+ # Display agent info right after help text if agent_provider is available
984
+ if agent_provider and not is_human_input:
985
+ # Display info for all available agents with tree structure for workflows
986
+ await _display_all_agents_with_hierarchy(available_agents, agent_provider)
987
+
988
+ # Show streaming status message
989
+ if agent_provider:
990
+ # Get logger settings from the agent's context (not agent_provider)
991
+ logger_settings = None
992
+ try:
993
+ active_agent = shell_agent
994
+ if active_agent is None:
995
+ active_agent = agent_provider._agent(agent_name)
996
+ agent_context = active_agent._context or active_agent.context
997
+ logger_settings = agent_context.config.logger
998
+ except Exception:
999
+ # If we can't get the agent or its context, logger_settings stays None
1000
+ pass
1001
+
1002
+ # Only show streaming messages if chat display is enabled AND we have logger_settings
1003
+ if logger_settings:
1004
+ show_chat = getattr(logger_settings, "show_chat", True)
1005
+
1006
+ if show_chat:
1007
+ # Check for parallel agents
1008
+ has_parallel = any(
1009
+ agent.agent_type == AgentType.PARALLEL
1010
+ for agent in agent_provider._agents.values()
1011
+ )
1012
+
1013
+ # Note: streaming may have been disabled by fastagent.py if parallel agents exist
1014
+ # So we check has_parallel first to show the appropriate message
1015
+ if has_parallel:
1016
+ # Streaming is disabled due to parallel agents
1017
+ rich_print(
1018
+ "[dim]Markdown Streaming disabled (Parallel Agents configured)[/dim]"
1019
+ )
1020
+ else:
1021
+ # Check if streaming is enabled
1022
+ streaming_enabled = getattr(logger_settings, "streaming_display", True)
1023
+ streaming_mode = getattr(logger_settings, "streaming", "markdown")
1024
+ if streaming_enabled and streaming_mode != "none":
1025
+ # Streaming is enabled - notify users since it's experimental
1026
+ rich_print(
1027
+ f"[dim]Experimental: Streaming Enabled - {streaming_mode} mode[/dim]"
1028
+ )
1029
+
1030
+ # Show model source if configured via env var or config file
1031
+ model_source = getattr(agent_context.config, "model_source", None)
1032
+ if model_source:
1033
+ rich_print(f"[dim]Model selected via {model_source}[/dim]")
1034
+
1035
+ # Show HuggingFace model and provider info if applicable
1036
+ try:
1037
+ if active_agent.llm:
1038
+ get_hf_info = getattr(active_agent.llm, "get_hf_display_info", None)
1039
+ if get_hf_info:
1040
+ hf_info = get_hf_info()
1041
+ model = hf_info.get("model", "unknown")
1042
+ provider = hf_info.get("provider", "auto-routing")
1043
+ rich_print(
1044
+ f"[dim]HuggingFace: {model} via {provider}[/dim]"
1045
+ )
1046
+ except Exception:
1047
+ pass
1048
+
1049
+ if shell_enabled:
1050
+ modes_display = ", ".join(shell_access_modes or ("direct",))
1051
+ shell_display = f"{modes_display}, {shell_name}" if shell_name else modes_display
1052
+
1053
+ # Add working directory info
1054
+ shell_runtime = getattr(shell_agent, "_shell_runtime", None)
1055
+ if shell_runtime:
1056
+ working_dir = shell_runtime.working_directory()
1057
+ try:
1058
+ # Try to show relative to cwd for cleaner display
1059
+ working_dir_display = str(working_dir.relative_to(Path.cwd()))
1060
+ if working_dir_display == ".":
1061
+ # Show last 2 parts of the path (e.g., "source/fast-agent")
1062
+ parts = Path.cwd().parts
1063
+ if len(parts) >= 2:
1064
+ working_dir_display = "/".join(parts[-2:])
1065
+ elif len(parts) == 1:
1066
+ working_dir_display = parts[0]
1067
+ else:
1068
+ working_dir_display = str(Path.cwd())
1069
+ except ValueError:
1070
+ # If not relative to cwd, show absolute path
1071
+ working_dir_display = str(working_dir)
1072
+ shell_display = f"{shell_display} | cwd: {working_dir_display}"
1073
+
1074
+ rich_print(f"[yellow]Shell Access ({shell_display})[/yellow]")
1075
+
1076
+ rich_print()
1077
+ help_message_shown = True
1078
+
1079
+ # Process special commands
1080
+
1081
+ def pre_process_input(text):
1082
+ # Command processing
1083
+ if text and text.startswith("/"):
1084
+ if text == "/":
1085
+ return ""
1086
+ cmd_parts = text[1:].strip().split(maxsplit=1)
1087
+ cmd = cmd_parts[0].lower()
1088
+
1089
+ if cmd == "help":
1090
+ return "HELP"
1091
+ elif cmd == "agents":
1092
+ return "LIST_AGENTS"
1093
+ elif cmd == "system":
1094
+ return "SHOW_SYSTEM"
1095
+ elif cmd == "usage":
1096
+ return "SHOW_USAGE"
1097
+ elif cmd == "history":
1098
+ target_agent = None
1099
+ if len(cmd_parts) > 1:
1100
+ candidate = cmd_parts[1].strip()
1101
+ if candidate:
1102
+ target_agent = candidate
1103
+ return {"show_history": {"agent": target_agent}}
1104
+ elif cmd == "clear":
1105
+ target_agent = None
1106
+ if len(cmd_parts) > 1:
1107
+ remainder = cmd_parts[1].strip()
1108
+ if remainder:
1109
+ tokens = remainder.split(maxsplit=1)
1110
+ if tokens and tokens[0].lower() == "last":
1111
+ if len(tokens) > 1:
1112
+ candidate = tokens[1].strip()
1113
+ if candidate:
1114
+ target_agent = candidate
1115
+ return {"clear_last": {"agent": target_agent}}
1116
+ target_agent = remainder
1117
+ return {"clear_history": {"agent": target_agent}}
1118
+ elif cmd == "markdown":
1119
+ return "MARKDOWN"
1120
+ elif cmd in ("save_history", "save"):
1121
+ # Return a structured action for the interactive loop to handle
1122
+ # Prefer programmatic saving via HistoryExporter; fall back to magic-string there if needed
1123
+ filename = (
1124
+ cmd_parts[1].strip() if len(cmd_parts) > 1 and cmd_parts[1].strip() else None
1125
+ )
1126
+ return {"save_history": True, "filename": filename}
1127
+ elif cmd in ("load_history", "load"):
1128
+ # Return a structured action for loading history from a file
1129
+ filename = (
1130
+ cmd_parts[1].strip() if len(cmd_parts) > 1 and cmd_parts[1].strip() else None
1131
+ )
1132
+ if not filename:
1133
+ return {"load_history": True, "error": "Filename required for load_history"}
1134
+ return {"load_history": True, "filename": filename}
1135
+ elif cmd in ("mcpstatus", "mcp"):
1136
+ return {"show_mcp_status": True}
1137
+ elif cmd == "prompt":
1138
+ # Handle /prompt with no arguments as interactive mode
1139
+ if len(cmd_parts) > 1:
1140
+ # Direct prompt selection with name or number
1141
+ prompt_arg = cmd_parts[1].strip()
1142
+ # Check if it's a number (use as index) or a name (use directly)
1143
+ if prompt_arg.isdigit():
1144
+ return {"select_prompt": True, "prompt_index": int(prompt_arg)}
1145
+ else:
1146
+ return f"SELECT_PROMPT:{prompt_arg}"
1147
+ else:
1148
+ # If /prompt is used without arguments, show interactive selection
1149
+ return {"select_prompt": True, "prompt_name": None}
1150
+ elif cmd == "tools":
1151
+ # Return a dictionary with list_tools action
1152
+ return {"list_tools": True}
1153
+ elif cmd == "skills":
1154
+ return {"list_skills": True}
1155
+ elif cmd == "exit":
1156
+ return "EXIT"
1157
+ elif cmd.lower() == "stop":
1158
+ return "STOP"
1159
+
1160
+ # Agent switching
1161
+ if text and text.startswith("@"):
1162
+ return f"SWITCH:{text[1:].strip()}"
1163
+
1164
+ # Remove the # command handling completely
1165
+
1166
+ return text
1167
+
1168
+ # Get the input - using async version
1169
+ try:
1170
+ result = await session.prompt_async(HTML(prompt_text), default=default)
1171
+ return pre_process_input(result)
1172
+ except KeyboardInterrupt:
1173
+ # Handle Ctrl+C gracefully
1174
+ return "STOP"
1175
+ except EOFError:
1176
+ # Handle Ctrl+D gracefully
1177
+ return "STOP"
1178
+ except Exception as e:
1179
+ # Log and gracefully handle other exceptions
1180
+ print(f"\nInput error: {type(e).__name__}: {e}")
1181
+ return "STOP"
1182
+ finally:
1183
+ # Ensure the prompt session is properly cleaned up
1184
+ # This is especially important on Windows to prevent resource leaks
1185
+ if session.app.is_running:
1186
+ session.app.exit()
1187
+
1188
+
1189
+ async def get_selection_input(
1190
+ prompt_text: str,
1191
+ options: list[str] = None,
1192
+ default: str = None,
1193
+ allow_cancel: bool = True,
1194
+ complete_options: bool = True,
1195
+ ) -> str | None:
1196
+ """
1197
+ Display a selection prompt and return the user's selection.
1198
+
1199
+ Args:
1200
+ prompt_text: Text to display as the prompt
1201
+ options: List of valid options (for auto-completion)
1202
+ default: Default value if user presses enter
1203
+ allow_cancel: Whether to allow cancellation with empty input
1204
+ complete_options: Whether to use the options for auto-completion
1205
+
1206
+ Returns:
1207
+ Selected value, or None if cancelled
1208
+ """
1209
+ try:
1210
+ # Initialize completer if options provided and completion requested
1211
+ completer = WordCompleter(options) if options and complete_options else None
1212
+
1213
+ # Create prompt session
1214
+ prompt_session = PromptSession(completer=completer)
1215
+
1216
+ try:
1217
+ # Get user input
1218
+ selection = await prompt_session.prompt_async(prompt_text, default=default or "")
1219
+
1220
+ # Handle cancellation
1221
+ if allow_cancel and not selection.strip():
1222
+ return None
1223
+
1224
+ return selection
1225
+ finally:
1226
+ # Ensure prompt session cleanup
1227
+ if prompt_session.app.is_running:
1228
+ prompt_session.app.exit()
1229
+ except (KeyboardInterrupt, EOFError):
1230
+ return None
1231
+ except Exception as e:
1232
+ rich_print(f"\n[red]Error getting selection: {e}[/red]")
1233
+ return None
1234
+
1235
+
1236
+ async def get_argument_input(
1237
+ arg_name: str,
1238
+ description: str = None,
1239
+ required: bool = True,
1240
+ ) -> str | None:
1241
+ """
1242
+ Prompt for an argument value with formatting and help text.
1243
+
1244
+ Args:
1245
+ arg_name: Name of the argument
1246
+ description: Optional description of the argument
1247
+ required: Whether this argument is required
1248
+
1249
+ Returns:
1250
+ Input value, or None if cancelled/skipped
1251
+ """
1252
+ # Format the prompt differently based on whether it's required
1253
+ required_text = "(required)" if required else "(optional, press Enter to skip)"
1254
+
1255
+ # Show description if available
1256
+ if description:
1257
+ rich_print(f" [dim]{arg_name}: {description}[/dim]")
1258
+
1259
+ prompt_text = HTML(
1260
+ f"Enter value for <ansibrightcyan>{arg_name}</ansibrightcyan> {required_text}: "
1261
+ )
1262
+
1263
+ # Create prompt session
1264
+ prompt_session = PromptSession()
1265
+
1266
+ try:
1267
+ # Get user input
1268
+ arg_value = await prompt_session.prompt_async(prompt_text)
1269
+
1270
+ # For optional arguments, empty input means skip
1271
+ if not required and not arg_value:
1272
+ return None
1273
+
1274
+ return arg_value
1275
+ except (KeyboardInterrupt, EOFError):
1276
+ return None
1277
+ except Exception as e:
1278
+ rich_print(f"\n[red]Error getting input: {e}[/red]")
1279
+ return None
1280
+ finally:
1281
+ # Ensure prompt session cleanup
1282
+ if prompt_session.app.is_running:
1283
+ prompt_session.app.exit()
1284
+
1285
+
1286
+ async def handle_special_commands(
1287
+ command: Any, agent_app: "AgentApp | None" = None
1288
+ ) -> bool | dict[str, Any]:
1289
+ """
1290
+ Handle special input commands.
1291
+
1292
+ Args:
1293
+ command: The command to handle, can be string or dictionary
1294
+ agent_app: Optional agent app reference
1295
+
1296
+ Returns:
1297
+ True if command was handled, False if not, or a dict with action info
1298
+ """
1299
+ # Quick guard for empty or None commands
1300
+ if not command:
1301
+ return False
1302
+
1303
+ # If command is already a dictionary, it has been pre-processed
1304
+ # Just return it directly (like when /prompts converts to select_prompt dict)
1305
+ if isinstance(command, dict):
1306
+ return command
1307
+
1308
+ global agent_histories
1309
+
1310
+ # Check for special string commands
1311
+ if command == "HELP":
1312
+ rich_print("\n[bold]Available Commands:[/bold]")
1313
+ rich_print(" /help - Show this help")
1314
+ rich_print(" /agents - List available agents")
1315
+ rich_print(" /system - Show the current system prompt")
1316
+ rich_print(" /prompt <name> - Apply a specific prompt by name")
1317
+ rich_print(" /usage - Show current usage statistics")
1318
+ rich_print(" /skills - List local skills for the active agent")
1319
+ rich_print(" /history [agent_name] - Show chat history overview")
1320
+ rich_print(" /clear [agent_name] - Clear conversation history (keeps templates)")
1321
+ rich_print(" /clear last [agent_name] - Remove the most recent message from history")
1322
+ rich_print(" /markdown - Show last assistant message without markdown formatting")
1323
+ rich_print(" /mcpstatus - Show MCP server status summary for the active agent")
1324
+ rich_print(" /save_history [filename] - Save current chat history to a file")
1325
+ rich_print(
1326
+ " [dim]Tip: Use a .json extension for MCP-compatible JSON; any other extension saves Markdown.[/dim]"
1327
+ )
1328
+ rich_print(
1329
+ " [dim]Default: Timestamped filename (e.g., 25_01_15_14_30-conversation.json)[/dim]"
1330
+ )
1331
+ rich_print(" /load_history <filename> - Load chat history from a file")
1332
+ rich_print(" @agent_name - Switch to agent")
1333
+ rich_print(" STOP - Return control back to the workflow")
1334
+ rich_print(" EXIT - Exit fast-agent, terminating any running workflows")
1335
+ rich_print("\n[bold]Keyboard Shortcuts:[/bold]")
1336
+ rich_print(" Enter - Submit (normal mode) / New line (multiline mode)")
1337
+ rich_print(" Ctrl+Enter - Always submit (in any mode)")
1338
+ rich_print(" Ctrl+T - Toggle multiline mode")
1339
+ rich_print(" Ctrl+E - Edit in external editor")
1340
+ rich_print(" Ctrl+Y - Copy last assistant response to clipboard")
1341
+ rich_print(" Ctrl+L - Redraw the screen")
1342
+ rich_print(" Ctrl+U - Clear input")
1343
+ rich_print(" Up/Down - Navigate history")
1344
+ return True
1345
+
1346
+ elif isinstance(command, str) and command.upper() == "EXIT":
1347
+ raise PromptExitError("User requested to exit fast-agent session")
1348
+
1349
+ elif command == "LIST_AGENTS":
1350
+ if available_agents:
1351
+ rich_print("\n[bold]Available Agents:[/bold]")
1352
+ for agent in sorted(available_agents):
1353
+ rich_print(f" @{agent}")
1354
+ else:
1355
+ rich_print("[yellow]No agents available[/yellow]")
1356
+ return True
1357
+
1358
+ elif command == "SHOW_USAGE":
1359
+ # Return a dictionary to signal that usage should be shown
1360
+ return {"show_usage": True}
1361
+
1362
+ elif command == "SHOW_SYSTEM":
1363
+ # Return a dictionary to signal that system prompt should be shown
1364
+ return {"show_system": True}
1365
+
1366
+ elif command == "MARKDOWN":
1367
+ # Return a dictionary to signal that markdown display should be shown
1368
+ return {"show_markdown": True}
1369
+
1370
+ elif command == "SELECT_PROMPT" or (
1371
+ isinstance(command, str) and command.startswith("SELECT_PROMPT:")
1372
+ ):
1373
+ # Handle prompt selection UI
1374
+ if agent_app:
1375
+ # If it's a specific prompt, extract the name
1376
+ prompt_name = None
1377
+ if isinstance(command, str) and command.startswith("SELECT_PROMPT:"):
1378
+ prompt_name = command.split(":", 1)[1].strip()
1379
+
1380
+ # Return a dictionary with a select_prompt action to be handled by the caller
1381
+ return {"select_prompt": True, "prompt_name": prompt_name}
1382
+ else:
1383
+ rich_print(
1384
+ "[yellow]Prompt selection is not available outside of an agent context[/yellow]"
1385
+ )
1386
+ return True
1387
+
1388
+ elif isinstance(command, str) and command.startswith("SWITCH:"):
1389
+ agent_name = command.split(":", 1)[1]
1390
+ if agent_name in available_agents:
1391
+ if agent_app:
1392
+ # The parameter can be the actual agent_app or just True to enable switching
1393
+ return {"switch_agent": agent_name}
1394
+ else:
1395
+ rich_print("[yellow]Agent switching not available in this context[/yellow]")
1396
+ else:
1397
+ rich_print(f"[red]Unknown agent: {agent_name}[/red]")
1398
+ return True
1399
+
1400
+ return False