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,68 @@
1
+ # File: lib/legate/session_service/base.rb
2
+ # frozen_string_literal: true
3
+
4
+ module Legate
5
+ module SessionService
6
+ # Base class for session services
7
+ class Base
8
+ # Returns whether this service persists state
9
+ # @return [Boolean] true if state is persisted, false otherwise
10
+ def persistent?
11
+ false
12
+ end
13
+
14
+ # Saves scoped state
15
+ # @param scope [String] The scope of the state ('user', 'app', or 'temp')
16
+ # @param key [String] The key to save
17
+ # @param value [Object] The value to save
18
+ # @raise [NotImplementedError] Must be implemented by subclasses
19
+ def save_scoped_state(scope, key, value)
20
+ raise NotImplementedError, "#{self.class} must implement #save_scoped_state"
21
+ end
22
+
23
+ # Loads scoped state
24
+ # @param scope [String] The scope of the state ('user', 'app', or 'temp')
25
+ # @param key [String] The key to load
26
+ # @return [Object, nil] The loaded value or nil if not found
27
+ # @raise [NotImplementedError] Must be implemented by subclasses
28
+ def load_scoped_state(scope, key)
29
+ raise NotImplementedError, "#{self.class} must implement #load_scoped_state"
30
+ end
31
+
32
+ # Clears scoped state
33
+ # @param scope [String] The scope of the state ('user', 'app', or 'temp')
34
+ # @param key [String] The key to clear
35
+ # @raise [NotImplementedError] Must be implemented by subclasses
36
+ def clear_scoped_state(scope, key)
37
+ raise NotImplementedError, "#{self.class} must implement #clear_scoped_state"
38
+ end
39
+
40
+ def append_event(session_id:, event:)
41
+ raise NotImplementedError, "#{self.class.name} must implement #append_event."
42
+ end
43
+
44
+ # Sets a key-value pair in the state associated with the session.
45
+ # This typically involves finding the session and then calling the session object's
46
+ # own state management methods (e.g., session.set_state(key, value)).
47
+ # @param session_id [String] The ID of the session.
48
+ # @param key [Symbol] The key for the state entry (should not have service-level prefixes like user: or app:).
49
+ # @param value [Object] The value to store.
50
+ # @return [void]
51
+ # @raise [NotImplementedError] If the subclass does not implement this method.
52
+ def set_state(session_id:, key:, value:)
53
+ raise NotImplementedError, "#{self.class.name} must implement #set_state."
54
+ end
55
+
56
+ # Retrieves a value from the state associated with the session.
57
+ # This typically involves finding the session and then calling the session object's
58
+ # own state management methods (e.g., session.get_state(key)).
59
+ # @param session_id [String] The ID of the session.
60
+ # @param key [Symbol] The key for the state entry (should not have service-level prefixes).
61
+ # @return [Object, nil] The value if found, or nil.
62
+ # @raise [NotImplementedError] If the subclass does not implement this method.
63
+ def get_state(session_id:, key:)
64
+ raise NotImplementedError, "#{self.class.name} must implement #get_state."
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,74 @@
1
+ # File: lib/legate/session_service/event_broadcast.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'concurrent'
5
+
6
+ module Legate
7
+ module SessionService
8
+ # Session-scoped pub/sub for streaming agent events (R3).
9
+ #
10
+ # A session service includes this mixin and calls {#broadcast_event} right
11
+ # after persisting an event in #append_event. Consumers (a Sinatra SSE
12
+ # response, a CLI, a test) {#subscribe} to a session_id and receive each
13
+ # event as it is appended, then {#unsubscribe} when done.
14
+ #
15
+ # Delivery is synchronous and in-order on the appending thread; a subscriber
16
+ # that raises is isolated (logged, never breaks persistence or other
17
+ # subscribers). Subscribers are keyed by session_id, so concurrent runs on
18
+ # different sessions never cross.
19
+ module EventBroadcast
20
+ # Guards the one-time creation of the subscriber registry. Shared across
21
+ # instances, but only contended on each instance's very first subscribe —
22
+ # negligible, and it avoids depending on the including class's #initialize.
23
+ INIT_LOCK = Mutex.new
24
+ private_constant :INIT_LOCK
25
+
26
+ # @param session_id [String]
27
+ # @yieldparam event [Legate::Event] each event appended to this session
28
+ # @return [Object] an opaque handle to pass to {#unsubscribe}
29
+ def subscribe(session_id, &listener)
30
+ raise ArgumentError, 'subscribe requires a block' unless listener
31
+
32
+ key = session_id.to_s
33
+ subscribers = event_subscribers.compute_if_absent(key) { Concurrent::Array.new }
34
+ subscribers << listener
35
+ [key, listener]
36
+ end
37
+
38
+ # Removes a subscription created by {#subscribe}. Safe to call twice / with nil.
39
+ # @param handle [Object] the value returned by {#subscribe}
40
+ def unsubscribe(handle)
41
+ return unless handle.is_a?(Array)
42
+
43
+ key, listener = handle
44
+ subscribers = event_subscribers[key]
45
+ return unless subscribers
46
+
47
+ subscribers.delete(listener)
48
+ event_subscribers.delete(key) if subscribers.empty?
49
+ end
50
+
51
+ # Notifies subscribers of `session_id` that `event` was appended.
52
+ # @return [void]
53
+ def broadcast_event(session_id, event)
54
+ subscribers = event_subscribers[session_id.to_s]
55
+ return if subscribers.nil? || subscribers.empty?
56
+
57
+ subscribers.each do |listener|
58
+ listener.call(event)
59
+ rescue StandardError => e
60
+ Legate.logger.error("EventBroadcast: subscriber raised #{e.class}: #{e.message}")
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ # Thread-safe lazy registry: the fast path is lock-free; the first caller
67
+ # per instance takes INIT_LOCK so concurrent first-subscribers can't each
68
+ # create (and orphan) a separate map.
69
+ def event_subscribers
70
+ @event_subscribers || INIT_LOCK.synchronize { @event_subscribers ||= Concurrent::Map.new }
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,188 @@
1
+ # File: lib/legate/session_service/in_memory.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'concurrent'
5
+ require_relative 'base'
6
+ require_relative 'event_broadcast'
7
+ require_relative '../session'
8
+ require_relative '../event'
9
+
10
+ module Legate
11
+ module SessionService
12
+ # Stores sessions entirely in memory. Data is lost on application restart.
13
+ # Useful for local development, testing, and simple use cases.
14
+ class InMemory < Base
15
+ include EventBroadcast
16
+
17
+ attr_reader :sessions, :scoped_states
18
+
19
+ def initialize
20
+ @sessions = Concurrent::Map.new
21
+ @scoped_states = Concurrent::Map.new
22
+ Legate.logger.info('InMemorySessionService initialized.')
23
+ end
24
+
25
+ def persistent?
26
+ false
27
+ end
28
+
29
+ # Creates a new session in memory.
30
+ # @param app_name [String] Identifier for the agent application.
31
+ # @param user_id [String] Identifier for the user initiating the session.
32
+ # @param session_id [String, nil] Optional explicit session id (defaults to a generated UUID).
33
+ # @param initial_state [Hash] Optional initial data for the session state.
34
+ # @return [Legate::Session] The newly created session object.
35
+ def create_session(app_name:, user_id:, session_id: nil, initial_state: {})
36
+ # Fix: Ensure keys are symbols before passing to Session constructor
37
+ symbolized_state = initial_state.transform_keys { |k|
38
+ begin
39
+ k.to_sym
40
+ rescue StandardError
41
+ k
42
+ end
43
+ }
44
+ session = Legate::Session.new(
45
+ app_name: app_name,
46
+ user_id: user_id,
47
+ id: session_id,
48
+ initial_state: symbolized_state,
49
+ session_service: self
50
+ )
51
+ @sessions[session.id] = session
52
+ Legate.logger.info("Created session: #{session.id} for app:#{app_name}, user:#{user_id}")
53
+ session
54
+ end
55
+
56
+ # Retrieves a session from memory by its ID.
57
+ # @param session_id [String] The unique ID of the session to retrieve.
58
+ # @return [Legate::Session, nil] The session object if found, otherwise nil.
59
+ def get_session(session_id:)
60
+ session = @sessions[session_id]
61
+ if session
62
+ Legate.logger.debug("Retrieved session: #{session_id}")
63
+ else
64
+ Legate.logger.warn("Session not found: #{session_id}")
65
+ end
66
+ session
67
+ end
68
+
69
+ # --- DEPRECATED ---
70
+ # Saves the session state (in memory this just means it's already updated).
71
+ # In a persistent store, this would write changes. Here, we just update the timestamp.
72
+ # NOTE: Events and state updates should be done via #append_event for atomicity.
73
+ # This method mainly exists for interface compatibility if needed but should be avoided.
74
+ # @param session [Legate::Session] The session object to "save".
75
+ # @return [Boolean] True if the session exists in memory.
76
+ def save_session(session:)
77
+ if @sessions.key?(session.id)
78
+ session.updated_at = Time.now.utc # Ensure timestamp reflects save attempt
79
+ Legate.logger.warn('InMemorySessionService#save_session called (likely unnecessary). Use append_event.')
80
+ true
81
+ else
82
+ Legate.logger.error("Attempted to save non-existent session: #{session.id}")
83
+ false
84
+ end
85
+ end
86
+
87
+ # --- REVISED METHOD ---
88
+ # Appends an event to a session and merges state updates from the event's state_delta.
89
+ # This should be the primary way to modify a session during a turn.
90
+ # @param session_id [String] The ID of the session to update.
91
+ # @param event [Legate::Event] The event to append. Must be an instance of Legate::Event.
92
+ # @return [Boolean] True if successful, false if session not found or event is invalid.
93
+ def append_event(session_id:, event:)
94
+ session = get_session(session_id: session_id)
95
+ return false unless session
96
+
97
+ session.add_event(event)
98
+ broadcast_event(session_id, event) # notify any streaming subscribers (R3)
99
+ true
100
+ end
101
+
102
+ # Deletes a session from memory.
103
+ # @param session_id [String] The ID of the session to delete.
104
+ # @return [Boolean] True if a session was deleted, false otherwise.
105
+ def delete_session(session_id:)
106
+ deleted_session = @sessions.delete(session_id)
107
+ if deleted_session
108
+ Legate.logger.info("Deleted session: #{session_id}")
109
+ true
110
+ else
111
+ Legate.logger.warn("Attempted to delete non-existent session: #{session_id}")
112
+ false
113
+ end
114
+ end
115
+
116
+ # Lists sessions (in this implementation, just returns all session objects).
117
+ # Filtering could be added later if needed.
118
+ # @param app_name [String, nil] Optional filter by app name.
119
+ # @param user_id [String, nil] Optional filter by user ID.
120
+ # @return [Array<Legate::Session>] An array of session objects matching filters.
121
+ def list_sessions(app_name: nil, user_id: nil)
122
+ filtered = @sessions.values # Get all session objects
123
+ filtered.select! { |s| s.app_name == app_name } if app_name
124
+ filtered.select! { |s| s.user_id == user_id } if user_id
125
+ Legate.logger.debug("Listing #{filtered.count} sessions.")
126
+ filtered
127
+ end
128
+
129
+ def save_scoped_state(scope, key, value)
130
+ state_key = "#{scope}:#{key}"
131
+ @scoped_states[state_key] = value
132
+ end
133
+
134
+ def load_scoped_state(scope, key)
135
+ state_key = "#{scope}:#{key}"
136
+ @scoped_states[state_key]
137
+ end
138
+
139
+ def clear_scoped_state(scope, key)
140
+ if key == '*'
141
+ # Clear all states for the given scope
142
+ @scoped_states.keys.each do |state_key|
143
+ @scoped_states.delete(state_key) if state_key.start_with?("#{scope}:")
144
+ end
145
+ else
146
+ state_key = "#{scope}:#{key}"
147
+ @scoped_states.delete(state_key)
148
+ end
149
+ end
150
+
151
+ # Sets a key-value pair in the state associated with the session.
152
+ # Delegates to the Legate::Session instance's set_state method.
153
+ # @param session_id [String] The ID of the session.
154
+ # @param key [Symbol] The key for the state entry.
155
+ # @param value [Object] The value to store.
156
+ # @return [void]
157
+ def set_state(session_id:, key:, value:)
158
+ session = get_session(session_id: session_id)
159
+ if session
160
+ begin
161
+ session.set_state(key, value) # Legate::Session#set_state handles its own logging
162
+ rescue Legate::SerializationError => e # Catch potential serialization errors from session.set_state
163
+ Legate.logger.error("InMemorySessionService: Error setting state for session '#{session_id}', key '#{key}': #{e.message}")
164
+ # Depending on desired behavior, could re-raise or just log
165
+ end
166
+ else
167
+ Legate.logger.warn("InMemorySessionService: Session not found '#{session_id}' when trying to set state for key '#{key}'.")
168
+ end
169
+ nil # Return void consistent with base
170
+ end
171
+
172
+ # Retrieves a value from the state associated with the session.
173
+ # Delegates to the Legate::Session instance's get_state method.
174
+ # @param session_id [String] The ID of the session.
175
+ # @param key [Symbol] The key for the state entry.
176
+ # @return [Object, nil] The value if found, or nil.
177
+ def get_state(session_id:, key:)
178
+ session = get_session(session_id: session_id)
179
+ if session
180
+ session.get_state(key) # Legate::Session#get_state handles its own logic
181
+ else
182
+ Legate.logger.warn("InMemorySessionService: Session not found '#{session_id}' when trying to get state for key '#{key}'.")
183
+ nil
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legate
4
+ class Tool
5
+ # Module to provide a more concise DSL for defining tool metadata.
6
+ module MetadataDsl
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+
10
+ # Define class instance variable accessors on the base class singleton
11
+ # These are primarily for the *new* DSL
12
+ class << base
13
+ # Replaced attr_accessor with manual methods to handle cache invalidation
14
+ # attr_accessor :explicit_tool_name, :description, :parameters_definition
15
+
16
+ attr_reader :explicit_tool_name, :description, :parameters_definition
17
+
18
+ def explicit_tool_name=(value)
19
+ @explicit_tool_name = value
20
+ @_tool_metadata_cache = nil # Invalidate cache
21
+ end
22
+
23
+ def description=(value)
24
+ @description = value
25
+ @_tool_metadata_cache = nil # Invalidate cache
26
+ end
27
+
28
+ def parameters_definition=(value)
29
+ @parameters_definition = value
30
+ @_tool_metadata_cache = nil # Invalidate cache
31
+ end
32
+
33
+ # Initialize with default values to ensure methods don't fail on nil
34
+ # Note: These are instance variables of the singleton class (class instance variables)
35
+ def initialize_dsl_storage
36
+ @explicit_tool_name ||= nil
37
+ @description ||= nil # DSL description storage
38
+ @parameters_definition ||= {} # DSL parameters storage
39
+ end
40
+ end
41
+ end
42
+
43
+ module ClassMethods
44
+ # DSL method for setting description
45
+ def tool_description(text)
46
+ initialize_dsl_storage # Ensure vars exist
47
+ self.description = text.to_s
48
+ end
49
+
50
+ # DSL method for defining a parameter
51
+ def parameter(name, options = {})
52
+ initialize_dsl_storage # Ensure hash exists
53
+ raise ArgumentError, 'Parameter name must be a Symbol' unless name.is_a?(Symbol)
54
+
55
+ @parameters_definition[name] = options
56
+ @_tool_metadata_cache = nil # Invalidate cache on modification
57
+ end
58
+
59
+ # Get the inferred name (logic unchanged)
60
+ def inferred_name
61
+ class_name_str = Module.instance_method(:name).bind(self).call
62
+ return nil unless class_name_str && !class_name_str.empty? && class_name_str.is_a?(String)
63
+ return nil if class_name_str.start_with?('#<Class:')
64
+
65
+ inferred = class_name_str.split('::').last
66
+ return nil if inferred.nil? || inferred.empty?
67
+
68
+ inferred = inferred.dup
69
+ inferred.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
70
+ inferred.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
71
+ inferred.tr!('-', '_')
72
+ inferred.downcase!
73
+ inferred.to_sym
74
+ end
75
+
76
+ # Get the final tool name with priority:
77
+ # 1. DSL's explicit_tool_name
78
+ # 2. define_metadata's @tool_name
79
+ # 3. Inferred name
80
+ def effective_tool_name
81
+ initialize_dsl_storage # Ensure @explicit_tool_name exists
82
+ explicit_dsl = explicit_tool_name
83
+ return explicit_dsl if explicit_dsl && explicit_dsl != :''
84
+
85
+ # Check define_metadata's variable if explicit DSL name wasn't set
86
+ # Use instance_variable_get as @tool_name is not directly accessible via reader here
87
+ if instance_variable_defined?(:@tool_name)
88
+ explicit_old = instance_variable_get(:@tool_name)
89
+ return explicit_old if explicit_old && explicit_old != :''
90
+ end
91
+
92
+ # Fallback to inferred name
93
+ inferred_name
94
+ end
95
+
96
+ # Retrieve consolidated metadata, preferring DSL values but falling back to define_metadata values.
97
+ # Cached for performance.
98
+ def tool_metadata
99
+ @_tool_metadata_cache ||= begin
100
+ initialize_dsl_storage # Ensure DSL variables exist
101
+
102
+ # Get description: Prefer DSL, fallback to define_metadata's @description
103
+ dsl_desc = description
104
+ old_desc = instance_variable_get(:@description) if instance_variable_defined?(:@description) && dsl_desc.nil?
105
+ final_desc = dsl_desc || old_desc
106
+
107
+ # Get parameters: Prefer DSL, fallback to define_metadata's @parameters_definition
108
+ dsl_params = parameters_definition
109
+ old_params = instance_variable_get(:@parameters_definition) if instance_variable_defined?(:@parameters_definition) && (dsl_params.nil? || dsl_params.empty?)
110
+ final_params = dsl_params && !dsl_params.empty? ? dsl_params : (old_params || {})
111
+
112
+ {
113
+ name: effective_tool_name,
114
+ description: final_desc,
115
+ parameters: final_params
116
+ }
117
+ end
118
+ end
119
+ end # End ClassMethods
120
+ end
121
+ end
122
+ end