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,803 @@
1
+ # File: lib/legate/web/routes/agent_definition_routes.rb
2
+ # frozen_string_literal: true
3
+
4
+ module Legate
5
+ module Web
6
+ module AgentDefinitionRoutes
7
+ def self.registered(app)
8
+ # GET /agents - Display the main agent management page.
9
+ app.get '/agents' do
10
+ # `self` is the Sinatra app instance in a route block
11
+ definition_store = instance_variable_get(:@definition_store)
12
+
13
+ view_agents_list = []
14
+ if definition_store
15
+ begin
16
+ agent_definitions = definition_store.list_definitions
17
+
18
+ active_agents_hash = instance_variable_get(:@agents)
19
+ view_agents_list = agent_definitions.map do |definition|
20
+ next unless definition && definition[:name] # Ensure definition and name are present
21
+
22
+ view_model = definition.dup # Create a mutable copy for the view
23
+ view_model[:configured_tools] = view_model.delete(:tools) || [] # Ensure it's an array
24
+ # Include agent_type in the view model, default to :llm if not present
25
+ view_model[:agent_type] = view_model[:agent_type]&.to_sym || :llm
26
+ # Running state is determined by the in-memory @agents hash, which
27
+ # is keyed by the agent's STRING name (the route-param form). The
28
+ # definition's :name is a Symbol, so normalize before the lookup.
29
+ view_model[:running] = active_agents_hash.key?(definition[:name].to_s)
30
+ view_model
31
+ end.compact # Remove any nils from failed definition fetches
32
+ rescue Legate::DefinitionStore::StoreError => e
33
+ logger.error("Store error fetching agent list (from AgentDefinitionRoutes): #{e.message}")
34
+ end
35
+ else
36
+ logger.error('Definition Store unavailable during GET /agents (from AgentDefinitionRoutes)')
37
+ end
38
+
39
+ instance_variable_set(:@view_agents, view_agents_list)
40
+ instance_variable_set(:@available_tools, Legate::GlobalToolManager.list_all_tools)
41
+ instance_variable_set(:@available_models, Legate::Web::App::AVAILABLE_MODELS) # Access constant via App class
42
+ slim :agents
43
+ end
44
+
45
+ # POST /agents - Create a new agent definition.
46
+ app.post '/agents' do
47
+ definition_store = instance_variable_get(:@definition_store)
48
+ halt 503, 'Definition Store unavailable.' unless definition_store
49
+
50
+ agent_name = params['name']&.strip
51
+ agent_description = params['description']&.strip
52
+ selected_tools = params['tools'] || []
53
+ selected_model = params['model']&.strip
54
+ selected_fallback = params['fallback_mode'] || 'error'
55
+ mcp_servers_json = params['mcp_servers_json']&.strip
56
+ instruction = params['instruction']&.strip
57
+ agent_type = params['agent_type']&.strip || 'llm'
58
+ planning_strategy = params['planning_strategy']&.strip
59
+ planning_strategy = 'plan' unless %w[plan react].include?(planning_strategy)
60
+ output_key = params['output_key']&.strip
61
+ output_key = output_key.empty? ? nil : output_key.to_sym if output_key
62
+
63
+ # Loop specific params
64
+ loop_max_iterations = params['loop_max_iterations']&.strip
65
+ loop_condition_state_key = params['loop_condition_state_key']&.strip
66
+ loop_condition_expected_value = params['loop_condition_expected_value']&.strip
67
+
68
+ # Get sub-agents for workflow agents
69
+ sub_agent_names = params['sub_agent_names'] || []
70
+
71
+ # Remove self from sub-agent selections to prevent circular references
72
+ sub_agent_names = sub_agent_names.reject { |name| name == agent_name }
73
+
74
+ # Validate agent_type
75
+ agent_type = 'llm' unless %w[llm sequential parallel loop].include?(agent_type)
76
+
77
+ mcp_servers_json_to_save = mcp_servers_json.nil? || mcp_servers_json.empty? ? '[]' : mcp_servers_json
78
+ model_to_save = selected_model && !selected_model.empty? ? selected_model : Legate::Agent::DEFAULT_MODEL
79
+
80
+ if agent_name.nil? || agent_name.empty? || agent_description.nil? || agent_description.empty?
81
+ status 400
82
+ halt "<div class='notification is-danger'>Name and description required.</div>"
83
+ end
84
+
85
+ begin
86
+ definition_params = {
87
+ name: agent_name,
88
+ description: agent_description,
89
+ tools: selected_tools,
90
+ model: model_to_save,
91
+ fallback_mode: selected_fallback, # Store will convert to symbol
92
+ mcp_servers_json: mcp_servers_json_to_save,
93
+ instruction: instruction,
94
+ agent_type: agent_type,
95
+ planning_strategy: planning_strategy,
96
+ output_key: output_key
97
+ }
98
+
99
+ # Add sub_agent_names or delegation_targets depending on type
100
+ unless sub_agent_names.empty?
101
+ if agent_type == 'llm'
102
+ definition_params[:delegation_targets] = sub_agent_names
103
+ else
104
+ definition_params[:sub_agent_names] = sub_agent_names
105
+ end
106
+ end
107
+
108
+ # Add loop params if type is loop
109
+ if agent_type == 'loop'
110
+ definition_params[:loop_max_iterations] = loop_max_iterations.to_i if loop_max_iterations && !loop_max_iterations.empty?
111
+ definition_params[:loop_condition_state_key] = loop_condition_state_key.to_sym if loop_condition_state_key && !loop_condition_state_key.empty?
112
+ definition_params[:loop_condition_expected_value] = loop_condition_expected_value if loop_condition_expected_value && !loop_condition_expected_value.empty?
113
+ end
114
+
115
+ definition_store.save_definition(**definition_params)
116
+ logger.info("Agent '#{agent_name}' definition saved (from AgentDefinitionRoutes)")
117
+ Legate::ActivityLog.safe_log(:agent_created, { name: agent_name })
118
+ rescue Legate::DefinitionStore::StoreError => e
119
+ logger.error("Store error saving agent definition (from AgentDefinitionRoutes): #{e.message}")
120
+ halt 500, 'Error saving agent definition.'
121
+ end
122
+
123
+ content_type :html
124
+ agent_data = {
125
+ name: agent_name, description: agent_description, running: false,
126
+ configured_tools: selected_tools, model: model_to_save,
127
+ fallback_mode: selected_fallback.to_sym, # Ensure symbol for partial
128
+ instruction: instruction,
129
+ agent_type: agent_type.to_sym, # Convert to symbol for the partial
130
+ is_new: true
131
+ }
132
+
133
+ # Include sub_agent_names if this is a workflow agent
134
+ agent_data[:sub_agent_names] = sub_agent_names if agent_type != 'llm' && !sub_agent_names.empty?
135
+
136
+ # available_tools needed by _agent_card partial
137
+ current_available_tools = Legate::GlobalToolManager.list_all_tools
138
+ agent_row_html = slim(:_agent_card, layout: false,
139
+ locals: { agent_info: agent_data, available_tools: current_available_tools })
140
+ oob_remove_message_html = "<tr id='no-agents-row' hx-swap-oob='true'></tr>"
141
+ headers 'HX-Trigger' => 'closeCreateAgentForm'
142
+ agent_row_html + oob_remove_message_html
143
+ end
144
+
145
+ # DELETE /agents/:name - Delete an agent definition.
146
+ app.delete '/agents/:name' do |name|
147
+ logger.info("Received request to delete agent '#{name}' (from AgentDefinitionRoutes)")
148
+ definition_store = instance_variable_get(:@definition_store)
149
+ active_agents_hash = instance_variable_get(:@agents)
150
+ halt 503, 'Definition Store unavailable.' unless definition_store
151
+
152
+ if active_agents_hash.key?(name)
153
+ logger.info("Stopping running agent '#{name}' before deletion (from AgentDefinitionRoutes)...")
154
+ begin
155
+ active_agents_hash[name].stop
156
+ active_agents_hash.delete(name)
157
+ logger.info("Agent '#{name}' stopped (from AgentDefinitionRoutes).")
158
+ rescue StandardError => e
159
+ logger.error("Error stopping agent (from AgentDefinitionRoutes): #{e.message}")
160
+ end
161
+ end
162
+ begin
163
+ definition_store.delete_definition(name)
164
+ logger.info("Agent '#{name}' definition deleted (from AgentDefinitionRoutes).")
165
+ Legate::ActivityLog.safe_log(:agent_deleted, { name: name })
166
+ status 200
167
+ body ''
168
+ rescue Legate::DefinitionStore::StoreError => e
169
+ logger.error("Store error deleting agent '#{name}' (from AgentDefinitionRoutes): #{e.message}")
170
+ halt 500, 'Database error during deletion.'
171
+ end
172
+ end
173
+
174
+ # POST /agents/:name/duplicate - Create a copy of an agent.
175
+ app.post '/agents/:name/duplicate' do |name|
176
+ logger.info("Received request to duplicate agent '#{name}'")
177
+ definition_store = instance_variable_get(:@definition_store)
178
+ halt 503, 'Definition Store unavailable.' unless definition_store
179
+
180
+ original_definition = definition_store.get_definition(name)
181
+ halt 404, 'Agent not found' unless original_definition
182
+
183
+ # Generate unique name for the copy
184
+ base_name = "Copy of #{name}"
185
+ new_name = base_name
186
+ counter = 1
187
+ while definition_store.get_definition(new_name)
188
+ counter += 1
189
+ new_name = "#{base_name} (#{counter})"
190
+ end
191
+
192
+ # Create the duplicate definition
193
+ new_definition = original_definition.dup
194
+ new_definition[:name] = new_name
195
+ new_definition[:description] = "Copy of: #{original_definition[:description]}"
196
+
197
+ begin
198
+ definition_store.save_definition(new_name, new_definition)
199
+ Legate::ActivityLog.safe_log(:agent_created, { name: new_name, source: 'duplicate' })
200
+ logger.info("Agent '#{name}' duplicated as '#{new_name}'")
201
+
202
+ # Redirect to the new agent
203
+ if request.xhr?
204
+ headers 'HX-Redirect' => "/agents/#{URI.encode_www_form_component(new_name)}"
205
+ status 200
206
+ body ''
207
+ else
208
+ redirect "/agents/#{URI.encode_www_form_component(new_name)}"
209
+ end
210
+ rescue Legate::DefinitionStore::StoreError => e
211
+ logger.error("Error duplicating agent: #{e.message}")
212
+ halt 500, 'Error duplicating agent.'
213
+ end
214
+ end
215
+
216
+ # GET /agents/:name/export - Export agent configuration as JSON.
217
+ app.get '/agents/:name/export' do |name|
218
+ logger.info("Received request to export agent '#{name}'")
219
+ definition_store = instance_variable_get(:@definition_store)
220
+ halt 503, 'Definition Store unavailable.' unless definition_store
221
+
222
+ agent_definition = definition_store.get_definition(name)
223
+ halt 404, 'Agent not found' unless agent_definition
224
+
225
+ # Prepare export data (clean up internal fields)
226
+ export_data = {
227
+ name: agent_definition[:name],
228
+ description: agent_definition[:description],
229
+ model: agent_definition[:model],
230
+ instruction: agent_definition[:instruction],
231
+ tools: agent_definition[:tools],
232
+ fallback_mode: agent_definition[:fallback_mode],
233
+ agent_type: agent_definition[:agent_type],
234
+ sub_agent_names: agent_definition[:sub_agent_names],
235
+ mcp_servers_json: agent_definition[:mcp_servers_json]
236
+ }.compact
237
+
238
+ content_type 'application/json'
239
+ attachment "#{name}.json"
240
+ JSON.pretty_generate(export_data)
241
+ end
242
+
243
+ # GET /agents/:name/download - Download agent as Ruby file.
244
+ app.get '/agents/:name/download' do |name|
245
+ logger.info("Received request to download agent '#{name}' as Ruby file")
246
+ definition_store = instance_variable_get(:@definition_store)
247
+ halt 503, 'Definition Store unavailable.' unless definition_store
248
+
249
+ agent_definition = definition_store.get_definition(name)
250
+ halt 404, 'Agent not found' unless agent_definition
251
+
252
+ # Ensure name is included in the definition hash
253
+ agent_definition[:name] ||= name
254
+
255
+ # Generate Ruby code
256
+ require 'legate/agent_code_generator'
257
+ ruby_code = Legate::AgentCodeGenerator.generate(agent_definition)
258
+
259
+ content_type 'application/x-ruby'
260
+ attachment "#{name.to_s.gsub(/[^a-zA-Z0-9_-]/, '_')}.rb"
261
+ ruby_code
262
+ end
263
+
264
+ # POST /agents/:name/save - Persist a (possibly runtime-created) agent to
265
+ # agents/<name>.rb so it survives a restart. Writes generated DSL code (no
266
+ # eval at request time) that the boot loader requires on next start.
267
+ app.post '/agents/:name/save' do |name|
268
+ content_type :json
269
+ definition_store = instance_variable_get(:@definition_store)
270
+ halt 503, json(error: 'Definition Store unavailable.') unless definition_store
271
+
272
+ agent_definition = definition_store.get_definition(name)
273
+ halt 404, json(error: 'Agent not found.') unless agent_definition
274
+ agent_definition[:name] ||= name
275
+
276
+ require 'legate/agent_code_generator'
277
+ require 'fileutils'
278
+ ruby_code = Legate::AgentCodeGenerator.generate(agent_definition)
279
+ safe_name = name.to_s.gsub(/[^a-zA-Z0-9_-]/, '_')
280
+ dir = File.join(Dir.pwd, 'agents')
281
+ path = File.join(dir, "#{safe_name}.rb")
282
+
283
+ begin
284
+ FileUtils.mkdir_p(dir)
285
+ File.write(path, ruby_code)
286
+ rescue SystemCallError => e
287
+ logger.error("Failed to save agent '#{name}' to #{path}: #{e.message}")
288
+ halt 500, json(error: "Could not write file (filesystem may be read-only): #{e.message}")
289
+ end
290
+
291
+ logger.info("Saved agent '#{name}' to #{path}")
292
+ json(ok: true, path: "agents/#{safe_name}.rb")
293
+ end
294
+
295
+ # GET /agents/:name - Display the detail page for a specific agent.
296
+ app.get '/agents/:name' do |name|
297
+ logger.info("GET /agents/#{name} route handler entered (from AgentDefinitionRoutes)")
298
+ definition_store = instance_variable_get(:@definition_store)
299
+ halt 503, 'Definition Store unavailable.' unless definition_store
300
+
301
+ agent_definition = nil
302
+ begin
303
+ agent_definition = definition_store.get_definition(name)
304
+ rescue Legate::DefinitionStore::StoreError => e
305
+ logger.error("Store error fetching definition for '#{name}' (from AgentDefinitionRoutes): #{e.message}")
306
+ halt 500, 'Error retrieving agent definition.'
307
+ end
308
+
309
+ unless agent_definition
310
+ logger.warn("Agent definition not found for '#{name}' in store (from AgentDefinitionRoutes).")
311
+ halt 404,
312
+ slim(:error_404, locals: { title: 'Agent Not Found', message: "Definition for '#{name}' not found." })
313
+ end
314
+
315
+ mcp_display_string = begin
316
+ parsed = JSON.parse(agent_definition[:mcp_servers_json])
317
+ parsed.is_a?(Array) && parsed.empty? ? 'No MCP Server(s) Configured.' : pretty_json(parsed)
318
+ rescue JSON::ParserError
319
+ agent_definition[:mcp_servers_json]
320
+ end
321
+
322
+ # Running state is determined by in-memory @agents hash
323
+ active_agents_hash = instance_variable_get(:@agents)
324
+ is_running = active_agents_hash.key?(name)
325
+
326
+ # Calculate tool count for header display
327
+ tool_count = agent_definition[:tools]&.size || 0
328
+
329
+ instance_variable_set(:@view_agent_data, {
330
+ name: name,
331
+ description: agent_definition[:description],
332
+ running: is_running,
333
+ model: agent_definition[:model],
334
+ fallback_mode: agent_definition[:fallback_mode],
335
+ instruction: agent_definition[:instruction],
336
+ mcp_servers_json: agent_definition[:mcp_servers_json],
337
+ mcp_display_string: mcp_display_string,
338
+ configured_tool_names: agent_definition[:tools],
339
+ tool_count: tool_count,
340
+ # Include agent type and sub-agent names for hierarchy display
341
+ agent_type: agent_definition[:agent_type]&.to_sym || :llm,
342
+ planning_strategy: agent_definition[:planning_strategy]&.to_sym || :plan,
343
+ # For LLM agents, use delegation_targets as 'sub-agents' for display purposes
344
+ sub_agent_names: (agent_definition[:agent_type]&.to_sym == :llm ? agent_definition[:delegation_targets] : agent_definition[:sub_agent_names]) || [],
345
+ # Last run timestamp for display
346
+ last_run_at: agent_definition[:last_run_at],
347
+ # Additional config
348
+ output_key: agent_definition[:output_key],
349
+ loop_max_iterations: agent_definition[:loop_max_iterations],
350
+ loop_condition_state_key: agent_definition[:loop_condition_state_key],
351
+ loop_condition_expected_value: agent_definition[:loop_condition_expected_value]
352
+ })
353
+
354
+ # Tool metadata fetching logic (similar to what's in app.rb for this route)
355
+ all_native_tools_metadata = Legate::GlobalToolManager.list_all_tools.map do |tm|
356
+ params_array = []
357
+ if tm[:parameters].is_a?(Hash) && !tm[:parameters].empty?
358
+ tm[:parameters].each { |pn, d|
359
+ params_array << { name: pn, type: d[:type], description: d[:description], required: d[:required] }
360
+ }
361
+ end
362
+ tm.merge(parameters: params_array, source: :native, source_detail: 'Native')
363
+ end
364
+
365
+ resolved = resolve_available_tools(agent_definition[:mcp_servers_json], all_native_tools_metadata,
366
+ log_context: "GET /agents/#{name}")
367
+ all_available_tools_map = resolved[:map]
368
+ configured_tool_syms = agent_definition[:tools].map(&:to_sym)
369
+ view_tools = configured_tool_syms.map { |ts| all_available_tools_map[ts] }.compact
370
+
371
+ needs_check_job = view_tools.any? { |tm|
372
+ tm[:async] == true || Legate::GlobalToolManager.find_class(tm[:name])&.ancestors&.include?(Legate::Tools::BaseAsyncJobTool)
373
+ }
374
+ if needs_check_job && !view_tools.any? { |t| t[:name] == :check_job_status }
375
+ status_tool_meta = all_available_tools_map[:check_job_status]
376
+ if status_tool_meta
377
+ view_tools << status_tool_meta.dup.merge(
378
+ description: "(Implicitly added) #{status_tool_meta[:description]}", source_detail: 'Native (Implicit)'
379
+ )
380
+ end
381
+ end
382
+
383
+ slim :agent, locals: { view_configured_tools: view_tools.sort_by! { |t|
384
+ t[:name].to_s
385
+ }, mcp_tool_results: resolved[:mcp_results] }
386
+ end
387
+
388
+ # GET /agents/:name/edit/:field - Show edit form for a specific agent field.
389
+ app.get '/agents/:name/edit/:field' do |name, field|
390
+ supported_fields = %w[description model tools fallback mcp instruction hierarchy type output_key]
391
+ halt 404, "Editing field '#{field}' not supported." unless supported_fields.include?(field)
392
+ definition_store = instance_variable_get(:@definition_store)
393
+ halt 503, 'Definition Store unavailable.' unless definition_store
394
+
395
+ agent_definition = definition_store.get_definition(name)
396
+ halt 404, 'Agent definition not found.' unless agent_definition
397
+
398
+ agent_data = {
399
+ name: name, description: agent_definition[:description], model: agent_definition[:model],
400
+ fallback_mode: agent_definition[:fallback_mode],
401
+ mcp_servers_json: agent_definition[:mcp_servers_json],
402
+ instruction: agent_definition[:instruction],
403
+ agent_type: agent_definition[:agent_type]&.to_sym || :llm,
404
+ planning_strategy: agent_definition[:planning_strategy]&.to_sym || :plan,
405
+ output_key: agent_definition[:output_key],
406
+ sub_agent_names: (agent_definition[:agent_type]&.to_sym == :llm ? agent_definition[:delegation_targets] : agent_definition[:sub_agent_names]) || [],
407
+ loop_max_iterations: agent_definition[:loop_max_iterations],
408
+ loop_condition_state_key: agent_definition[:loop_condition_state_key],
409
+ loop_condition_expected_value: agent_definition[:loop_condition_expected_value]
410
+ }
411
+
412
+ view_locals = { agent_data: agent_data }
413
+
414
+ if field == 'model'
415
+ view_locals[:available_models] = Legate::Web::App::AVAILABLE_MODELS
416
+ elsif field == 'tools'
417
+ # Ensure configured_tool_names is an array of strings for the view's .include? check
418
+ view_locals[:configured_tool_names] = agent_definition[:tools].map(&:to_s)
419
+ native_tools = Legate::GlobalToolManager.list_all_tools
420
+
421
+ mcp_configs = []
422
+ begin
423
+ mcp_json = agent_definition[:mcp_servers_json]
424
+ mcp_configs = JSON.parse(mcp_json) if mcp_json && !mcp_json.empty? && mcp_json != '[]'
425
+ rescue JSON::ParserError => e
426
+ logger.error("Invalid MCP JSON for agent '#{name}' (edit tools - AgentDefinitionRoutes): #{e.message}")
427
+ end
428
+ mcp_results = fetch_mcp_tools(mcp_configs)
429
+
430
+ fetched_mcp_meta = []
431
+ mcp_results.each do |res|
432
+ next unless res[:status] == :success && res[:tools]
433
+
434
+ res[:tools].each do |schema|
435
+ params = Legate::Mcp::Util::SchemaConverter.json_to_legate(schema.dig(:inputSchema, 'properties') || {},
436
+ schema.dig(:inputSchema, 'required') || [])
437
+ fetched_mcp_meta << { name: schema[:name].to_sym, description: schema[:description] || '',
438
+ parameters: params }
439
+ end
440
+ end
441
+ view_locals[:all_available_tools] = (native_tools + fetched_mcp_meta).uniq { |t|
442
+ t[:name]
443
+ }.sort_by { |t| t[:name].to_s }
444
+ elsif field == 'hierarchy'
445
+ # Get all available agent definitions for sub-agent selection
446
+ begin
447
+ all_agent_definitions = definition_store.list_definitions
448
+ # Filter out the current agent from available sub-agents to prevent self-reference
449
+ filtered_agent_definitions = all_agent_definitions.reject { |def_data| def_data[:name].to_s == name.to_s }
450
+ logger.info("Agent '#{name}' hierarchy edit view: Filtered out self-reference from #{all_agent_definitions.size} to #{filtered_agent_definitions.size} agents.")
451
+ view_locals[:all_agent_definitions] = filtered_agent_definitions || []
452
+ rescue Legate::DefinitionStore::StoreError => e
453
+ logger.error("Store error fetching agent list for hierarchy edit: #{e.message}")
454
+ view_locals[:all_agent_definitions] = []
455
+ end
456
+ end
457
+ slim :"_edit_agent_#{field}", layout: false, locals: view_locals
458
+ end
459
+
460
+ # GET /agents/:name/display/tool_table - Render the tool table display partial.
461
+ # NOTE: This specific route must be defined BEFORE the generic /display/:field route
462
+ app.get '/agents/:name/display/tool_table' do |name|
463
+ definition_store = instance_variable_get(:@definition_store)
464
+ halt 503, 'Definition Store unavailable.' unless definition_store
465
+ agent_definition = definition_store.get_definition(name)
466
+ halt 404, 'Agent not found' unless agent_definition
467
+
468
+ agent_data = {
469
+ name: name, description: agent_definition[:description], model: agent_definition[:model],
470
+ fallback_mode: agent_definition[:fallback_mode], mcp_servers_json: agent_definition[:mcp_servers_json],
471
+ running: instance_variable_get(:@agents).key?(name)
472
+ }
473
+ configured_tool_names = agent_definition[:tools]
474
+ configured_tool_syms = configured_tool_names.map(&:to_sym)
475
+
476
+ all_native_tools_metadata = Legate::GlobalToolManager.list_all_tools.map { |tm|
477
+ tm.merge(source: :native, source_detail: 'Native')
478
+ }
479
+
480
+ resolved = resolve_available_tools(agent_data[:mcp_servers_json], all_native_tools_metadata,
481
+ log_context: "display_tool_table #{name}")
482
+ all_available_tools_map = resolved[:map]
483
+ mcp_tool_fetch_results = resolved[:mcp_results]
484
+ view_configured_tools_list = configured_tool_syms.map { |ts| all_available_tools_map[ts] }.compact
485
+
486
+ if view_configured_tools_list.any? { |tm|
487
+ Legate::GlobalToolManager.find_class(tm[:name])&.ancestors&.include?(Legate::Tools::BaseAsyncJobTool)
488
+ }
489
+ status_tool_meta = all_available_tools_map[:check_job_status]
490
+ view_configured_tools_list << status_tool_meta if status_tool_meta && !view_configured_tools_list.any? { |t| t[:name] == :check_job_status }
491
+ end
492
+
493
+ slim :_agent_tool_table, layout: false, locals: {
494
+ agent_data: agent_data,
495
+ view_configured_tools: view_configured_tools_list.sort_by { |t| t[:name].to_s },
496
+ mcp_tool_results: mcp_tool_fetch_results
497
+ }
498
+ end
499
+
500
+ # GET /agents/:name/display/:field - Display an agent field (after edit cancel).
501
+ app.get '/agents/:name/display/:field' do |name, field|
502
+ # 'tools' is intentionally excluded: there is no _display_agent_tools
503
+ # template; the tools view is the dedicated GET .../display/tool_table
504
+ # route. Listing it here previously produced a 500 for /display/tools.
505
+ supported_fields = %w[description model fallback mcp instruction hierarchy type output_key]
506
+ halt 404, "Displaying field '#{field}' not supported." unless supported_fields.include?(field)
507
+ definition_store = instance_variable_get(:@definition_store)
508
+ halt 503, 'Definition Store unavailable.' unless definition_store
509
+
510
+ agent_definition = definition_store.get_definition(name)
511
+ halt 404, 'Agent definition not found.' unless agent_definition
512
+
513
+ response_locals = { show_edit_button: true }
514
+ agent_data_for_display = {
515
+ name: name, description: agent_definition[:description], model: agent_definition[:model],
516
+ fallback_mode: agent_definition[:fallback_mode],
517
+ mcp_servers_json: agent_definition[:mcp_servers_json],
518
+ instruction: agent_definition[:instruction],
519
+ agent_type: agent_definition[:agent_type]&.to_sym || :llm
520
+ }
521
+
522
+ if field == 'mcp'
523
+ mcp_json_val = agent_definition[:mcp_servers_json]
524
+ agent_data_for_display[:mcp_display_string] = begin
525
+ parsed = JSON.parse(mcp_json_val)
526
+ parsed.is_a?(Array) && parsed.empty? ? 'No MCP Server(s) Configured.' : pretty_json(parsed)
527
+ rescue JSON::ParserError
528
+ mcp_json_val
529
+ end
530
+ elsif field == 'hierarchy'
531
+ # Add sub_agent_names for hierarchy display
532
+ agent_data_for_display[:sub_agent_names] = agent_definition[:sub_agent_names] || []
533
+ agent_data_for_display[:agent_type] = agent_definition[:agent_type]&.to_sym || :llm
534
+ elsif field == 'type'
535
+ # Add agent_type and planning_strategy for type display
536
+ agent_data_for_display[:agent_type] = agent_definition[:agent_type]&.to_sym || :llm
537
+ agent_data_for_display[:planning_strategy] = agent_definition[:planning_strategy]&.to_sym || :plan
538
+ elsif field == 'output_key'
539
+ # Add output_key for display
540
+ agent_data_for_display[:output_key] = agent_definition[:output_key]
541
+ end
542
+ response_locals[:agent_data] = agent_data_for_display
543
+
544
+ slim :"_display_agent_#{field}", layout: false, locals: response_locals
545
+ end
546
+
547
+ # PUT /agents/:name/update/:field - Update a specific field of an agent definition.
548
+ app.put '/agents/:name/update/:field' do |name, field|
549
+ supported_fields = %w[description model tools fallback mcp instruction type hierarchy output_key]
550
+ halt 404, "Updating field '#{field}' not supported." unless supported_fields.include?(field)
551
+ definition_store = instance_variable_get(:@definition_store)
552
+ active_agents_hash = instance_variable_get(:@agents)
553
+ halt 503, 'Definition Store unavailable.' unless definition_store
554
+
555
+ field_to_update_in_store = case field
556
+ when 'fallback' then 'fallback_mode'
557
+ when 'mcp' then 'mcp_servers_json'
558
+ when 'type' then 'agent_type'
559
+ when 'output_key' then 'output_key'
560
+ else field
561
+ end
562
+ new_value_for_store = nil
563
+ agent_data_for_display_partial = { name: name }
564
+
565
+ case field
566
+ when 'output_key'
567
+ new_value_for_store = params['value']&.strip
568
+ new_value_for_store = new_value_for_store.empty? ? nil : new_value_for_store.to_sym
569
+ agent_data_for_display_partial[:output_key] = new_value_for_store
570
+ when 'tools'
571
+ current_definition = definition_store.get_definition(name)
572
+ halt 404, 'Agent not found for tool update.' unless current_definition
573
+ mcp_json = current_definition[:mcp_servers_json]
574
+ native_tool_names = Legate::GlobalToolManager.list_all_tools.map { |t| t[:name].to_s }
575
+ mcp_configs = begin
576
+ JSON.parse(mcp_json)
577
+ rescue StandardError
578
+ []
579
+ end
580
+ mcp_results = fetch_mcp_tools(mcp_configs)
581
+ mcp_tool_names = mcp_results.flat_map { |res|
582
+ if res[:status] == :success
583
+ res[:tools].map { |t|
584
+ t[:name].to_s
585
+ }
586
+ else
587
+ []
588
+ end
589
+ }.uniq
590
+ all_valid_tool_names = (native_tool_names + mcp_tool_names).uniq
591
+
592
+ submitted_tools = params['tools'] || []
593
+ new_value_for_store = submitted_tools.select { |st| all_valid_tool_names.include?(st) }
594
+ # For display partial:
595
+ # Rebuild metadata for validated tools
596
+ all_native_meta = Legate::GlobalToolManager.list_all_tools.map do |tm|
597
+ params_array = []
598
+ if tm[:parameters].is_a?(Hash) && !tm[:parameters].empty?
599
+ tm[:parameters].each { |pn, d|
600
+ params_array << { name: pn, type: d[:type], description: d[:description], required: d[:required] }
601
+ }
602
+ end
603
+ tm.merge(parameters: params_array, source: :native, source_detail: 'Native')
604
+ end
605
+ fetched_mcp_meta = []
606
+ mcp_results.each do |res|
607
+ next unless res[:status] == :success && res[:tools]
608
+
609
+ res[:tools].each do |schema|
610
+ params = Legate::Mcp::Util::SchemaConverter.json_to_legate(
611
+ schema.dig(:inputSchema, 'properties') || {}, schema.dig(:inputSchema, 'required') || []
612
+ )
613
+ fetched_mcp_meta << { name: schema[:name].to_sym, description: schema[:description] || '',
614
+ parameters: params, source: :mcp, source_detail: "MCP (#{res[:server]})" }
615
+ end
616
+ end
617
+ all_available_meta_map = (all_native_meta + fetched_mcp_meta).each_with_object({}) { |tool, map|
618
+ map[tool[:name]] ||= tool
619
+ }
620
+ agent_data_for_display_partial[:view_configured_tools] = new_value_for_store.map { |tn|
621
+ all_available_meta_map[tn.to_sym]
622
+ }.compact
623
+ agent_data_for_display_partial[:mcp_tool_results] = mcp_results # For errors
624
+ when 'mcp'
625
+ submitted_json = params['value']&.strip
626
+ new_value_for_store = submitted_json.nil? || submitted_json.empty? ? '[]' : submitted_json
627
+ begin
628
+ parsed = JSON.parse(new_value_for_store)
629
+ raise JSON::ParserError, 'Input must be a valid JSON array.' unless parsed.is_a?(Array)
630
+ rescue JSON::ParserError => e
631
+ current_def = definition_store.get_definition(name)
632
+ edit_locals = {
633
+ agent_data: { name: name,
634
+ mcp_servers_json: current_def ? current_def[:mcp_servers_json] : new_value_for_store }, error_message: "Invalid JSON: #{e.message}"
635
+ }
636
+ halt 200, slim(:_edit_agent_mcp, layout: false, locals: edit_locals) # Return 200 for HTMX form error display
637
+ end
638
+ agent_data_for_display_partial[:mcp_servers_json] = new_value_for_store
639
+ agent_data_for_display_partial[:mcp_display_string] =
640
+ JSON.parse(new_value_for_store).empty? ? 'No MCP Server(s) Configured.' : pretty_json(JSON.parse(new_value_for_store))
641
+
642
+ when 'fallback'
643
+ submitted_value = params['value']&.strip
644
+ unless %w[error echo].include?(submitted_value)
645
+ current_def = definition_store.get_definition(name)
646
+ edit_locals = {
647
+ agent_data: { name: name,
648
+ fallback_mode: current_def ? current_def[:fallback_mode] : :error }, error_message: 'Invalid fallback.'
649
+ }
650
+ halt 400, slim(:_edit_agent_fallback, layout: false, locals: edit_locals)
651
+ end
652
+ new_value_for_store = submitted_value.to_sym
653
+ agent_data_for_display_partial[:fallback_mode] = new_value_for_store
654
+ when 'type'
655
+ submitted_value = params['agent_type']&.strip
656
+ unless %w[llm sequential parallel loop].include?(submitted_value)
657
+ current_def = definition_store.get_definition(name)
658
+ edit_locals = {
659
+ agent_data: { name: name, agent_type: current_def ? current_def[:agent_type]&.to_sym : :llm },
660
+ error_message: 'Invalid agent type.'
661
+ }
662
+ halt 400, slim(:_edit_agent_type, layout: false, locals: edit_locals)
663
+ end
664
+
665
+ # Planning strategy is edited alongside the type (it governs how an
666
+ # :llm agent runs). Persist it here; the generic update below handles
667
+ # agent_type itself.
668
+ submitted_strategy = params['planning_strategy']&.strip
669
+ submitted_strategy = 'plan' unless %w[plan react].include?(submitted_strategy)
670
+ definition_store.update_definition(name, planning_strategy: submitted_strategy.to_sym)
671
+ agent_data_for_display_partial[:planning_strategy] = submitted_strategy.to_sym
672
+
673
+ # Check if switching to LLM type and clear sub-agent lists if so
674
+ if submitted_value == 'llm'
675
+ # Get current definition to check current type
676
+ current_def = definition_store.get_definition(name)
677
+ current_type = current_def ? current_def[:agent_type]&.to_s : nil
678
+
679
+ # Only clear sub-agents if switching from a workflow type to LLM
680
+ if current_type && %w[sequential parallel loop].include?(current_type)
681
+ # Update sub-agent fields first
682
+ begin
683
+ definition_store.update_definition(name, {
684
+ sub_agent_names: [],
685
+ sequential_sub_agent_names: [],
686
+ parallel_sub_agent_names: [],
687
+ loop_sub_agent_names: []
688
+ })
689
+ logger.info("Agent '#{name}' switched from '#{current_type}' to 'llm', cleared all sub-agent lists.")
690
+ rescue StandardError => e
691
+ logger.error("Failed to clear sub-agent lists for agent '#{name}': #{e.message}")
692
+ end
693
+ end
694
+ end
695
+
696
+ new_value_for_store = submitted_value
697
+ agent_data_for_display_partial[:agent_type] = submitted_value.to_sym
698
+ when 'instruction', 'description', 'model'
699
+ new_value_for_store = params['value']&.strip || (field == 'instruction' ? '' : nil)
700
+ if new_value_for_store.nil? && field != 'instruction' # Description and model cannot be nil (empty is ok for description)
701
+ current_def = definition_store.get_definition(name)
702
+ edit_locals = {
703
+ agent_data: { name: name, description: current_def[:description], model: current_def[:model],
704
+ instruction: current_def[:instruction] }, error_message: "#{field.capitalize} cannot be empty."
705
+ }
706
+ halt 400, slim(:"_edit_agent_#{field}", layout: false, locals: edit_locals)
707
+ end
708
+ agent_data_for_display_partial[field.to_sym] = new_value_for_store
709
+ when 'hierarchy'
710
+ # Get selected sub-agent names from the form
711
+ sub_agent_names = params['sub_agent_names'] || []
712
+
713
+ # Update the definition via a separate field
714
+ begin
715
+ update_success = definition_store.update_definition(name, sub_agent_names: sub_agent_names)
716
+ halt 404, 'Agent not found for update.' unless update_success
717
+ logger.info("Agent '#{name}' hierarchy updated with #{sub_agent_names.size} sub-agents (from AgentDefinitionRoutes)")
718
+
719
+ # Refresh agent data for display
720
+ updated_definition = definition_store.get_definition(name)
721
+ agent_data = {
722
+ name: name,
723
+ description: updated_definition[:description],
724
+ agent_type: updated_definition[:agent_type]&.to_sym || :llm,
725
+ sub_agent_names: updated_definition[:sub_agent_names] || [],
726
+ show_edit_button: true
727
+ }
728
+
729
+ # Return the updated display partial directly
730
+ return slim :_display_agent_hierarchy, layout: false, locals: { agent_data: agent_data }
731
+ rescue Legate::DefinitionStore::StoreError => e
732
+ logger.error("Store error updating agent hierarchy: #{e.message}")
733
+ halt 500, 'Error updating agent hierarchy.'
734
+ end
735
+ end
736
+
737
+ begin
738
+ update_success = definition_store.update_definition(name,
739
+ { field_to_update_in_store.to_sym => new_value_for_store })
740
+ halt 404, 'Agent not found for update.' unless update_success
741
+ logger.info("Agent '#{name}' field '#{field_to_update_in_store}' updated (from AgentDefinitionRoutes).")
742
+
743
+ was_running = active_agents_hash.key?(name)
744
+ if was_running
745
+ logger.info("Agent '#{name}' config updated while running. Triggering auto-restart (from AgentDefinitionRoutes).")
746
+ send(:_stop_agent, name)
747
+ newly_started_agent = send(:_start_agent, name)
748
+ agent_data_for_display_partial[:running] = !newly_started_agent.nil?
749
+ headers 'HX-Trigger-After-Swap' => (agent_data_for_display_partial[:running] ? 'showRestartToast' : 'showRestartErrorToast')
750
+ else
751
+ agent_data_for_display_partial[:running] = false
752
+ end
753
+
754
+ # Re-fetch full definition for display consistency
755
+ full_updated_def = definition_store.get_definition(name)
756
+ agent_data_for_display_partial.merge!(
757
+ description: full_updated_def[:description], model: full_updated_def[:model],
758
+ fallback_mode: full_updated_def[:fallback_mode], mcp_servers_json: full_updated_def[:mcp_servers_json],
759
+ instruction: full_updated_def[:instruction]
760
+ )
761
+ # Ensure mcp_display_string is set if field was 'mcp'
762
+ if field == 'mcp'
763
+ agent_data_for_display_partial[:mcp_display_string] ||= JSON.parse(new_value_for_store).empty? ? 'No MCP Server(s) Configured.' : pretty_json(JSON.parse(new_value_for_store))
764
+ end
765
+
766
+ response_locals_for_display = { agent_data: agent_data_for_display_partial, show_edit_button: true }
767
+
768
+ if field == 'tools'
769
+ # For tools, the _agent_tool_table partial is rendered
770
+ # It expects :view_configured_tools and :mcp_tool_results
771
+ # We already prepared agent_data_for_display_partial[:view_configured_tools]
772
+ # and agent_data_for_display_partial[:mcp_tool_results]
773
+ slim :_agent_tool_table, layout: false, locals: agent_data_for_display_partial # Pass the whole hash
774
+ else
775
+ response_html = slim :"_display_agent_#{field}", layout: false, locals: response_locals_for_display
776
+
777
+ # Add OOB update for hierarchy section if changing to LLM type
778
+ if field == 'type' && new_value_for_store == 'llm'
779
+ # Add an out-of-band swap to update the hierarchy section with empty sub-agents
780
+ empty_hierarchy_data = {
781
+ name: name,
782
+ agent_type: :llm,
783
+ sub_agent_names: [],
784
+ show_edit_button: true
785
+ }
786
+ response_html += '<div id="agent-hierarchy-display" hx-swap-oob="true">' +
787
+ slim(:_display_agent_hierarchy, layout: false, locals: { agent_data: empty_hierarchy_data }) +
788
+ '</div>'
789
+ end
790
+
791
+ response_html
792
+ end
793
+ rescue Legate::DefinitionStore::StoreError => e
794
+ logger.error("Store error updating agent '#{name}' (from AgentDefinitionRoutes): #{e.message}")
795
+ halt 500, 'Error updating agent definition.'
796
+ rescue ArgumentError => e # From store validation
797
+ halt 400, "Invalid input: #{e.message}"
798
+ end
799
+ end
800
+ end
801
+ end
802
+ end
803
+ end