legate 0.1.0

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 (317) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +345 -0
  4. data/bin/legate +13 -0
  5. data/examples/00_quickstart.rb +51 -0
  6. data/examples/01_simple_agent.rb +105 -0
  7. data/examples/02_multi_tool_agent.rb +140 -0
  8. data/examples/03_custom_tool.rb +93 -0
  9. data/examples/04_agent_instructions.rb +84 -0
  10. data/examples/05_state_and_sessions.rb +91 -0
  11. data/examples/06_callbacks.rb +186 -0
  12. data/examples/07_async_jobs.rb +112 -0
  13. data/examples/08_loop_agent.rb +197 -0
  14. data/examples/09_sequential_workflow.rb +40 -0
  15. data/examples/10_parallel_workflow.rb +34 -0
  16. data/examples/11_agent_delegation.rb +24 -0
  17. data/examples/12_http_client_tool.rb +156 -0
  18. data/examples/13_authentication.rb +220 -0
  19. data/examples/14_mcp_client.rb +154 -0
  20. data/examples/15_mcp_server.rb +79 -0
  21. data/examples/16_webhooks.rb +91 -0
  22. data/examples/README_sequential_agents.md +164 -0
  23. data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
  24. data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
  25. data/examples/advanced/auth/excon_middleware.rb +317 -0
  26. data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
  27. data/examples/advanced/auth/fiber_auth_example.rb +281 -0
  28. data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
  29. data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
  30. data/examples/advanced/auth/oauth2_auth.rb +419 -0
  31. data/examples/advanced/auth/oidc_auth.rb +514 -0
  32. data/examples/advanced/auth/openweather_api.rb +251 -0
  33. data/examples/advanced/auth/openweather_tool.rb +153 -0
  34. data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
  35. data/examples/advanced/auth/service_account.rb +135 -0
  36. data/examples/advanced/auth/test_with_httpbin.rb +202 -0
  37. data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
  38. data/examples/advanced/callback_monitoring.rb +679 -0
  39. data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
  40. data/examples/advanced/mas/loop_workflow.rb +28 -0
  41. data/examples/advanced/mas/mock_planner.rb +77 -0
  42. data/examples/advanced/mas/proper_delegation_example.rb +276 -0
  43. data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
  44. data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
  45. data/examples/advanced/mcp/mcp_server_async.rb +76 -0
  46. data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
  47. data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
  48. data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
  49. data/examples/advanced/random_calculator.rb +104 -0
  50. data/examples/advanced/sleep_agent.rb +153 -0
  51. data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
  52. data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
  53. data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
  54. data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
  55. data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
  56. data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
  57. data/examples/tools/oauth2_example.rb +136 -0
  58. data/examples/tools/sleepy_tool.rb +42 -0
  59. data/lib/legate/activity_log.rb +71 -0
  60. data/lib/legate/agent.rb +959 -0
  61. data/lib/legate/agent_code_generator.rb +185 -0
  62. data/lib/legate/agent_definition.rb +812 -0
  63. data/lib/legate/agentic/decision.rb +49 -0
  64. data/lib/legate/agentic/loop.rb +134 -0
  65. data/lib/legate/agentic.rb +5 -0
  66. data/lib/legate/agents/loop_agent.rb +248 -0
  67. data/lib/legate/agents/parallel_agent.rb +163 -0
  68. data/lib/legate/agents/sequential_agent.rb +190 -0
  69. data/lib/legate/agents.rb +14 -0
  70. data/lib/legate/auth/config.rb +148 -0
  71. data/lib/legate/auth/coordinator.rb +218 -0
  72. data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
  73. data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
  74. data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
  75. data/lib/legate/auth/credential.rb +157 -0
  76. data/lib/legate/auth/encryption.rb +108 -0
  77. data/lib/legate/auth/error.rb +94 -0
  78. data/lib/legate/auth/exchanged_credential.rb +180 -0
  79. data/lib/legate/auth/excon_middleware.rb +285 -0
  80. data/lib/legate/auth/http_client_utils.rb +364 -0
  81. data/lib/legate/auth/manager.rb +531 -0
  82. data/lib/legate/auth/manager_store.rb +394 -0
  83. data/lib/legate/auth/middleware_factory.rb +290 -0
  84. data/lib/legate/auth/runner.rb +279 -0
  85. data/lib/legate/auth/scheme.rb +125 -0
  86. data/lib/legate/auth/schemes/api_key.rb +212 -0
  87. data/lib/legate/auth/schemes/google_service_account.rb +108 -0
  88. data/lib/legate/auth/schemes/http_bearer.rb +98 -0
  89. data/lib/legate/auth/schemes/oauth2.rb +396 -0
  90. data/lib/legate/auth/schemes/openid_connect.rb +346 -0
  91. data/lib/legate/auth/schemes/service_account.rb +388 -0
  92. data/lib/legate/auth/schemes.rb +40 -0
  93. data/lib/legate/auth/token_manager.rb +362 -0
  94. data/lib/legate/auth/token_store.rb +86 -0
  95. data/lib/legate/auth/tool_context_extension.rb +97 -0
  96. data/lib/legate/auth/tool_integration.rb +188 -0
  97. data/lib/legate/auth/url_guard.rb +81 -0
  98. data/lib/legate/auth.rb +453 -0
  99. data/lib/legate/callbacks/callback_context.rb +71 -0
  100. data/lib/legate/cli/agent_commands.rb +950 -0
  101. data/lib/legate/cli/auth_commands.rb +520 -0
  102. data/lib/legate/cli/base_command.rb +24 -0
  103. data/lib/legate/cli/deployment_commands.rb +934 -0
  104. data/lib/legate/cli/output_helper.rb +108 -0
  105. data/lib/legate/cli/session_commands.rb +138 -0
  106. data/lib/legate/cli/skaffold_commands.rb +223 -0
  107. data/lib/legate/cli/tool_commands.rb +261 -0
  108. data/lib/legate/cli/web_commands.rb +182 -0
  109. data/lib/legate/cli.rb +40 -0
  110. data/lib/legate/configuration/webhooks.rb +113 -0
  111. data/lib/legate/configuration.rb +39 -0
  112. data/lib/legate/definition_store.rb +23 -0
  113. data/lib/legate/errors.rb +118 -0
  114. data/lib/legate/event.rb +161 -0
  115. data/lib/legate/gemini_ai_beta_patch.rb +39 -0
  116. data/lib/legate/generators/agent_generator.rb +412 -0
  117. data/lib/legate/generators/code_validator.rb +48 -0
  118. data/lib/legate/generators/legate/install_generator.rb +35 -0
  119. data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
  120. data/lib/legate/generators/legate/templates/initializer.rb +18 -0
  121. data/lib/legate/generators/runtime_tool_loader.rb +76 -0
  122. data/lib/legate/generators/tool_generator.rb +408 -0
  123. data/lib/legate/generators.rb +11 -0
  124. data/lib/legate/global_definition_registry.rb +506 -0
  125. data/lib/legate/global_tool_manager.rb +135 -0
  126. data/lib/legate/llm/adapter.rb +69 -0
  127. data/lib/legate/llm/gemini.rb +172 -0
  128. data/lib/legate/llm/ollama.rb +80 -0
  129. data/lib/legate/llm.rb +34 -0
  130. data/lib/legate/mcp/client.rb +320 -0
  131. data/lib/legate/mcp/connection/sse.rb +292 -0
  132. data/lib/legate/mcp/connection/stdio.rb +273 -0
  133. data/lib/legate/mcp/connection_manager.rb +103 -0
  134. data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
  135. data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
  136. data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
  137. data/lib/legate/mcp/tool_wrapper.rb +138 -0
  138. data/lib/legate/mcp/util/schema_converter.rb +134 -0
  139. data/lib/legate/mcp.rb +23 -0
  140. data/lib/legate/plan_executor.rb +375 -0
  141. data/lib/legate/planner.rb +839 -0
  142. data/lib/legate/rails/railtie.rb +43 -0
  143. data/lib/legate/rails.rb +9 -0
  144. data/lib/legate/redaction.rb +32 -0
  145. data/lib/legate/session.rb +299 -0
  146. data/lib/legate/session_service/active_record.rb +300 -0
  147. data/lib/legate/session_service/base.rb +68 -0
  148. data/lib/legate/session_service/event_broadcast.rb +74 -0
  149. data/lib/legate/session_service/in_memory.rb +188 -0
  150. data/lib/legate/tool/metadata_dsl.rb +122 -0
  151. data/lib/legate/tool.rb +276 -0
  152. data/lib/legate/tool_code_generator.rb +103 -0
  153. data/lib/legate/tool_context.rb +350 -0
  154. data/lib/legate/tool_loader.rb +39 -0
  155. data/lib/legate/tool_registry.rb +73 -0
  156. data/lib/legate/tool_result.rb +61 -0
  157. data/lib/legate/tools/agent_tool.rb +187 -0
  158. data/lib/legate/tools/base/http_client.rb +319 -0
  159. data/lib/legate/tools/base/safe_url.rb +56 -0
  160. data/lib/legate/tools/base_async_job_tool.rb +91 -0
  161. data/lib/legate/tools/calculator.rb +89 -0
  162. data/lib/legate/tools/cat_facts.rb +81 -0
  163. data/lib/legate/tools/check_job_status_tool.rb +48 -0
  164. data/lib/legate/tools/current_time_tool.rb +64 -0
  165. data/lib/legate/tools/echo.rb +43 -0
  166. data/lib/legate/tools/http_request_tool.rb +105 -0
  167. data/lib/legate/tools/random_number_tool.rb +64 -0
  168. data/lib/legate/tools/read_webpage_tool.rb +92 -0
  169. data/lib/legate/tools/sleepy_tool.rb +74 -0
  170. data/lib/legate/tools/webhook_tool.rb +146 -0
  171. data/lib/legate/version.rb +5 -0
  172. data/lib/legate/web/app.rb +984 -0
  173. data/lib/legate/web/public/css/main.css +4980 -0
  174. data/lib/legate/web/public/images/favicon-256.png +0 -0
  175. data/lib/legate/web/public/images/favicon-32.png +0 -0
  176. data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
  177. data/lib/legate/web/public/images/legate-logo-light.png +0 -0
  178. data/lib/legate/web/public/js/legate.js +616 -0
  179. data/lib/legate/web/public/styles/main.scss +4402 -0
  180. data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
  181. data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
  182. data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
  183. data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
  184. data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
  185. data/lib/legate/web/routes/api_routes.rb +56 -0
  186. data/lib/legate/web/routes/authentication_routes.rb +1541 -0
  187. data/lib/legate/web/routes/core_routes.rb +111 -0
  188. data/lib/legate/web/routes/documentation_routes.rb +220 -0
  189. data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
  190. data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
  191. data/lib/legate/web/sass_compiler.rb +73 -0
  192. data/lib/legate/web/views/_active_session_info.slim +25 -0
  193. data/lib/legate/web/views/_activity_list.slim +55 -0
  194. data/lib/legate/web/views/_agent_card.slim +56 -0
  195. data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
  196. data/lib/legate/web/views/_agent_status_controls.slim +71 -0
  197. data/lib/legate/web/views/_agent_tool_table.slim +74 -0
  198. data/lib/legate/web/views/_chat_message.slim +95 -0
  199. data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
  200. data/lib/legate/web/views/_display_agent_description.slim +11 -0
  201. data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
  202. data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
  203. data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
  204. data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
  205. data/lib/legate/web/views/_display_agent_model.slim +17 -0
  206. data/lib/legate/web/views/_display_agent_name.slim +42 -0
  207. data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
  208. data/lib/legate/web/views/_display_agent_type.slim +65 -0
  209. data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
  210. data/lib/legate/web/views/_edit_agent_description.slim +16 -0
  211. data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
  212. data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
  213. data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
  214. data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
  215. data/lib/legate/web/views/_edit_agent_model.slim +23 -0
  216. data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
  217. data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
  218. data/lib/legate/web/views/_edit_agent_type.slim +67 -0
  219. data/lib/legate/web/views/_session_error.slim +4 -0
  220. data/lib/legate/web/views/_skeleton.slim +69 -0
  221. data/lib/legate/web/views/_tool_card.slim +9 -0
  222. data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
  223. data/lib/legate/web/views/agent.slim +436 -0
  224. data/lib/legate/web/views/agent_auth.slim +562 -0
  225. data/lib/legate/web/views/agents.slim +369 -0
  226. data/lib/legate/web/views/auth.slim +112 -0
  227. data/lib/legate/web/views/auth_credential_detail.slim +327 -0
  228. data/lib/legate/web/views/auth_credentials.slim +261 -0
  229. data/lib/legate/web/views/auth_debug.slim +94 -0
  230. data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
  231. data/lib/legate/web/views/auth_mapping_new.slim +123 -0
  232. data/lib/legate/web/views/auth_mappings.slim +120 -0
  233. data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
  234. data/lib/legate/web/views/auth_schemes.slim +259 -0
  235. data/lib/legate/web/views/auth_test.slim +418 -0
  236. data/lib/legate/web/views/chat.slim +192 -0
  237. data/lib/legate/web/views/docs_index.slim +105 -0
  238. data/lib/legate/web/views/docs_show.slim +105 -0
  239. data/lib/legate/web/views/error_404.slim +5 -0
  240. data/lib/legate/web/views/index.slim +148 -0
  241. data/lib/legate/web/views/layout.slim +144 -0
  242. data/lib/legate/web/views/tool_detail.slim +87 -0
  243. data/lib/legate/web/views/tools.slim +50 -0
  244. data/lib/legate/web/webhook_listener.rb +367 -0
  245. data/lib/legate/web.rb +9 -0
  246. data/lib/legate.rb +220 -0
  247. data/public/docs/advanced/callbacks.md +828 -0
  248. data/public/docs/advanced/mcp_schema_conversion.md +59 -0
  249. data/public/docs/authentication/api_reference/config.md +210 -0
  250. data/public/docs/authentication/api_reference/credential.md +246 -0
  251. data/public/docs/authentication/api_reference/encryption.md +218 -0
  252. data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
  253. data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
  254. data/public/docs/authentication/api_reference/index.md +30 -0
  255. data/public/docs/authentication/api_reference/scheme.md +250 -0
  256. data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
  257. data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
  258. data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
  259. data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
  260. data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
  261. data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
  262. data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
  263. data/public/docs/authentication/api_reference/token_manager.md +221 -0
  264. data/public/docs/authentication/api_reference/token_store.md +146 -0
  265. data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
  266. data/public/docs/authentication/guides/api_key.md +190 -0
  267. data/public/docs/authentication/guides/bearer.md +172 -0
  268. data/public/docs/authentication/guides/configuration.md +255 -0
  269. data/public/docs/authentication/guides/custom_flow.md +523 -0
  270. data/public/docs/authentication/guides/index.md +24 -0
  271. data/public/docs/authentication/guides/migration.md +435 -0
  272. data/public/docs/authentication/guides/oauth2.md +252 -0
  273. data/public/docs/authentication/guides/oidc.md +241 -0
  274. data/public/docs/authentication/guides/overview.md +155 -0
  275. data/public/docs/authentication/guides/secure_storage.md +301 -0
  276. data/public/docs/authentication/guides/service_account.md +228 -0
  277. data/public/docs/authentication/guides/token_lifecycle.md +295 -0
  278. data/public/docs/authentication/guides/web_ui_integration.md +504 -0
  279. data/public/docs/authentication/index.md +58 -0
  280. data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
  281. data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
  282. data/public/docs/authentication/troubleshooting/index.md +11 -0
  283. data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
  284. data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
  285. data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
  286. data/public/docs/cli/legate_cli_usage.md +363 -0
  287. data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
  288. data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
  289. data/public/docs/core_concepts/legate_configuration.md +116 -0
  290. data/public/docs/core_concepts/legate_definition_store.md +102 -0
  291. data/public/docs/core_concepts/legate_planner.md +94 -0
  292. data/public/docs/core_concepts/legate_session_service.md +104 -0
  293. data/public/docs/error_handling/legate_error_handling.md +122 -0
  294. data/public/docs/examples.md +199 -0
  295. data/public/docs/getting_started.md +111 -0
  296. data/public/docs/guides/agentic_agents.md +137 -0
  297. data/public/docs/guides/ai_code_generators.md +437 -0
  298. data/public/docs/guides/auto_loading.md +326 -0
  299. data/public/docs/guides/configuring_agent_webhooks.md +219 -0
  300. data/public/docs/guides/http_client_usage.md +264 -0
  301. data/public/docs/guides/llm_providers.md +137 -0
  302. data/public/docs/guides/mcp_client_integration.md +232 -0
  303. data/public/docs/guides/mcp_server_exposure.md +206 -0
  304. data/public/docs/guides/rails_integration.md +128 -0
  305. data/public/docs/guides/sending_outbound_webhooks.md +227 -0
  306. data/public/docs/guides/streaming.md +112 -0
  307. data/public/docs/guides/webhooks.md +288 -0
  308. data/public/docs/introduction.md +51 -0
  309. data/public/docs/multi_agent_systems/advanced_features.md +57 -0
  310. data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
  311. data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
  312. data/public/docs/multi_agent_systems/state_management.md +47 -0
  313. data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
  314. data/public/docs/tools/legate_built_in_tools.md +332 -0
  315. data/public/docs/tools/legate_tools_and_registry.md +263 -0
  316. data/public/docs/web_ui/legate_web_ui.md +137 -0
  317. metadata +823 -0
@@ -0,0 +1,950 @@
1
+ # File: lib/legate/cli/agent_commands.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'thor'
5
+ require_relative 'base_command'
6
+ require 'json'
7
+ require 'yaml'
8
+ require 'fileutils' # For creating directories
9
+ require 'cli/ui' # Correct require
10
+ require_relative 'output_helper' # OutputHelper is included below
11
+ require_relative '../tool_registry'
12
+ require_relative '../agent'
13
+ require_relative '../event'
14
+ require_relative '../session'
15
+ require_relative '../session_service/in_memory'
16
+ require_relative '../global_tool_manager'
17
+ require_relative '../../legate' # For Legate.config, Legate.logger
18
+
19
+ module Legate
20
+ module CLI
21
+ # CLI commands for agent definition management AND temporary execution
22
+ class AgentCommands < BaseCommand
23
+ include OutputHelper
24
+
25
+ # In-memory session service backing the `execute` command. Overridable in tests.
26
+ @@session_service_for_execute = Legate::SessionService::InMemory.new
27
+
28
+ no_commands do
29
+ # --- Existing format_cli_result (for 'execute' command) ---
30
+ def format_cli_result(result_data)
31
+ content_to_display = nil
32
+ is_error = false
33
+ is_pending = false
34
+ status_prefix = ''
35
+
36
+ if result_data.is_a?(Legate::Event)
37
+ if %i[agent tool_result].include?(result_data.role)
38
+ content_to_display = result_data.content
39
+ if content_to_display.is_a?(Hash) && content_to_display.key?(:status)
40
+ is_error = (content_to_display[:status] == :error)
41
+ is_pending = (content_to_display[:status] == :pending)
42
+ status_prefix = '(Nested Result) ' if result_data.role == :agent
43
+ end
44
+ end
45
+ elsif result_data.is_a?(Hash) && result_data.key?(:status)
46
+ content_to_display = result_data
47
+ is_error = (result_data[:status] == :error)
48
+ is_pending = (result_data[:status] == :pending)
49
+ else
50
+ content_to_display = result_data
51
+ is_error = false
52
+ is_pending = false
53
+ end
54
+
55
+ if content_to_display.is_a?(Array) && !is_error && !is_pending
56
+ say "#{status_prefix}Multi-Step Result:", :cyan
57
+ any_step_errors = false
58
+ any_step_pending = false
59
+ content_to_display.each_with_index do |step_hash, index|
60
+ if step_hash.is_a?(Hash) && step_hash.key?(:status)
61
+ case step_hash[:status]
62
+ when :success
63
+ say " Step #{index + 1} (Success):", :green
64
+ step_result = step_hash[:result]
65
+ if step_result.is_a?(Hash) && step_result.key?(:status)
66
+ say " Result (Nested): #{step_result.inspect}"
67
+ else
68
+ say " Result: #{step_result}"
69
+ end
70
+ when :pending
71
+ say " Step #{index + 1} (Pending):", :yellow
72
+ say " Job ID: #{step_hash[:job_id]}"
73
+ say " Message: #{step_hash[:message]}" if step_hash[:message]
74
+ any_step_pending = true
75
+ when :error
76
+ say " Step #{index + 1} (Error):", :red
77
+ say " Message: #{step_hash[:error_message]}"
78
+ any_step_errors = true
79
+ else
80
+ say " Step #{index + 1} (Unknown Status): #{step_hash.inspect}", :yellow
81
+ any_step_errors = true
82
+ end
83
+ else
84
+ say " Step #{index + 1} (Unknown Step Format): #{step_hash.inspect}", :yellow
85
+ any_step_errors = true
86
+ end
87
+ end
88
+ overall_msg = if any_step_errors then 'Completed with errors'
89
+ elsif any_step_pending then 'Completed with pending steps'
90
+ else
91
+ 'Completed successfully'
92
+ end
93
+ overall_color = if any_step_errors then :red
94
+ elsif any_step_pending then :yellow
95
+ else
96
+ :green
97
+ end
98
+ say "Overall Plan Status: #{overall_msg}", overall_color
99
+ elsif content_to_display.is_a?(Hash) && content_to_display.key?(:status)
100
+ case content_to_display[:status]
101
+ when :success
102
+ say "#{status_prefix}Success:", :green
103
+ say " Result: #{content_to_display[:result]}"
104
+ when :pending
105
+ say "#{status_prefix}Pending:", :yellow
106
+ say " Job ID: #{content_to_display[:job_id]}"
107
+ say " Message: #{content_to_display[:message]}" if content_to_display[:message]
108
+ when :error
109
+ say "#{status_prefix}Error:", :red
110
+ say " Message: #{content_to_display[:error_message]}"
111
+ else
112
+ say "#{status_prefix}Unknown Status:", :yellow
113
+ say " Data: #{content_to_display.inspect}"
114
+ end
115
+ else
116
+ say "#{status_prefix}Success:", :green
117
+ say " Result: #{content_to_display}"
118
+ end
119
+ end
120
+ # --- End format_cli_result ---
121
+
122
+ # Recursively extract the innermost :result value for nested :success hashes
123
+ def deep_result_value(val)
124
+ # Always normalize keys to symbols
125
+ if val.is_a?(Hash)
126
+ val = val.transform_keys(&:to_sym)
127
+ # Prefer to recurse into :result if present
128
+ if val.key?(:result)
129
+ return deep_result_value(val[:result])
130
+ elsif val.key?(:plan_details) && val[:plan_details].is_a?(Array) && !val[:plan_details].empty?
131
+ return deep_result_value(val[:plan_details].last)
132
+ end
133
+ end
134
+ val
135
+ end
136
+
137
+ # --- formatting helper for CLI UI Chat ---
138
+ def _format_chat_turn_output_cli_ui(event_or_hash, role_override = nil, timestamp = nil)
139
+ event_obj = event_or_hash.is_a?(Legate::Event) ? event_or_hash : nil
140
+ data_to_format = event_obj ? event_obj.content : event_or_hash
141
+ current_role = role_override || (event_obj ? event_obj.role : :agent)
142
+
143
+ # Use provided timestamp or create one if not provided
144
+ formatted_time = timestamp || Time.now.strftime('%H:%M:%S')
145
+
146
+ # Normalize keys to symbols
147
+ data_to_format = data_to_format.transform_keys(&:to_sym) if data_to_format.is_a?(Hash)
148
+
149
+ if current_role == :user
150
+ # Add extra line break for spacing and show timestamp
151
+ ::CLI::UI.puts "\n{{blue:You}} {{gray:(#{formatted_time})}}:"
152
+ ::CLI::UI.puts " #{data_to_format}\n"
153
+ return
154
+ end
155
+
156
+ if data_to_format.is_a?(Hash) && data_to_format.key?(:status) &&
157
+ data_to_format[:status].to_s.downcase == 'success'
158
+ actual_result = deep_result_value(data_to_format)
159
+ # Add timestamp to the header for agent responses
160
+ ::CLI::UI::Frame.open("Agent Response (#{formatted_time})", color: :green) do
161
+ ::CLI::UI.puts actual_result.to_s
162
+ end
163
+ ::CLI::UI.puts '' # Add extra line break after the response
164
+ return
165
+ end
166
+
167
+ unless data_to_format.is_a?(Hash) && data_to_format.key?(:status)
168
+ ::CLI::UI::Frame.open("Agent Response (#{formatted_time})", color: :cyan) do
169
+ ::CLI::UI.puts data_to_format.inspect
170
+ end
171
+ ::CLI::UI.puts '' # Add extra line break after the response
172
+ return
173
+ end
174
+
175
+ case data_to_format[:status]
176
+ when :error
177
+ title_color = :red
178
+ title_prefix = 'Agent Error'
179
+ message_body_content = data_to_format[:error_message]
180
+ ::CLI::UI::Frame.open("#{title_prefix} (#{formatted_time})", color: title_color) do
181
+ ::CLI::UI.puts message_body_content
182
+ end
183
+ when :pending
184
+ title_color = :yellow
185
+ title_prefix = 'Agent Pending'
186
+ message_body_content = "Job ID [#{data_to_format[:job_id]}] - #{data_to_format[:message]}"
187
+ ::CLI::UI::Frame.open("#{title_prefix} (#{formatted_time})", color: title_color) do
188
+ ::CLI::UI.puts message_body_content
189
+ end
190
+ else
191
+ title_color = :magenta
192
+ title_prefix = "Agent (Status: #{data_to_format[:status]})"
193
+ message_body_content = data_to_format.inspect
194
+ ::CLI::UI::Frame.open("#{title_prefix} (#{formatted_time})", color: title_color) do
195
+ ::CLI::UI.puts message_body_content
196
+ end
197
+ end
198
+ ::CLI::UI.puts '' # Add extra line break after any response
199
+ end
200
+ # --- END _format_chat_turn_output_cli_ui ---
201
+ end # end no_commands
202
+
203
+ # --- Definition Management Commands (Existing - no changes shown for brevity) ---
204
+ desc 'list', 'List all defined agents'
205
+ method_option :json, type: :boolean, default: false,
206
+ desc: 'Output result in JSON format'
207
+ def list
208
+ definitions = Legate::GlobalDefinitionRegistry.all
209
+ if json_mode?
210
+ agents = definitions.sort_by { |name, _| name.to_s }.map do |name, defn|
211
+ {
212
+ name: name.to_s,
213
+ description: defn.description || nil,
214
+ model: (defn.model_name || Legate::Agent::DEFAULT_MODEL).to_s,
215
+ tools: defn.tool_names.to_a.map(&:to_s)
216
+ }
217
+ end
218
+ puts JSON.generate({ agents: agents })
219
+ elsif definitions.empty?
220
+ say 'No agent definitions found.'
221
+ else
222
+ say 'Defined Agents:', :bold
223
+ definitions.sort_by { |name, _| name.to_s }.each do |name, defn|
224
+ description = defn.description || '[No description]'
225
+ tools = defn.tool_names.to_a
226
+ model = (defn.model_name || "#{Legate::Agent::DEFAULT_MODEL} (Default)").to_s
227
+ tools_str = tools.empty? ? 'None' : tools.join(', ')
228
+ say "- #{name}: #{description} (Model: #{model}, Tools: #{tools_str})"
229
+ end
230
+ end
231
+ end
232
+
233
+ desc 'save NAME', 'Create or update an agent definition'
234
+ method_option :description, type: :string, required: true, desc: 'Agent description'
235
+ method_option :tools, type: :string, aliases: '-t', desc: 'Comma-separated list of tool names (e.g., "echo,calculator")'
236
+ method_option :model, type: :string, desc: "LLM model name (default: #{Legate::Agent::DEFAULT_MODEL})"
237
+ method_option :instruction, type: :string, desc: "Core instructions for the agent's behavior (system prompt)."
238
+ method_option :webhook_enabled, type: :boolean, default: false, desc: 'Enable webhook triggering for this agent.'
239
+ method_option :webhook_secret, type: :string, desc: 'Secret key for webhook validation (if webhook_enabled).'
240
+ method_option :mcp_servers_json, type: :string, desc: 'JSON string of MCP server configurations array.'
241
+
242
+ def save(name)
243
+ name_sym = name.to_sym
244
+ description = options[:description]
245
+ model_to_save = options[:model] && !options[:model].empty? ? options[:model] : Legate::Agent::DEFAULT_MODEL
246
+ instruction_to_save = options[:instruction]
247
+
248
+ selected_tools = []
249
+ valid_tools = Legate::GlobalToolManager.registered_tool_names.map(&:to_s)
250
+ if options[:tools]
251
+ requested_tools = options[:tools].split(',').map(&:strip).reject(&:empty?)
252
+ requested_tools.each do |tool_name|
253
+ if valid_tools.include?(tool_name)
254
+ selected_tools << tool_name unless selected_tools.include?(tool_name)
255
+ else
256
+ say "Warning: Unknown globally registered tool '#{tool_name}', ignoring.", :yellow
257
+ end
258
+ end
259
+ end
260
+
261
+ definition = {
262
+ description: description,
263
+ tools: selected_tools,
264
+ model: model_to_save,
265
+ instruction: instruction_to_save,
266
+ fallback_mode: :error,
267
+ mcp_servers_json: options[:mcp_servers_json] || '[]',
268
+ webhook_enabled: options[:webhook_enabled],
269
+ webhook_secret: options[:webhook_secret]
270
+ }
271
+
272
+ # Build an AgentDefinition object and register it
273
+ agent_def = Legate::AgentDefinition.from_hash(definition.merge(name: name_sym))
274
+ unless agent_def
275
+ say 'Error creating agent definition. Aborting.', :red
276
+ exit(1)
277
+ end
278
+ Legate::GlobalDefinitionRegistry.register(agent_def)
279
+ tools_msg = selected_tools.empty? ? 'None' : selected_tools.join(', ')
280
+ say "Agent definition '#{name}' saved (Model: #{model_to_save}, Tools: #{tools_msg}, Instruction: #{instruction_to_save ? 'Set' : 'Not Set'}).", :green
281
+ end
282
+
283
+ desc 'delete NAME', "Delete an agent's definition"
284
+ def delete(name)
285
+ name_sym = name.to_sym
286
+ definition_exists = Legate::GlobalDefinitionRegistry.definition_exists?(name_sym)
287
+
288
+ unless definition_exists
289
+ say "Error: Agent definition '#{name}' not found.", :red
290
+ exit(1)
291
+ end
292
+
293
+ if yes?("Are you sure you want to permanently delete agent definition '#{name}'? [y/N]", :yellow)
294
+ Legate::GlobalDefinitionRegistry.delete_definition(name_sym)
295
+ say "Agent definition '#{name}' deleted successfully.", :green
296
+ else
297
+ say 'Deletion cancelled.', :yellow
298
+ end
299
+ end
300
+
301
+ desc 'generate NAME', 'Generate a new agent definition file'
302
+ method_option :description, type: :string, default: 'A new Legate agent.', desc: 'Agent description'
303
+ method_option :instruction, type: :string, default: 'You are a helpful assistant.', desc: 'Agent instruction (system prompt)'
304
+ method_option :tools, type: :string, aliases: '-t', default: '', desc: 'Comma-separated list of tool names (e.g., "echo,calculator")'
305
+ method_option :model, type: :string, desc: 'LLM model name (uses framework default if blank)'
306
+ method_option :dir, type: :string, default: './agents', desc: 'Directory to save the agent definition file'
307
+ method_option :force, type: :boolean, default: false, desc: 'Overwrite existing file without prompting'
308
+ method_option :webhook_enabled, type: :boolean, default: false, desc: 'Include webhook configuration placeholders'
309
+ def generate(name)
310
+ agent_name_sym = name.to_sym
311
+ dir_path = File.expand_path(options[:dir])
312
+ file_path = File.join(dir_path, "#{name}_agent.rb")
313
+ if File.exist?(file_path) && !options[:force] && !yes?("Agent file '#{file_path}' already exists. Overwrite? [y/N]", :yellow)
314
+ say 'Generation cancelled.', :yellow
315
+ exit(0)
316
+ end
317
+ begin
318
+ FileUtils.mkdir_p(dir_path)
319
+ rescue SystemCallError => e
320
+ say "Error: Could not create directory '#{dir_path}': #{e.message}", :red
321
+ exit(1)
322
+ end
323
+ agent_name_str = name
324
+ description = options[:description]
325
+ instruction = options[:instruction]
326
+ tools_list = options[:tools].split(',').map(&:strip).reject(&:empty?).map(&:to_sym)
327
+ model_str = options[:model]
328
+ webhook_enabled = options[:webhook_enabled]
329
+ code = <<~RUBY
330
+ require 'legate'
331
+
332
+ Legate::Agent.define do |a|
333
+ a.name :#{agent_name_sym}
334
+ a.description "#{description}"
335
+ a.instruction "#{instruction}"
336
+ #{' '}
337
+ RUBY
338
+ if model_str && !model_str.empty?
339
+ code += " # Optional: Specify model (defaults to Legate.config.default_model_name)\n"
340
+ code += " a.model_name '#{model_str}'\n\n"
341
+ else
342
+ code += " # Model will use framework default: #{Legate.config.default_model_name}\n\n"
343
+ end
344
+ code += " # Define tools the agent can use\n"
345
+ if tools_list.empty?
346
+ code += " # a.use_tool :echo # Example\n"
347
+ else
348
+ tools_list.each { |tool| code += " a.use_tool :#{tool}\n" }
349
+ end
350
+ code += "\n"
351
+ if webhook_enabled
352
+ code += <<~WEBHOOK
353
+ # --- Webhook Configuration ---#{' '}
354
+ # This agent can be triggered by POST /webhooks/agents/#{agent_name_sym}/trigger
355
+ # (Assuming default listener base_path and dynamic_agent_route_pattern in Legate.configure)
356
+
357
+ a.webhook_enabled true
358
+
359
+ a.webhook_transformer ->(request_body) do#{' '}
360
+ raise NotImplementedError, "Please implement the webhook_transformer proc to convert request_body into agent user_input."
361
+ end
362
+
363
+ a.webhook_session_extractor ->(request_body) do
364
+ raise NotImplementedError, "Please implement the webhook_session_extractor proc to extract a session ID."
365
+ end
366
+ WEBHOOK
367
+ end
368
+ code += "end\n"
369
+ begin
370
+ File.write(file_path, code)
371
+ say "Agent definition file created at '#{file_path}'", :green
372
+ if webhook_enabled
373
+ say "\nWebhook configuration placeholders added. Please implement the required transformer and extractor procs.", :yellow
374
+ say 'Remember to configure validation and secrets for production use!', :yellow
375
+ end
376
+ rescue SystemCallError => e
377
+ say "Error: Could not write file '#{file_path}': #{e.message}", :red
378
+ exit(1)
379
+ end
380
+ end
381
+
382
+ desc 'ai_generate', 'Generate agent definition code using AI from a natural language description'
383
+ long_desc <<-LONGDESC
384
+ Uses AI (Gemini LLM) to generate a production-ready agent definition based on
385
+ your natural language description.
386
+
387
+ Input sources (in priority order):
388
+ 1. --description / -d : Inline description
389
+ 2. --prompt-file / -f : Read description from a file
390
+ 3. stdin : Pipe description via stdin (auto-enables stdout output)
391
+
392
+ Output destinations:
393
+ - Default: Writes to ./<suggested_name>_agent.rb
394
+ - --output / -o : Custom output file path
395
+ - --stdout : Output to stdout instead of file
396
+ - When reading from stdin: Auto-outputs to stdout
397
+
398
+ Examples:
399
+ legate agent ai_generate -d "An agent that helps with customer support"
400
+ legate agent ai_generate -f prompt.txt -o ./agents/support_agent.rb
401
+ echo "A hello world agent" | legate agent ai_generate
402
+ echo "A calculator agent" | legate agent ai_generate > calc_agent.rb
403
+
404
+ Requires GOOGLE_API_KEY environment variable to be set.
405
+ LONGDESC
406
+ method_option :description, aliases: '-d', type: :string, desc: 'Description of the agent to generate'
407
+ method_option :prompt_file, aliases: '-f', type: :string, desc: 'Read description from a file'
408
+ method_option :output, aliases: '-o', type: :string, desc: 'Output file path (default: ./<suggested_name>_agent.rb)'
409
+ method_option :stdout, type: :boolean, default: false, desc: 'Output to stdout instead of file'
410
+ method_option :force, type: :boolean, default: false, desc: 'Overwrite existing file without prompting'
411
+ def ai_generate
412
+ require_relative '../generators'
413
+
414
+ description = nil
415
+ from_stdin = false
416
+
417
+ # Priority: --description > --prompt-file > stdin
418
+ if options[:description] && !options[:description].strip.empty?
419
+ description = options[:description].strip
420
+ elsif options[:prompt_file]
421
+ unless File.exist?(options[:prompt_file])
422
+ say "Error: Prompt file '#{options[:prompt_file]}' not found.", :red
423
+ exit(1)
424
+ end
425
+ description = File.read(options[:prompt_file]).strip
426
+ elsif !$stdin.tty?
427
+ # Reading from stdin (piped input)
428
+ description = $stdin.read.strip
429
+ from_stdin = true
430
+ end
431
+
432
+ if description.nil? || description.empty?
433
+ say 'Error: No description provided. Use --description, --prompt-file, or pipe via stdin.', :red
434
+ exit(1)
435
+ end
436
+
437
+ # Determine output mode
438
+ output_to_stdout = options[:stdout] || from_stdin
439
+
440
+ say 'Generating agent code via AI...', :cyan unless output_to_stdout
441
+
442
+ begin
443
+ result = Legate::Generators::AgentGenerator.generate(description: description)
444
+ code = result[:code]
445
+ suggested_name = result[:suggested_name]
446
+
447
+ if output_to_stdout
448
+ puts code
449
+ else
450
+ # Write to file
451
+ file_path = options[:output] || "./#{suggested_name}_agent.rb"
452
+
453
+ if File.exist?(file_path) && !options[:force] && !yes?("File '#{file_path}' already exists. Overwrite? [y/N]", :yellow)
454
+ say 'Generation cancelled.', :yellow
455
+ exit(0)
456
+ end
457
+
458
+ File.write(file_path, code)
459
+ say "Agent definition generated and saved to '#{file_path}'", :green
460
+ say " Suggested name: #{suggested_name}", :cyan
461
+ end
462
+ rescue Legate::Generators::AgentGenerator::ApiKeyMissingError => e
463
+ say "Error: #{e.message}", :red
464
+ exit(1)
465
+ rescue Legate::Generators::AgentGenerator::ApiError => e
466
+ say "Error: #{e.message}", :red
467
+ exit(1)
468
+ rescue Legate::Generators::AgentGenerator::GenerationError => e
469
+ say "Error: #{e.message}", :red
470
+ exit(1)
471
+ rescue StandardError => e
472
+ say "Unexpected error: #{e.class} - #{e.message}", :red
473
+ exit(1)
474
+ end
475
+ end
476
+
477
+ desc 'start NAME', 'Verify agent definition loading and start (Ephemeral)'
478
+ long_desc <<-LONGDESC
479
+ Loads agent definition, instantiates agent, starts agent runtime state,
480
+ verifies all components loaded correctly, prints details & exits.
481
+ This is a diagnostic tool to verify agent definition loads properly.
482
+ Use 'execute' or 'chat' command to run an actual task with the agent.
483
+ LONGDESC
484
+ method_option :quiet, type: :boolean, default: false, aliases: '-q',
485
+ desc: 'Suppress status messages, only output result'
486
+ method_option :json, type: :boolean, default: false,
487
+ desc: 'Output result in JSON format (implies --quiet)'
488
+ def start(name)
489
+ # Suppress all logging in JSON mode for clean output
490
+ Legate.logger.level = Logger::FATAL if json_mode?
491
+
492
+ name_sym = name.to_sym
493
+ status_message("Loading agent '#{name}'...")
494
+
495
+ # First check the global registry
496
+ agent_definition_object = Legate::GlobalDefinitionRegistry.find(name_sym)
497
+
498
+ unless agent_definition_object
499
+ output_error("Agent definition '#{name}' not found.", metadata: { agent: name })
500
+ exit(1)
501
+ end
502
+
503
+ agent = nil
504
+ result_data = nil
505
+ begin
506
+ # Pass the definition object directly. Session service will use global default.
507
+ agent = Legate::Agent.new(definition: agent_definition_object)
508
+
509
+ # Tool loading is now handled by Legate::Agent#initialize via the definition.
510
+ loaded_tool_names = agent.tools.map(&:name)
511
+ defined_tool_names = agent_definition_object.tool_names.to_a
512
+ missing_tools = defined_tool_names - loaded_tool_names
513
+
514
+ status_message(" - Agent uses model: #{agent.model_name}", :cyan)
515
+ status_message(" - Agent instruction: #{agent.instruction.inspect}", :cyan)
516
+ status_message(" - Warning: Tools defined but not loaded/found: [#{missing_tools.join(', ')}]", :yellow) unless missing_tools.empty?
517
+ status_message(" - Loaded tools: [#{loaded_tool_names.join(', ')}]", :cyan)
518
+
519
+ status_message(' - Starting agent runtime...', :cyan)
520
+ agent.start
521
+ status_message('started.', :cyan)
522
+ status_message("\nAgent '#{name}' is ready.", :green)
523
+
524
+ result_data = {
525
+ status: 'ready',
526
+ agent: name,
527
+ model: agent.model_name,
528
+ tools: loaded_tool_names.map(&:to_s),
529
+ missing_tools: missing_tools.map(&:to_s)
530
+ }
531
+ rescue StandardError => e
532
+ output_error(e, metadata: { agent: name })
533
+ puts e.backtrace.first(5).join("\n") unless json_mode?
534
+ exit(1)
535
+ ensure
536
+ if agent&.running?
537
+ status_message(' - Stopping agent runtime...', :cyan)
538
+ agent.stop
539
+ status_message('stopped.', :cyan)
540
+ end
541
+ end
542
+
543
+ # Output final result in JSON mode
544
+ return unless json_mode? && result_data
545
+
546
+ puts JSON.generate(result_data)
547
+ end
548
+
549
+ desc 'stop NAME', 'Stop a persistent agent'
550
+ long_desc <<-LONGDESC
551
+ Stops a persistent agent by updating its status in the definition registry.
552
+
553
+ This command marks the agent as 'stopped'. If the agent is running
554
+ in a web server process, it will be stopped the next time the server checks
555
+ the agent status, or you can restart the web server.
556
+
557
+ Note: This command updates the persistent_status in the registry. If no web server
558
+ is running the agent, this simply ensures the status is set to 'stopped'.
559
+ LONGDESC
560
+ method_option :force, type: :boolean, default: false, desc: 'Force stop without confirmation'
561
+ method_option :quiet, type: :boolean, default: false, aliases: '-q',
562
+ desc: 'Suppress status messages, only output result'
563
+ method_option :json, type: :boolean, default: false,
564
+ desc: 'Output result in JSON format (implies --quiet)'
565
+ def stop(name)
566
+ # Suppress all logging in JSON mode for clean output
567
+ Legate.logger.level = Logger::FATAL if json_mode?
568
+
569
+ name_sym = name.to_sym
570
+ status_message("Stopping agent '#{name}'...")
571
+
572
+ # Load definition from registry
573
+ definition = Legate::GlobalDefinitionRegistry.get_definition(name_sym)
574
+
575
+ unless definition
576
+ output_error("Agent definition '#{name}' not found.", metadata: { agent: name })
577
+ exit(1)
578
+ end
579
+
580
+ # Check current status
581
+ current_status = definition[:persistent_status] || 'stopped'
582
+ if current_status == 'stopped'
583
+ if json_mode?
584
+ puts JSON.generate({ status: 'already_stopped', agent: name })
585
+ else
586
+ say "Agent '#{name}' is already stopped.", :yellow
587
+ end
588
+ return
589
+ end
590
+
591
+ # Confirm if not forced (skip in quiet/json mode)
592
+ if !(options[:force] || quiet_mode?) && !yes?("Agent '#{name}' is currently marked as '#{current_status}'. Stop it? [y/N]", :yellow)
593
+ say 'Stop cancelled.', :yellow
594
+ return
595
+ end
596
+
597
+ # Update the persistent_status to stopped
598
+ Legate::GlobalDefinitionRegistry.update_definition(name_sym, { persistent_status: 'stopped' })
599
+
600
+ if json_mode?
601
+ puts JSON.generate({ status: 'stopped', agent: name, previous_status: current_status })
602
+ else
603
+ say "Agent '#{name}' has been marked as stopped.", :green
604
+ say ' Note: If running in a web server, the agent will stop on next status check or server restart.', :cyan
605
+ end
606
+ end
607
+
608
+ desc 'status NAME', 'Check the status of a persistent agent'
609
+ long_desc <<-LONGDESC
610
+ Shows the current status of a persistent agent from the definition store.
611
+
612
+ Displays the agent's persistent_status (running, stopped, etc.) along with
613
+ basic agent information. This is useful for checking if an agent is currently
614
+ active or has been stopped.
615
+ LONGDESC
616
+ method_option :quiet, type: :boolean, default: false, aliases: '-q',
617
+ desc: 'Suppress status messages, only output result'
618
+ method_option :json, type: :boolean, default: false,
619
+ desc: 'Output result in JSON format (implies --quiet)'
620
+ def status(name)
621
+ # Suppress all logging in JSON mode for clean output
622
+ Legate.logger.level = Logger::FATAL if json_mode?
623
+
624
+ name_sym = name.to_sym
625
+ status_message("Checking status of agent '#{name}'...")
626
+
627
+ # Load definition from registry
628
+ definition = Legate::GlobalDefinitionRegistry.get_definition(name_sym)
629
+
630
+ unless definition
631
+ output_error("Agent definition '#{name}' not found.", metadata: { agent: name })
632
+ exit(1)
633
+ end
634
+
635
+ persistent_status = definition[:persistent_status] || 'stopped'
636
+ model = definition[:model] || Legate::Agent::DEFAULT_MODEL
637
+ description = definition[:description] || '[No description]'
638
+ tools = definition[:tools] || []
639
+
640
+ if json_mode?
641
+ puts JSON.generate({
642
+ agent: name,
643
+ status: persistent_status,
644
+ model: model,
645
+ description: description,
646
+ tools: tools
647
+ })
648
+ else
649
+ say "Agent: #{name}", :bold
650
+ case persistent_status
651
+ when 'running'
652
+ say " Status: #{persistent_status}", :green
653
+ when 'stopped'
654
+ say " Status: #{persistent_status}", :yellow
655
+ else
656
+ say " Status: #{persistent_status}", :cyan
657
+ end
658
+ say " Model: #{model}"
659
+ say " Description: #{description}"
660
+ say " Tools: #{tools.empty? ? 'None' : tools.join(', ')}"
661
+ end
662
+ end
663
+
664
+ desc 'export NAME', 'Export an agent definition to YAML or JSON'
665
+ method_option :format, type: :string, default: 'yaml', enum: %w[yaml json], desc: 'Output format (yaml or json)'
666
+ method_option :output, type: :string, aliases: '-o', desc: 'Output file path (default: stdout)'
667
+ def export(name)
668
+ name_sym = name.to_sym
669
+
670
+ # Load definition from registry
671
+ definition = Legate::GlobalDefinitionRegistry.get_definition(name_sym)
672
+
673
+ unless definition
674
+ say "Error: Agent definition '#{name}' not found.", :red
675
+ exit(1)
676
+ end
677
+
678
+ # Clean up internal fields before export
679
+ export_data = definition.dup
680
+ export_data.delete(:persistent_status) # implementation detail
681
+ # Ensure keys are strings for cleaner JSON/YAML
682
+ export_data = export_data.transform_keys(&:to_s)
683
+
684
+ # Format output
685
+ output_content = ''
686
+ case options[:format].downcase
687
+ when 'json'
688
+ output_content = JSON.pretty_generate(export_data)
689
+ when 'yaml'
690
+ output_content = export_data.to_yaml
691
+ end
692
+
693
+ # Write to file or stdout
694
+ if options[:output]
695
+ begin
696
+ File.write(options[:output], output_content)
697
+ say "Agent definition exported to #{options[:output]}", :green
698
+ rescue StandardError => e
699
+ say "Error writing to file: #{e.message}", :red
700
+ exit(1)
701
+ end
702
+ else
703
+ say output_content
704
+ end
705
+ end
706
+
707
+ desc 'execute NAME TASK', 'Execute a task using agent definition (ephemeral)'
708
+ long_desc <<-LONGDESC
709
+ Loads agent definition, instantiates agent, runs TASK within a session context,
710
+ prints the result, stops agent runtime & exits.
711
+
712
+ Use --session-id to continue an existing conversation,
713
+ otherwise starts a new session for this execution. The session ID used will be printed.
714
+
715
+ If a task results in a :pending status (e.g., for an async job),
716
+ the job_id will be printed. Use the check_job_status tool
717
+ in a subsequent call to get the final result.
718
+ LONGDESC
719
+ method_option :session_id, type: :string, desc: 'Optional ID of an existing session to use.'
720
+ method_option :user_id, type: :string, default: 'cli_user', desc: 'User ID for the session'
721
+ method_option :quiet, type: :boolean, default: false, aliases: '-q',
722
+ desc: 'Suppress status messages, only output result'
723
+ method_option :json, type: :boolean, default: false,
724
+ desc: 'Output result in JSON format (implies --quiet)'
725
+ def execute(name, task)
726
+ # Suppress all logging in JSON mode for clean output
727
+ Legate.logger.level = Logger::FATAL if json_mode?
728
+
729
+ name_sym = name.to_sym
730
+ status_message("Loading agent '#{name}' to execute task: \"#{task}\"...")
731
+ agent_definition_object = Legate::GlobalDefinitionRegistry.find(name_sym)
732
+
733
+ unless agent_definition_object
734
+ output_error("Agent definition '#{name}' not found.", metadata: { agent: name })
735
+ exit(1)
736
+ end
737
+
738
+ session_service_instance = @@session_service_for_execute
739
+ session_id_opt = options[:session_id]
740
+ legate_session = nil
741
+ if session_id_opt
742
+ legate_session = session_service_instance.get_session(session_id: session_id_opt)
743
+ if legate_session then status_message("Continuing session: #{session_id_opt}", :cyan)
744
+ else
745
+ status_message("Warning: Session ID '#{session_id_opt}' provided but not found. Starting a new session.", :yellow)
746
+ session_id_opt = nil
747
+ end
748
+ end
749
+ unless legate_session
750
+ legate_session = session_service_instance.create_session(app_name: name, user_id: options[:user_id])
751
+ session_id_opt = legate_session.id
752
+ status_message("Started new session: #{session_id_opt}", :cyan)
753
+ status_message(' (Using in-memory session storage)', :cyan)
754
+ end
755
+
756
+ agent = nil
757
+ e_outer = nil
758
+ begin
759
+ # Pass the definition object. Session service for the agent instance itself will use global default
760
+ # or the one passed if Legate::Agent.new supported it directly for its own session_service attr.
761
+ # The run_task method will use the session_service_instance passed to it for actual session operations.
762
+ agent = Legate::Agent.new(
763
+ definition: agent_definition_object,
764
+ session_service: session_service_instance
765
+ )
766
+
767
+ status_message(" - Agent uses model: #{agent.model_name}", :cyan)
768
+
769
+ # Tool loading is now handled by Legate::Agent#initialize via the definition.
770
+ loaded_tool_instances = agent.tools
771
+ loaded_tool_names = loaded_tool_instances.map(&:name)
772
+ defined_tool_names = agent_definition_object.tool_names.to_a
773
+ missing_tools = defined_tool_names - loaded_tool_names
774
+
775
+ status_message(" - Warning: Tools defined but not loaded/found: [#{missing_tools.join(', ')}]", :yellow) unless missing_tools.empty?
776
+ status_message(" - Loaded tools: [#{loaded_tool_names.join(', ')}]", :cyan)
777
+ status_message(' - Starting agent runtime...', :cyan)
778
+ agent.start
779
+ status_message('started.', :cyan)
780
+ status_message(" - Running task in session #{session_id_opt}: '#{task}'...", :cyan)
781
+ final_event_or_error = agent.run_task(
782
+ session_id: session_id_opt,
783
+ user_input: task,
784
+ session_service: session_service_instance
785
+ )
786
+ status_message('finished.', :cyan)
787
+ status_message("\nTask Result:", :bold)
788
+ output_result(final_event_or_error, metadata: { session_id: session_id_opt, agent: name }, format_method: :format_cli_result)
789
+ rescue StandardError => e
790
+ e_outer = e
791
+ output_error(e, metadata: { agent: name, session_id: session_id_opt })
792
+ puts e.backtrace.first(5).join("\n") unless json_mode?
793
+ ensure
794
+ if agent&.running?
795
+ status_message(' - Stopping agent runtime...', :cyan)
796
+ agent.stop
797
+ status_message('stopped.', :cyan)
798
+ end
799
+ exit(1) if e_outer
800
+ end
801
+ end # End 'execute' command
802
+
803
+ # --- CHAT COMMAND ---
804
+ desc 'chat AGENT_NAME', 'Interactively chat with an agent definition'
805
+ long_desc <<-LONGDESC
806
+ Starts an interactive chat session with the specified agent.
807
+ The agent definition is loaded from the definition registry.
808
+
809
+ Session Handling:
810
+ - Uses an in-memory session that is lost when the chat ends.
811
+ - Use `--session-id <ID>` to resume a specific existing session. If the ID
812
+ is not found, a new session will be created.
813
+
814
+ Type "exit" or "quit" to end the chat.
815
+ LONGDESC
816
+ method_option :session_id, type: :string, desc: 'ID of an existing session to resume.'
817
+ method_option :user_id, type: :string, default: 'cli_user', desc: 'User ID for the session'
818
+ def chat(agent_name_str)
819
+ ::CLI::UI::StdoutRouter.enable
820
+ agent_name_sym = agent_name_str.to_sym
821
+
822
+ agent_definition_object = Legate::GlobalDefinitionRegistry.find(agent_name_sym)
823
+ unless agent_definition_object
824
+ ::CLI::UI.puts "{{red:Error: Agent definition '#{agent_name_str}' not found.}}"
825
+ exit(1)
826
+ end
827
+
828
+ # Convert AgentDefinition to hash for display/compatibility
829
+ definition = Legate::GlobalDefinitionRegistry.get_definition(agent_name_sym)
830
+
831
+ session_service_instance = Legate::SessionService::InMemory.new
832
+
833
+ current_session_id = options[:session_id]
834
+ legate_session = nil # Will hold the loaded Legate::Session object
835
+
836
+ ::CLI::UI::Frame.open("Chat Session with #{agent_name_str}", color: :blue) do
837
+ ::CLI::UI.puts "{{bold:Agent Description:}} #{definition[:description]}"
838
+ if current_session_id
839
+ legate_session = session_service_instance.get_session(session_id: current_session_id)
840
+ if legate_session
841
+ ::CLI::UI.puts "{{green:Resuming session:}} #{current_session_id} (in-memory)"
842
+ # --- MODIFIED: Display history if session is loaded and has events ---
843
+ if legate_session.events && !legate_session.events.empty?
844
+ ::CLI::UI.puts "\n{{bold:━━━ Recent Conversation History ━━━}}"
845
+
846
+ # Group events by conversation turns
847
+ history_events = legate_session.events.last(20) # Show more history items
848
+ current_date = nil
849
+
850
+ history_events.each do |event|
851
+ # Extract timestamp from event and format it
852
+ event_time = event.timestamp ? Time.at(event.timestamp) : Time.now
853
+ formatted_time = event_time.strftime('%H:%M:%S')
854
+
855
+ # Show date separator if this is a new day
856
+ event_date = event_time.strftime('%Y-%m-%d')
857
+ if current_date != event_date
858
+ current_date = event_date
859
+ ::CLI::UI.puts "\n{{bold:┅┅┅ #{event_time.strftime('%B %d, %Y')} ┅┅┅}}"
860
+ end
861
+
862
+ if event.role == :user
863
+ # For user role, event.content is the string message
864
+ _format_chat_turn_output_cli_ui(event.content, :user, formatted_time)
865
+ elsif event.role == :agent
866
+ # For agent role, event.content is the hash {status:, result:, ...}
867
+ _format_chat_turn_output_cli_ui(event.content, :agent, formatted_time)
868
+ end
869
+ # Tool events are generally not shown in simple chat history
870
+ end
871
+ ::CLI::UI.puts "{{bold:━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━}}\n"
872
+ else
873
+ ::CLI::UI.puts '{{italic:No previous messages in this session.}}'
874
+ end
875
+ # --- END MODIFICATION ---
876
+ else
877
+ ::CLI::UI.puts "{{yellow:Warning: Session ID '#{current_session_id}' not found. Starting new session.}}"
878
+ current_session_id = nil # Force new session creation
879
+ end
880
+ end
881
+ unless legate_session # If still no session (either not provided, or provided but not found)
882
+ legate_session = session_service_instance.create_session(app_name: agent_name_str, user_id: options[:user_id])
883
+ current_session_id = legate_session.id # Update current_session_id with the new one
884
+ ::CLI::UI.puts "{{green:Started new session:}} #{current_session_id} (in-memory)"
885
+ end
886
+ ::CLI::UI.puts "{{gray:Type 'exit' or 'quit' to end the chat.}}"
887
+ end
888
+ ::CLI::UI.puts '---'
889
+
890
+ agent = nil
891
+ begin
892
+ # Instantiate the agent with its definition object and the session service
893
+ agent = Legate::Agent.new(
894
+ definition: agent_definition_object,
895
+ session_service: session_service_instance
896
+ )
897
+ # Tool setup is now handled within Legate::Agent#initialize based on the definition object.
898
+
899
+ agent.start
900
+ rescue StandardError => e
901
+ ::CLI::UI.puts "{{red:Error initializing or starting agent: #{e.message}}}"
902
+ exit(1)
903
+ end
904
+
905
+ loop do
906
+ user_input = ::CLI::UI::Prompt.ask('You')
907
+ break if user_input.nil?
908
+
909
+ user_input.strip!
910
+ break if %w[exit quit].include?(user_input.downcase)
911
+ next if user_input.empty?
912
+
913
+ final_event = nil
914
+ session_lost_flag = false
915
+ begin
916
+ ::CLI::UI::Spinner.spin('Agent thinking...') do |_spinner|
917
+ current_legate_session_for_task = session_service_instance.get_session(session_id: current_session_id)
918
+ unless current_legate_session_for_task
919
+ ::CLI::UI.puts "{{red:Error: Session '#{current_session_id}' lost. Please restart chat.}}"
920
+ session_lost_flag = true
921
+ break
922
+ end
923
+
924
+ final_event = agent.run_task(
925
+ session_id: current_session_id,
926
+ user_input: user_input,
927
+ session_service: session_service_instance
928
+ )
929
+ end
930
+ break if session_lost_flag
931
+
932
+ _format_chat_turn_output_cli_ui(final_event)
933
+ rescue StandardError => e
934
+ ::CLI::UI::Frame.open('Error During Task', color: :red) do
935
+ ::CLI::UI.puts e.message
936
+ end
937
+ end
938
+ end
939
+ ensure
940
+ agent&.stop if agent&.running?
941
+ ::CLI::UI.puts '{{yellow:Chat ended.}}'
942
+ end
943
+ # --- END CHAT COMMAND ---
944
+
945
+ def self.exit_on_failure?
946
+ true
947
+ end
948
+ end # End AgentCommands class
949
+ end # End CLI module
950
+ end # End Legate module