enya-agent 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 (389) hide show
  1. package/.env.example +20 -0
  2. package/.github/workflows/ci.yml +70 -0
  3. package/.github/workflows/publish.yml +250 -0
  4. package/.gitmodules +3 -0
  5. package/Cargo.lock +3584 -0
  6. package/Cargo.toml +97 -0
  7. package/crates/enact/Cargo.toml +27 -0
  8. package/crates/enact/src/lib.rs +60 -0
  9. package/crates/enact-a2a/Cargo.toml +25 -0
  10. package/crates/enact-a2a/src/lib.rs +411 -0
  11. package/crates/enact-channels/Cargo.toml +64 -0
  12. package/crates/enact-channels/examples/README.md +80 -0
  13. package/crates/enact-channels/examples/channel_bot.rs +169 -0
  14. package/crates/enact-channels/examples/telegram-echo.rs +34 -0
  15. package/crates/enact-channels/examples/whatsapp-echo.rs +142 -0
  16. package/crates/enact-channels/src/config.rs +213 -0
  17. package/crates/enact-channels/src/lib.rs +25 -0
  18. package/crates/enact-channels/src/runtime.rs +237 -0
  19. package/crates/enact-channels/src/security/mod.rs +5 -0
  20. package/crates/enact-channels/src/security/pairing.rs +205 -0
  21. package/crates/enact-channels/src/teams.rs +601 -0
  22. package/crates/enact-channels/src/telegram.rs +2833 -0
  23. package/crates/enact-channels/src/traits.rs +200 -0
  24. package/crates/enact-channels/src/webhook.rs +262 -0
  25. package/crates/enact-channels/src/whatsapp.rs +310 -0
  26. package/crates/enact-cli/Cargo.toml +40 -0
  27. package/crates/enact-cli/src/commands/doctor.rs +62 -0
  28. package/crates/enact-cli/src/commands/mod.rs +3 -0
  29. package/crates/enact-cli/src/commands/run.rs +69 -0
  30. package/crates/enact-cli/src/commands/serve.rs +81 -0
  31. package/crates/enact-cli/src/config.rs +2 -0
  32. package/crates/enact-cli/src/main.rs +79 -0
  33. package/crates/enact-config/Cargo.toml +36 -0
  34. package/crates/enact-config/ENV_VAR_MAPPING.md +135 -0
  35. package/crates/enact-config/QUICK_REFERENCE.md +92 -0
  36. package/crates/enact-config/README.md +107 -0
  37. package/crates/enact-config/TESTING.md +161 -0
  38. package/crates/enact-config/examples/test-env-vars.rs +100 -0
  39. package/crates/enact-config/src/config.rs +399 -0
  40. package/crates/enact-config/src/encrypted_store.rs +211 -0
  41. package/crates/enact-config/src/lib.rs +298 -0
  42. package/crates/enact-config/src/secrets.rs +149 -0
  43. package/crates/enact-config/src/sync.rs +260 -0
  44. package/crates/enact-config/test-env-vars.sh +34 -0
  45. package/crates/enact-config/tests/README.md +99 -0
  46. package/crates/enact-config/tests/config_integration_test.rs +202 -0
  47. package/crates/enact-config/tests/security_test.rs +140 -0
  48. package/crates/enact-context/Cargo.toml +41 -0
  49. package/crates/enact-context/src/budget.rs +314 -0
  50. package/crates/enact-context/src/calibrator.rs +535 -0
  51. package/crates/enact-context/src/compactor.rs +392 -0
  52. package/crates/enact-context/src/condenser.rs +826 -0
  53. package/crates/enact-context/src/lib.rs +94 -0
  54. package/crates/enact-context/src/segment.rs +238 -0
  55. package/crates/enact-context/src/step_context.rs +645 -0
  56. package/crates/enact-context/src/token_counter.rs +148 -0
  57. package/crates/enact-context/src/window.rs +372 -0
  58. package/crates/enact-core/Cargo.toml +42 -0
  59. package/crates/enact-core/README.md +98 -0
  60. package/crates/enact-core/src/background/executor.rs +524 -0
  61. package/crates/enact-core/src/background/mod.rs +48 -0
  62. package/crates/enact-core/src/background/target_binding.rs +390 -0
  63. package/crates/enact-core/src/background/trigger.rs +511 -0
  64. package/crates/enact-core/src/callable/callable.rs +152 -0
  65. package/crates/enact-core/src/callable/composite.rs +817 -0
  66. package/crates/enact-core/src/callable/graph.rs +104 -0
  67. package/crates/enact-core/src/callable/llm.rs +211 -0
  68. package/crates/enact-core/src/callable/mod.rs +64 -0
  69. package/crates/enact-core/src/callable/registry.rs +206 -0
  70. package/crates/enact-core/src/context/execution_context.rs +757 -0
  71. package/crates/enact-core/src/context/invocation.rs +99 -0
  72. package/crates/enact-core/src/context/mod.rs +50 -0
  73. package/crates/enact-core/src/context/tenant.rs +175 -0
  74. package/crates/enact-core/src/context/trace.rs +127 -0
  75. package/crates/enact-core/src/flow/conditional.rs +293 -0
  76. package/crates/enact-core/src/flow/mod.rs +43 -0
  77. package/crates/enact-core/src/flow/parallel.rs +437 -0
  78. package/crates/enact-core/src/flow/repeat.rs +534 -0
  79. package/crates/enact-core/src/flow/sequential.rs +248 -0
  80. package/crates/enact-core/src/graph/checkpoint.rs +79 -0
  81. package/crates/enact-core/src/graph/checkpoint_store.rs +76 -0
  82. package/crates/enact-core/src/graph/compiled.rs +189 -0
  83. package/crates/enact-core/src/graph/edge.rs +59 -0
  84. package/crates/enact-core/src/graph/graph_schema.rs +218 -0
  85. package/crates/enact-core/src/graph/loader.rs +155 -0
  86. package/crates/enact-core/src/graph/mod.rs +18 -0
  87. package/crates/enact-core/src/graph/node/function.rs +49 -0
  88. package/crates/enact-core/src/graph/node/mod.rs +48 -0
  89. package/crates/enact-core/src/graph/schema.rs +62 -0
  90. package/crates/enact-core/src/inbox/message.rs +405 -0
  91. package/crates/enact-core/src/inbox/mod.rs +31 -0
  92. package/crates/enact-core/src/inbox/store.rs +355 -0
  93. package/crates/enact-core/src/kernel/artifact/filesystem.rs +546 -0
  94. package/crates/enact-core/src/kernel/artifact/metadata.rs +283 -0
  95. package/crates/enact-core/src/kernel/artifact/mod.rs +27 -0
  96. package/crates/enact-core/src/kernel/artifact/store.rs +427 -0
  97. package/crates/enact-core/src/kernel/enforcement.rs +1315 -0
  98. package/crates/enact-core/src/kernel/error.rs +1200 -0
  99. package/crates/enact-core/src/kernel/event.rs +1394 -0
  100. package/crates/enact-core/src/kernel/execution_model.rs +831 -0
  101. package/crates/enact-core/src/kernel/execution_state.rs +189 -0
  102. package/crates/enact-core/src/kernel/execution_strategy.rs +117 -0
  103. package/crates/enact-core/src/kernel/ids.rs +2086 -0
  104. package/crates/enact-core/src/kernel/interrupt.rs +125 -0
  105. package/crates/enact-core/src/kernel/kernel.rs +1283 -0
  106. package/crates/enact-core/src/kernel/mod.rs +205 -0
  107. package/crates/enact-core/src/kernel/persistence/event_store.rs +270 -0
  108. package/crates/enact-core/src/kernel/persistence/message_store.rs +908 -0
  109. package/crates/enact-core/src/kernel/persistence/mod.rs +102 -0
  110. package/crates/enact-core/src/kernel/persistence/state_store.rs +228 -0
  111. package/crates/enact-core/src/kernel/persistence/vector_store.rs +299 -0
  112. package/crates/enact-core/src/kernel/reducer.rs +808 -0
  113. package/crates/enact-core/src/kernel/replay.rs +153 -0
  114. package/crates/enact-core/src/lib.rs +413 -0
  115. package/crates/enact-core/src/memory/episodic.rs +0 -0
  116. package/crates/enact-core/src/memory/mod.rs +6 -0
  117. package/crates/enact-core/src/memory/semantic.rs +0 -0
  118. package/crates/enact-core/src/memory/trait.rs +0 -0
  119. package/crates/enact-core/src/memory/vector_db.rs +0 -0
  120. package/crates/enact-core/src/memory/working.rs +0 -0
  121. package/crates/enact-core/src/policy/execution_policy.rs +292 -0
  122. package/crates/enact-core/src/policy/filters.rs +458 -0
  123. package/crates/enact-core/src/policy/input_processor.rs +407 -0
  124. package/crates/enact-core/src/policy/long_running.rs +134 -0
  125. package/crates/enact-core/src/policy/mod.rs +193 -0
  126. package/crates/enact-core/src/policy/pii_input.rs +274 -0
  127. package/crates/enact-core/src/policy/tenant_policy.rs +453 -0
  128. package/crates/enact-core/src/policy/tool_policy.rs +407 -0
  129. package/crates/enact-core/src/providers/mod.rs +63 -0
  130. package/crates/enact-core/src/providers/trait.rs +292 -0
  131. package/crates/enact-core/src/runner/callbacks.rs +6 -0
  132. package/crates/enact-core/src/runner/execution_runner.rs +476 -0
  133. package/crates/enact-core/src/runner/loop.rs +117 -0
  134. package/crates/enact-core/src/runner/mod.rs +58 -0
  135. package/crates/enact-core/src/runner/protected_runner.rs +280 -0
  136. package/crates/enact-core/src/signal/inmemory.rs +231 -0
  137. package/crates/enact-core/src/signal/mod.rs +108 -0
  138. package/crates/enact-core/src/streaming/event_logger.rs +195 -0
  139. package/crates/enact-core/src/streaming/event_stream.rs +1423 -0
  140. package/crates/enact-core/src/streaming/mod.rs +108 -0
  141. package/crates/enact-core/src/streaming/pause_cancel.rs +0 -0
  142. package/crates/enact-core/src/streaming/protected_emitter.rs +173 -0
  143. package/crates/enact-core/src/streaming/protection/context.rs +136 -0
  144. package/crates/enact-core/src/streaming/protection/encryption.rs +289 -0
  145. package/crates/enact-core/src/streaming/protection/mod.rs +43 -0
  146. package/crates/enact-core/src/streaming/protection/pii_protection.rs +243 -0
  147. package/crates/enact-core/src/streaming/protection/processor.rs +166 -0
  148. package/crates/enact-core/src/streaming/sse.rs +0 -0
  149. package/crates/enact-core/src/telemetry/exporter.rs +0 -0
  150. package/crates/enact-core/src/telemetry/init.rs +0 -0
  151. package/crates/enact-core/src/telemetry/mod.rs +49 -0
  152. package/crates/enact-core/src/telemetry/spans.rs +245 -0
  153. package/crates/enact-core/src/tool/agent_tool.rs +177 -0
  154. package/crates/enact-core/src/tool/browser/mod.rs +0 -0
  155. package/crates/enact-core/src/tool/browser/webdriver.rs +0 -0
  156. package/crates/enact-core/src/tool/cost.rs +247 -0
  157. package/crates/enact-core/src/tool/discovery.rs +0 -0
  158. package/crates/enact-core/src/tool/dispatcher.rs +347 -0
  159. package/crates/enact-core/src/tool/filesystem.rs +231 -0
  160. package/crates/enact-core/src/tool/function.rs +99 -0
  161. package/crates/enact-core/src/tool/git.rs +162 -0
  162. package/crates/enact-core/src/tool/http.rs +214 -0
  163. package/crates/enact-core/src/tool/mcp/client.rs +0 -0
  164. package/crates/enact-core/src/tool/mcp/mod.rs +0 -0
  165. package/crates/enact-core/src/tool/mod.rs +51 -0
  166. package/crates/enact-core/src/tool/reasoning/debugging.rs +0 -0
  167. package/crates/enact-core/src/tool/reasoning/mcts.rs +0 -0
  168. package/crates/enact-core/src/tool/reasoning/mod.rs +0 -0
  169. package/crates/enact-core/src/tool/reasoning/sequential.rs +0 -0
  170. package/crates/enact-core/src/tool/sandbox/dagger.rs +0 -0
  171. package/crates/enact-core/src/tool/sandbox/mod.rs +0 -0
  172. package/crates/enact-core/src/tool/shell.rs +147 -0
  173. package/crates/enact-core/src/tool/trait.rs +33 -0
  174. package/crates/enact-core/src/tool/web_search.rs +277 -0
  175. package/crates/enact-core/src/util/config.rs +0 -0
  176. package/crates/enact-core/src/util/errors.rs +0 -0
  177. package/crates/enact-core/src/util/mod.rs +6 -0
  178. package/crates/enact-core/tests/airgapped_e2e_test.rs +291 -0
  179. package/crates/enact-core/tests/e2e_agentic_loop.rs +119 -0
  180. package/crates/enact-core/tests/e2e_test.rs +259 -0
  181. package/crates/enact-core/tests/graph_test.rs +130 -0
  182. package/crates/enact-core/tests/stream_event_id_validation.rs +435 -0
  183. package/crates/enact-cron/Cargo.toml +28 -0
  184. package/crates/enact-cron/src/lib.rs +44 -0
  185. package/crates/enact-cron/src/schedule.rs +156 -0
  186. package/crates/enact-cron/src/store.rs +589 -0
  187. package/crates/enact-cron/src/types.rs +148 -0
  188. package/crates/enact-gateway/Cargo.toml +31 -0
  189. package/crates/enact-gateway/README.md +30 -0
  190. package/crates/enact-gateway/examples/whatsapp-gateway-runner-mock.rs +59 -0
  191. package/crates/enact-gateway/examples/whatsapp-gateway.rs +42 -0
  192. package/crates/enact-gateway/src/lib.rs +582 -0
  193. package/crates/enact-mcp/Cargo.toml +24 -0
  194. package/crates/enact-mcp/src/lib.rs +178 -0
  195. package/crates/enact-memory/Cargo.toml +25 -0
  196. package/crates/enact-memory/src/backend.rs +20 -0
  197. package/crates/enact-memory/src/chunker.rs +230 -0
  198. package/crates/enact-memory/src/embeddings.rs +221 -0
  199. package/crates/enact-memory/src/lib.rs +67 -0
  200. package/crates/enact-memory/src/markdown.rs +127 -0
  201. package/crates/enact-memory/src/none.rs +61 -0
  202. package/crates/enact-memory/src/sqlite.rs +276 -0
  203. package/crates/enact-memory/src/traits.rs +65 -0
  204. package/crates/enact-memory/src/vector.rs +198 -0
  205. package/crates/enact-oauth/Cargo.toml +27 -0
  206. package/crates/enact-oauth/src/lib.rs +584 -0
  207. package/crates/enact-observability/Cargo.toml +22 -0
  208. package/crates/enact-observability/src/lib.rs +197 -0
  209. package/crates/enact-providers/Cargo.toml +33 -0
  210. package/crates/enact-providers/examples/hello-agent.rs +33 -0
  211. package/crates/enact-providers/src/anthropic.rs +182 -0
  212. package/crates/enact-providers/src/azure.rs +96 -0
  213. package/crates/enact-providers/src/bridge.rs +221 -0
  214. package/crates/enact-providers/src/gemini.rs +227 -0
  215. package/crates/enact-providers/src/http.rs +78 -0
  216. package/crates/enact-providers/src/lib.rs +53 -0
  217. package/crates/enact-providers/src/openai_compatible.rs +167 -0
  218. package/crates/enact-providers/src/openrouter.rs +33 -0
  219. package/crates/enact-runner/Cargo.toml +24 -0
  220. package/crates/enact-runner/README.md +76 -0
  221. package/crates/enact-runner/src/compaction.rs +225 -0
  222. package/crates/enact-runner/src/config.rs +118 -0
  223. package/crates/enact-runner/src/lib.rs +63 -0
  224. package/crates/enact-runner/src/loop_driver.rs +414 -0
  225. package/crates/enact-runner/src/parser.rs +421 -0
  226. package/crates/enact-runner/src/retry.rs +262 -0
  227. package/crates/enact-runner/tests/integration.rs +278 -0
  228. package/crates/enact-security/Cargo.toml +22 -0
  229. package/crates/enact-security/src/audit.rs +375 -0
  230. package/crates/enact-security/src/lib.rs +37 -0
  231. package/crates/enact-security/src/policy.rs +406 -0
  232. package/crates/enact-skills/Cargo.toml +25 -0
  233. package/crates/enact-skills/src/lib.rs +506 -0
  234. package/crates/enact-tools/Cargo.toml +22 -0
  235. package/crates/enact-tools/src/file_read.rs +166 -0
  236. package/crates/enact-tools/src/file_write.rs +216 -0
  237. package/crates/enact-tools/src/git_operations.rs +513 -0
  238. package/crates/enact-tools/src/http_request.rs +417 -0
  239. package/crates/enact-tools/src/lib.rs +104 -0
  240. package/crates/enact-tools/src/security.rs +227 -0
  241. package/crates/enact-tools/src/shell.rs +191 -0
  242. package/crates/enact-tools/src/traits.rs +159 -0
  243. package/docs/Makefile +74 -0
  244. package/docs/config.toml +62 -0
  245. package/docs/content/_index.md +174 -0
  246. package/docs/content/a2a/_index.md +431 -0
  247. package/docs/content/api/_index.md +323 -0
  248. package/docs/content/channels/_index.md +160 -0
  249. package/docs/content/channels/teams.md +205 -0
  250. package/docs/content/channels/telegram.md +182 -0
  251. package/docs/content/channels/webhook.md +423 -0
  252. package/docs/content/channels/whatsapp.md +240 -0
  253. package/docs/content/cli/_index.md +261 -0
  254. package/docs/content/concepts/_index.md +273 -0
  255. package/docs/content/configuration/_index.md +241 -0
  256. package/docs/content/cron/_index.md +248 -0
  257. package/docs/content/developers/_index.md +278 -0
  258. package/docs/content/getting-started/_index.md +180 -0
  259. package/docs/content/installation/_index.md +186 -0
  260. package/docs/content/installation/uninstall.md +101 -0
  261. package/docs/content/installation/updating.md +120 -0
  262. package/docs/content/mcp/_index.md +215 -0
  263. package/docs/content/memory/_index.md +163 -0
  264. package/docs/content/oauth/_index.md +515 -0
  265. package/docs/content/providers/_index.md +206 -0
  266. package/docs/content/roadmap/_index.md +199 -0
  267. package/docs/content/security/_index.md +219 -0
  268. package/docs/content/skills/_index.md +228 -0
  269. package/docs/content/tools/_index.md +485 -0
  270. package/docs/content/troubleshooting/_index.md +259 -0
  271. package/docs/content/yaml-schema/_index.md +294 -0
  272. package/docs/static/giallo-dark.css +91 -0
  273. package/docs/static/giallo-light.css +91 -0
  274. package/docs/themes/tanuki/.github/workflows/deploy.yml +44 -0
  275. package/docs/themes/tanuki/LICENSE +21 -0
  276. package/docs/themes/tanuki/README.md +166 -0
  277. package/docs/themes/tanuki/examples/blog/config.toml +58 -0
  278. package/docs/themes/tanuki/examples/blog/content/_index.md +4 -0
  279. package/docs/themes/tanuki/examples/blog/content/about.md +33 -0
  280. package/docs/themes/tanuki/examples/blog/content/blog/_index.md +7 -0
  281. package/docs/themes/tanuki/examples/blog/content/blog/api-design-best-practices.md +245 -0
  282. package/docs/themes/tanuki/examples/blog/content/blog/building-accessible-websites.md +147 -0
  283. package/docs/themes/tanuki/examples/blog/content/blog/css-grid-vs-flexbox.md +165 -0
  284. package/docs/themes/tanuki/examples/blog/content/blog/customizing-catppuccin-colors.md +137 -0
  285. package/docs/themes/tanuki/examples/blog/content/blog/dark-mode-best-practices.md +82 -0
  286. package/docs/themes/tanuki/examples/blog/content/blog/docker-essentials.md +301 -0
  287. package/docs/themes/tanuki/examples/blog/content/blog/getting-started-with-zola.md +129 -0
  288. package/docs/themes/tanuki/examples/blog/content/blog/git-workflow-for-content.md +112 -0
  289. package/docs/themes/tanuki/examples/blog/content/blog/introduction-to-webassembly.md +183 -0
  290. package/docs/themes/tanuki/examples/blog/content/blog/modern-javascript-features.md +234 -0
  291. package/docs/themes/tanuki/examples/blog/content/blog/testing-strategies.md +311 -0
  292. package/docs/themes/tanuki/examples/blog/content/blog/typography-for-developers.md +104 -0
  293. package/docs/themes/tanuki/examples/blog/content/blog/welcome-to-tanuki.md +67 -0
  294. package/docs/themes/tanuki/examples/blog/content/blog/why-static-sites.md +85 -0
  295. package/docs/themes/tanuki/examples/blog/content/projects.md +64 -0
  296. package/docs/themes/tanuki/examples/book/config.toml +17 -0
  297. package/docs/themes/tanuki/examples/book/content/_index.md +12 -0
  298. package/docs/themes/tanuki/examples/book/content/chapter-1.md +90 -0
  299. package/docs/themes/tanuki/examples/book/content/chapter-2.md +143 -0
  300. package/docs/themes/tanuki/examples/book/content/chapter-3.md +217 -0
  301. package/docs/themes/tanuki/examples/book/content/chapter-4.md +224 -0
  302. package/docs/themes/tanuki/examples/book/content/chapter-5.md +297 -0
  303. package/docs/themes/tanuki/examples/book/content/print.md +6 -0
  304. package/docs/themes/tanuki/examples/docs/config.toml +28 -0
  305. package/docs/themes/tanuki/examples/docs/content/_index.md +20 -0
  306. package/docs/themes/tanuki/examples/docs/content/components.md +156 -0
  307. package/docs/themes/tanuki/examples/docs/content/configuration.md +94 -0
  308. package/docs/themes/tanuki/examples/docs/content/customization.md +202 -0
  309. package/docs/themes/tanuki/examples/docs/content/deployment.md +204 -0
  310. package/docs/themes/tanuki/examples/docs/content/installation.md +59 -0
  311. package/docs/themes/tanuki/examples/docs/content/print.md +6 -0
  312. package/docs/themes/tanuki/examples/docs/static/img/tanuki-icon.avif +0 -0
  313. package/docs/themes/tanuki/examples/index.html +2104 -0
  314. package/docs/themes/tanuki/mise.toml +108 -0
  315. package/docs/themes/tanuki/sass/base/_catppuccin.scss +164 -0
  316. package/docs/themes/tanuki/sass/base/_fonts.scss +64 -0
  317. package/docs/themes/tanuki/sass/base/_reset.scss +152 -0
  318. package/docs/themes/tanuki/sass/base/_typography.scss +523 -0
  319. package/docs/themes/tanuki/sass/components/_buttons.scss +209 -0
  320. package/docs/themes/tanuki/sass/components/_code.scss +457 -0
  321. package/docs/themes/tanuki/sass/components/_landing.scss +633 -0
  322. package/docs/themes/tanuki/sass/components/_layout.scss +294 -0
  323. package/docs/themes/tanuki/sass/components/_navigation.scss +1200 -0
  324. package/docs/themes/tanuki/sass/components/_print.scss +237 -0
  325. package/docs/themes/tanuki/sass/components/_search.scss +224 -0
  326. package/docs/themes/tanuki/sass/components/_sidebar.scss +473 -0
  327. package/docs/themes/tanuki/sass/components/_theme-toggle.scss +186 -0
  328. package/docs/themes/tanuki/sass/modes/_blog.scss +366 -0
  329. package/docs/themes/tanuki/sass/modes/_product.scss +875 -0
  330. package/docs/themes/tanuki/sass/modes/_raskell.scss +1696 -0
  331. package/docs/themes/tanuki/sass/patterns/_buttons.scss +183 -0
  332. package/docs/themes/tanuki/sass/patterns/_cards.scss +144 -0
  333. package/docs/themes/tanuki/sass/patterns/_index.scss +9 -0
  334. package/docs/themes/tanuki/sass/patterns/_lists.scss +259 -0
  335. package/docs/themes/tanuki/sass/patterns/_sections.scss +243 -0
  336. package/docs/themes/tanuki/sass/style.scss +47 -0
  337. package/docs/themes/tanuki/sass/tokens/_colors.scss +139 -0
  338. package/docs/themes/tanuki/sass/tokens/_spacing.scss +100 -0
  339. package/docs/themes/tanuki/sass/tokens/_typography.scss +186 -0
  340. package/docs/themes/tanuki/screenshot.png +0 -0
  341. package/docs/themes/tanuki/sentinel.kdl +59 -0
  342. package/docs/themes/tanuki/static/elasticlunr.min.js +10 -0
  343. package/docs/themes/tanuki/static/fonts/GEIST-LICENSE.txt +92 -0
  344. package/docs/themes/tanuki/static/fonts/Geist-Variable.woff2 +0 -0
  345. package/docs/themes/tanuki/static/fonts/GeistMono-Variable.woff2 +0 -0
  346. package/docs/themes/tanuki/static/img/tanuki-icon.avif +0 -0
  347. package/docs/themes/tanuki/static/img/tanuki-icon.png +0 -0
  348. package/docs/themes/tanuki/static/js/anchors.js +18 -0
  349. package/docs/themes/tanuki/static/js/app.js +274 -0
  350. package/docs/themes/tanuki/static/js/code.js +394 -0
  351. package/docs/themes/tanuki/static/js/navigation.js +778 -0
  352. package/docs/themes/tanuki/static/js/scroll-to-top.js +33 -0
  353. package/docs/themes/tanuki/static/js/search-raskell.js +240 -0
  354. package/docs/themes/tanuki/static/js/search.js +215 -0
  355. package/docs/themes/tanuki/static/js/theme.js +169 -0
  356. package/docs/themes/tanuki/static/syntax-dark.css +151 -0
  357. package/docs/themes/tanuki/static/syntax-light.css +151 -0
  358. package/docs/themes/tanuki/static/wasm/sentinel_playground_wasm.js +486 -0
  359. package/docs/themes/tanuki/static/wasm/sentinel_playground_wasm_bg.wasm +0 -0
  360. package/docs/themes/tanuki/templates/404.html +52 -0
  361. package/docs/themes/tanuki/templates/base.html +428 -0
  362. package/docs/themes/tanuki/templates/blog.html +66 -0
  363. package/docs/themes/tanuki/templates/home.html +108 -0
  364. package/docs/themes/tanuki/templates/index.html +178 -0
  365. package/docs/themes/tanuki/templates/landing.html +168 -0
  366. package/docs/themes/tanuki/templates/macros/nav.html +128 -0
  367. package/docs/themes/tanuki/templates/macros/posts.html +101 -0
  368. package/docs/themes/tanuki/templates/macros/ui.html +159 -0
  369. package/docs/themes/tanuki/templates/page.html +135 -0
  370. package/docs/themes/tanuki/templates/partials/footer.html +38 -0
  371. package/docs/themes/tanuki/templates/partials/header.html +366 -0
  372. package/docs/themes/tanuki/templates/partials/nav-buttons.html +55 -0
  373. package/docs/themes/tanuki/templates/partials/nav-overlay.html +81 -0
  374. package/docs/themes/tanuki/templates/partials/page-toc-panel.html +43 -0
  375. package/docs/themes/tanuki/templates/partials/search.html +52 -0
  376. package/docs/themes/tanuki/templates/partials/sidebar.html +107 -0
  377. package/docs/themes/tanuki/templates/partials/theme-toggle.html +35 -0
  378. package/docs/themes/tanuki/templates/partials/toc-overlay.html +146 -0
  379. package/docs/themes/tanuki/templates/partials/version-picker.html +38 -0
  380. package/docs/themes/tanuki/templates/print.html +244 -0
  381. package/docs/themes/tanuki/templates/section.html +186 -0
  382. package/docs/themes/tanuki/templates/taxonomy_list.html +18 -0
  383. package/docs/themes/tanuki/templates/taxonomy_single.html +31 -0
  384. package/docs/themes/tanuki/theme.toml +58 -0
  385. package/examples/hello-agent.rs +55 -0
  386. package/package.json +36 -0
  387. package/proto/config.proto +60 -0
  388. package/proto/events.proto +0 -0
  389. package/proto/runtime.proto +215 -0
@@ -0,0 +1,908 @@
1
+ //! MessageStore - User-facing message persistence
2
+ //!
3
+ //! The MessageStore handles thread/message persistence for UI display.
4
+ //! Unlike EventStore (audit-focused, immutable), MessageStore is:
5
+ //! - **Mutable**: Messages can be soft-deleted/edited
6
+ //! - **User-facing**: AI SDK compatible format
7
+ //! - **Hierarchical**: Thread → Message with parent linkage
8
+ //!
9
+ //! ## Guarantees
10
+ //!
11
+ //! - **Soft Delete**: Messages are never hard-deleted (GDPR compliance)
12
+ //! - **Thread Ownership**: Messages belong to exactly one thread
13
+ //! - **Parent Linkage**: Every message tracks its causal origin
14
+ //!
15
+ //! @see docs/TECHNICAL/20-MESSAGE-PERSISTENCE-STRATEGY.md
16
+
17
+ use async_trait::async_trait;
18
+ use chrono::{DateTime, Utc};
19
+ use serde::{Deserialize, Serialize};
20
+ use std::collections::HashMap;
21
+ use std::sync::RwLock;
22
+
23
+ use crate::kernel::{ExecutionId, MessageId, ParentType, TenantId, ThreadId, UserId};
24
+
25
+ use super::StorageBackend;
26
+
27
+ // =============================================================================
28
+ // Thread - Conversation container
29
+ // =============================================================================
30
+
31
+ /// A conversation thread containing messages
32
+ #[derive(Debug, Clone, Serialize, Deserialize)]
33
+ pub struct Thread {
34
+ /// Unique thread identifier
35
+ pub id: ThreadId,
36
+ /// Tenant for multi-tenancy
37
+ pub tenant_id: TenantId,
38
+ /// User who owns this thread
39
+ pub user_id: UserId,
40
+ /// Optional thread title (auto-generated from first message)
41
+ pub title: Option<String>,
42
+ /// When the thread was created
43
+ pub created_at: DateTime<Utc>,
44
+ /// When the thread was last updated
45
+ pub updated_at: DateTime<Utc>,
46
+ /// Soft delete timestamp (None = active)
47
+ pub deleted_at: Option<DateTime<Utc>>,
48
+ }
49
+
50
+ impl Thread {
51
+ /// Create a new thread
52
+ pub fn new(tenant_id: TenantId, user_id: UserId) -> Self {
53
+ let now = Utc::now();
54
+ Self {
55
+ id: ThreadId::new(),
56
+ tenant_id,
57
+ user_id,
58
+ title: None,
59
+ created_at: now,
60
+ updated_at: now,
61
+ deleted_at: None,
62
+ }
63
+ }
64
+
65
+ /// Create a thread with a specific ID (for testing/restore)
66
+ pub fn with_id(id: ThreadId, tenant_id: TenantId, user_id: UserId) -> Self {
67
+ let now = Utc::now();
68
+ Self {
69
+ id,
70
+ tenant_id,
71
+ user_id,
72
+ title: None,
73
+ created_at: now,
74
+ updated_at: now,
75
+ deleted_at: None,
76
+ }
77
+ }
78
+
79
+ /// Check if thread is soft-deleted
80
+ pub fn is_deleted(&self) -> bool {
81
+ self.deleted_at.is_some()
82
+ }
83
+ }
84
+
85
+ // =============================================================================
86
+ // Message Role
87
+ // =============================================================================
88
+
89
+ /// Message role (AI SDK compatible)
90
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91
+ #[serde(rename_all = "lowercase")]
92
+ pub enum MessageRole {
93
+ /// User message
94
+ User,
95
+ /// Assistant/AI response
96
+ Assistant,
97
+ /// System message (instructions, context)
98
+ System,
99
+ }
100
+
101
+ impl std::fmt::Display for MessageRole {
102
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103
+ match self {
104
+ MessageRole::User => write!(f, "user"),
105
+ MessageRole::Assistant => write!(f, "assistant"),
106
+ MessageRole::System => write!(f, "system"),
107
+ }
108
+ }
109
+ }
110
+
111
+ // =============================================================================
112
+ // Message Parts (AI SDK compatible)
113
+ // =============================================================================
114
+
115
+ /// Message part for rich content (AI SDK compatible)
116
+ #[derive(Debug, Clone, Serialize, Deserialize)]
117
+ #[serde(tag = "type", rename_all = "kebab-case")]
118
+ pub enum MessagePart {
119
+ /// Plain text content
120
+ Text { text: String },
121
+
122
+ /// Reasoning/thinking (collapsed in UI)
123
+ Reasoning { text: String },
124
+
125
+ /// Tool invocation
126
+ ToolCall {
127
+ tool_call_id: String,
128
+ tool_name: String,
129
+ args: serde_json::Value,
130
+ },
131
+
132
+ /// Tool result
133
+ ToolResult {
134
+ tool_call_id: String,
135
+ tool_name: String,
136
+ result: serde_json::Value,
137
+ is_error: bool,
138
+ },
139
+
140
+ /// Source/citation
141
+ Source {
142
+ source_id: String,
143
+ url: Option<String>,
144
+ title: Option<String>,
145
+ },
146
+
147
+ /// File attachment
148
+ File {
149
+ file_id: String,
150
+ filename: String,
151
+ mime_type: String,
152
+ size_bytes: u64,
153
+ },
154
+
155
+ /// Image (inline or referenced)
156
+ Image {
157
+ image_id: String,
158
+ url: Option<String>,
159
+ alt_text: Option<String>,
160
+ },
161
+
162
+ /// Code block
163
+ Code {
164
+ language: Option<String>,
165
+ code: String,
166
+ },
167
+ }
168
+
169
+ // =============================================================================
170
+ // Token Usage
171
+ // =============================================================================
172
+
173
+ /// Token usage for billing and analytics
174
+ #[derive(Debug, Clone, Default, Serialize, Deserialize)]
175
+ pub struct TokenUsage {
176
+ /// Input/prompt tokens
177
+ pub prompt_tokens: u32,
178
+ /// Output/completion tokens
179
+ pub completion_tokens: u32,
180
+ /// Total tokens (may include cached)
181
+ pub total_tokens: u32,
182
+ }
183
+
184
+ impl TokenUsage {
185
+ /// Create new token usage
186
+ pub fn new(prompt: u32, completion: u32) -> Self {
187
+ Self {
188
+ prompt_tokens: prompt,
189
+ completion_tokens: completion,
190
+ total_tokens: prompt + completion,
191
+ }
192
+ }
193
+ }
194
+
195
+ // =============================================================================
196
+ // Execution Stats
197
+ // =============================================================================
198
+
199
+ /// Execution statistics for a message
200
+ #[derive(Debug, Clone, Default, Serialize, Deserialize)]
201
+ pub struct ExecutionStats {
202
+ /// Total LLM invocations
203
+ pub llm_calls: u32,
204
+ /// Total tool invocations
205
+ pub tool_calls: u32,
206
+ /// Nested agent executions
207
+ pub sub_agents: u32,
208
+ /// Total steps executed
209
+ pub steps: u32,
210
+ /// Decisions made (audit)
211
+ pub decisions: u32,
212
+ /// Artifacts produced
213
+ pub artifacts: u32,
214
+ }
215
+
216
+ // =============================================================================
217
+ // Cost Info
218
+ // =============================================================================
219
+
220
+ /// Cost information for billing
221
+ #[derive(Debug, Clone, Serialize, Deserialize)]
222
+ pub struct CostInfo {
223
+ /// Input token cost
224
+ pub input_cost: f64,
225
+ /// Output token cost
226
+ pub output_cost: f64,
227
+ /// Total cost
228
+ pub total_cost: f64,
229
+ /// Currency (default: USD)
230
+ pub currency: String,
231
+ }
232
+
233
+ impl Default for CostInfo {
234
+ fn default() -> Self {
235
+ Self {
236
+ input_cost: 0.0,
237
+ output_cost: 0.0,
238
+ total_cost: 0.0,
239
+ currency: "USD".to_string(),
240
+ }
241
+ }
242
+ }
243
+
244
+ // =============================================================================
245
+ // Finish Reason
246
+ // =============================================================================
247
+
248
+ /// Why the response finished
249
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250
+ #[serde(rename_all = "snake_case")]
251
+ pub enum FinishReason {
252
+ /// Normal completion
253
+ Stop,
254
+ /// Hit max tokens
255
+ Length,
256
+ /// Needs to call tools
257
+ ToolCalls,
258
+ /// Content filter triggered
259
+ ContentFilter,
260
+ /// Error occurred
261
+ Error,
262
+ }
263
+
264
+ // =============================================================================
265
+ // Message Metadata
266
+ // =============================================================================
267
+
268
+ /// Message metadata (AI SDK compatible)
269
+ #[derive(Debug, Clone, Default, Serialize, Deserialize)]
270
+ pub struct MessageMetadata {
271
+ /// When response completed (Unix ms)
272
+ pub completed_at: Option<i64>,
273
+ /// Total duration in ms
274
+ pub duration_ms: Option<u64>,
275
+
276
+ /// Model used (e.g., "gpt-4o", "claude-3-opus")
277
+ pub model: Option<String>,
278
+ /// Provider (e.g., "azure", "anthropic")
279
+ pub provider: Option<String>,
280
+
281
+ /// Token usage (aggregated across all LLM calls)
282
+ pub token_usage: Option<TokenUsage>,
283
+
284
+ /// Execution statistics
285
+ pub stats: Option<ExecutionStats>,
286
+
287
+ /// Why the response finished
288
+ pub finish_reason: Option<FinishReason>,
289
+
290
+ /// Cost information (if available)
291
+ pub cost: Option<CostInfo>,
292
+ }
293
+
294
+ // =============================================================================
295
+ // Message
296
+ // =============================================================================
297
+
298
+ /// A message in a thread (AI SDK compatible)
299
+ #[derive(Debug, Clone, Serialize, Deserialize)]
300
+ pub struct Message {
301
+ /// Unique message identifier
302
+ pub id: MessageId,
303
+ /// Thread this message belongs to
304
+ pub thread_id: ThreadId,
305
+ /// Link to execution (for assistant messages)
306
+ pub execution_id: Option<ExecutionId>,
307
+
308
+ // Hierarchy
309
+ /// Parent message (for reply chains)
310
+ pub parent_id: Option<MessageId>,
311
+ /// What triggered this message
312
+ pub parent_type: ParentType,
313
+
314
+ // Content
315
+ /// Message role
316
+ pub role: MessageRole,
317
+ /// Simple text content (for display)
318
+ pub content: String,
319
+ /// Rich content parts (AI SDK format)
320
+ pub parts: Vec<MessagePart>,
321
+
322
+ // Timestamps
323
+ /// When created
324
+ pub created_at: DateTime<Utc>,
325
+ /// When last updated
326
+ pub updated_at: Option<DateTime<Utc>>,
327
+ /// Soft delete timestamp
328
+ pub deleted_at: Option<DateTime<Utc>>,
329
+
330
+ /// Additional metadata
331
+ pub metadata: MessageMetadata,
332
+ }
333
+
334
+ impl Message {
335
+ /// Create a new user message
336
+ pub fn user(thread_id: ThreadId, content: impl Into<String>) -> Self {
337
+ let content = content.into();
338
+ Self {
339
+ id: MessageId::new(),
340
+ thread_id,
341
+ execution_id: None,
342
+ parent_id: None,
343
+ parent_type: ParentType::UserMessage,
344
+ role: MessageRole::User,
345
+ content: content.clone(),
346
+ parts: vec![MessagePart::Text { text: content }],
347
+ created_at: Utc::now(),
348
+ updated_at: None,
349
+ deleted_at: None,
350
+ metadata: MessageMetadata::default(),
351
+ }
352
+ }
353
+
354
+ /// Create a new assistant message
355
+ pub fn assistant(
356
+ thread_id: ThreadId,
357
+ execution_id: ExecutionId,
358
+ content: impl Into<String>,
359
+ parent_id: Option<MessageId>,
360
+ ) -> Self {
361
+ let content = content.into();
362
+ Self {
363
+ id: MessageId::new(),
364
+ thread_id,
365
+ execution_id: Some(execution_id),
366
+ parent_id,
367
+ parent_type: ParentType::UserMessage,
368
+ role: MessageRole::Assistant,
369
+ content: content.clone(),
370
+ parts: vec![MessagePart::Text { text: content }],
371
+ created_at: Utc::now(),
372
+ updated_at: None,
373
+ deleted_at: None,
374
+ metadata: MessageMetadata::default(),
375
+ }
376
+ }
377
+
378
+ /// Create a new system message
379
+ pub fn system(thread_id: ThreadId, content: impl Into<String>) -> Self {
380
+ let content = content.into();
381
+ Self {
382
+ id: MessageId::new(),
383
+ thread_id,
384
+ execution_id: None,
385
+ parent_id: None,
386
+ parent_type: ParentType::System,
387
+ role: MessageRole::System,
388
+ content: content.clone(),
389
+ parts: vec![MessagePart::Text { text: content }],
390
+ created_at: Utc::now(),
391
+ updated_at: None,
392
+ deleted_at: None,
393
+ metadata: MessageMetadata::default(),
394
+ }
395
+ }
396
+
397
+ /// Check if message is soft-deleted
398
+ pub fn is_deleted(&self) -> bool {
399
+ self.deleted_at.is_some()
400
+ }
401
+
402
+ /// Set the parent message
403
+ pub fn with_parent(mut self, parent_id: MessageId, parent_type: ParentType) -> Self {
404
+ self.parent_id = Some(parent_id);
405
+ self.parent_type = parent_type;
406
+ self
407
+ }
408
+
409
+ /// Add rich content parts
410
+ pub fn with_parts(mut self, parts: Vec<MessagePart>) -> Self {
411
+ self.parts = parts;
412
+ self
413
+ }
414
+
415
+ /// Set metadata
416
+ pub fn with_metadata(mut self, metadata: MessageMetadata) -> Self {
417
+ self.metadata = metadata;
418
+ self
419
+ }
420
+ }
421
+
422
+ // =============================================================================
423
+ // MessageStore Trait
424
+ // =============================================================================
425
+
426
+ /// MessageStore trait - thread/message persistence
427
+ ///
428
+ /// This is the core persistence trait for user-facing message storage.
429
+ /// Unlike EventStore (immutable audit log), MessageStore supports:
430
+ /// - Soft delete (GDPR compliance)
431
+ /// - Message editing (optional)
432
+ /// - Thread management
433
+ #[async_trait]
434
+ pub trait MessageStore: StorageBackend {
435
+ // =========================================================================
436
+ // Thread Operations
437
+ // =========================================================================
438
+
439
+ /// Create a new thread
440
+ async fn create_thread(&self, thread: Thread) -> anyhow::Result<ThreadId>;
441
+
442
+ /// Get a thread by ID
443
+ async fn get_thread(&self, thread_id: &ThreadId) -> anyhow::Result<Option<Thread>>;
444
+
445
+ /// Update thread (title, etc.)
446
+ async fn update_thread(&self, thread: Thread) -> anyhow::Result<()>;
447
+
448
+ /// Soft-delete a thread (sets deleted_at)
449
+ async fn delete_thread(&self, thread_id: &ThreadId) -> anyhow::Result<()>;
450
+
451
+ /// List threads for a user
452
+ async fn list_threads(
453
+ &self,
454
+ tenant_id: &TenantId,
455
+ user_id: &UserId,
456
+ limit: usize,
457
+ offset: usize,
458
+ ) -> anyhow::Result<Vec<Thread>>;
459
+
460
+ // =========================================================================
461
+ // Message Operations
462
+ // =========================================================================
463
+
464
+ /// Create a new message in a thread
465
+ async fn create_message(&self, message: Message) -> anyhow::Result<MessageId>;
466
+
467
+ /// Get a message by ID
468
+ async fn get_message(&self, message_id: &MessageId) -> anyhow::Result<Option<Message>>;
469
+
470
+ /// Update a message (content, metadata)
471
+ async fn update_message(&self, message: Message) -> anyhow::Result<()>;
472
+
473
+ /// Soft-delete a message
474
+ async fn delete_message(&self, message_id: &MessageId) -> anyhow::Result<()>;
475
+
476
+ /// Get all messages in a thread (ordered by created_at)
477
+ async fn list_messages(
478
+ &self,
479
+ thread_id: &ThreadId,
480
+ include_deleted: bool,
481
+ ) -> anyhow::Result<Vec<Message>>;
482
+
483
+ /// Get messages by execution ID
484
+ async fn get_messages_by_execution(
485
+ &self,
486
+ execution_id: &ExecutionId,
487
+ ) -> anyhow::Result<Vec<Message>>;
488
+
489
+ // =========================================================================
490
+ // Convenience Methods
491
+ // =========================================================================
492
+
493
+ /// Get or create a thread (implicit thread creation)
494
+ async fn get_or_create_thread(
495
+ &self,
496
+ tenant_id: TenantId,
497
+ user_id: UserId,
498
+ ) -> anyhow::Result<Thread> {
499
+ // Default: just create a new thread
500
+ let thread = Thread::new(tenant_id, user_id);
501
+ self.create_thread(thread.clone()).await?;
502
+ Ok(thread)
503
+ }
504
+
505
+ /// Count messages in a thread
506
+ async fn count_messages(&self, thread_id: &ThreadId) -> anyhow::Result<u64> {
507
+ let messages = self.list_messages(thread_id, false).await?;
508
+ Ok(messages.len() as u64)
509
+ }
510
+ }
511
+
512
+ // =============================================================================
513
+ // InMemoryMessageStore
514
+ // =============================================================================
515
+
516
+ /// In-memory implementation of MessageStore
517
+ ///
518
+ /// Thread-safe storage using RwLock. Suitable for single-node deployments
519
+ /// and testing. For persistence across restarts, use SQLite implementation.
520
+ #[derive(Default)]
521
+ pub struct InMemoryMessageStore {
522
+ threads: RwLock<HashMap<String, Thread>>,
523
+ messages: RwLock<HashMap<String, Message>>,
524
+ }
525
+
526
+ impl InMemoryMessageStore {
527
+ /// Create a new empty message store
528
+ pub fn new() -> Self {
529
+ Self {
530
+ threads: RwLock::new(HashMap::new()),
531
+ messages: RwLock::new(HashMap::new()),
532
+ }
533
+ }
534
+
535
+ /// Create an Arc-wrapped instance for sharing
536
+ pub fn shared() -> std::sync::Arc<Self> {
537
+ std::sync::Arc::new(Self::new())
538
+ }
539
+ }
540
+
541
+ #[async_trait]
542
+ impl StorageBackend for InMemoryMessageStore {
543
+ fn name(&self) -> &str {
544
+ "in-memory-message-store"
545
+ }
546
+
547
+ fn requires_network(&self) -> bool {
548
+ false
549
+ }
550
+
551
+ async fn health_check(&self) -> anyhow::Result<()> {
552
+ Ok(())
553
+ }
554
+ }
555
+
556
+ #[async_trait]
557
+ impl MessageStore for InMemoryMessageStore {
558
+ async fn create_thread(&self, thread: Thread) -> anyhow::Result<ThreadId> {
559
+ let id = thread.id.clone();
560
+ let mut guard = self.threads.write().expect("lock poisoned");
561
+ guard.insert(id.to_string(), thread);
562
+ Ok(id)
563
+ }
564
+
565
+ async fn get_thread(&self, thread_id: &ThreadId) -> anyhow::Result<Option<Thread>> {
566
+ let guard = self.threads.read().expect("lock poisoned");
567
+ Ok(guard.get(&thread_id.to_string()).cloned())
568
+ }
569
+
570
+ async fn update_thread(&self, thread: Thread) -> anyhow::Result<()> {
571
+ let mut guard = self.threads.write().expect("lock poisoned");
572
+ guard.insert(thread.id.to_string(), thread);
573
+ Ok(())
574
+ }
575
+
576
+ async fn delete_thread(&self, thread_id: &ThreadId) -> anyhow::Result<()> {
577
+ let mut guard = self.threads.write().expect("lock poisoned");
578
+ if let Some(thread) = guard.get_mut(&thread_id.to_string()) {
579
+ thread.deleted_at = Some(Utc::now());
580
+ }
581
+ Ok(())
582
+ }
583
+
584
+ async fn list_threads(
585
+ &self,
586
+ tenant_id: &TenantId,
587
+ user_id: &UserId,
588
+ limit: usize,
589
+ offset: usize,
590
+ ) -> anyhow::Result<Vec<Thread>> {
591
+ let guard = self.threads.read().expect("lock poisoned");
592
+ let mut threads: Vec<_> = guard
593
+ .values()
594
+ .filter(|t| {
595
+ t.tenant_id == *tenant_id
596
+ && t.user_id == *user_id
597
+ && t.deleted_at.is_none()
598
+ })
599
+ .cloned()
600
+ .collect();
601
+
602
+ // Sort by created_at descending (newest first)
603
+ threads.sort_by(|a, b| b.created_at.cmp(&a.created_at));
604
+
605
+ Ok(threads.into_iter().skip(offset).take(limit).collect())
606
+ }
607
+
608
+ async fn create_message(&self, message: Message) -> anyhow::Result<MessageId> {
609
+ let id = message.id.clone();
610
+ let thread_id = message.thread_id.clone();
611
+
612
+ // Update thread's updated_at
613
+ {
614
+ let mut thread_guard = self.threads.write().expect("lock poisoned");
615
+ if let Some(thread) = thread_guard.get_mut(&thread_id.to_string()) {
616
+ thread.updated_at = Utc::now();
617
+ }
618
+ }
619
+
620
+ let mut guard = self.messages.write().expect("lock poisoned");
621
+ guard.insert(id.to_string(), message);
622
+ Ok(id)
623
+ }
624
+
625
+ async fn get_message(&self, message_id: &MessageId) -> anyhow::Result<Option<Message>> {
626
+ let guard = self.messages.read().expect("lock poisoned");
627
+ Ok(guard.get(&message_id.to_string()).cloned())
628
+ }
629
+
630
+ async fn update_message(&self, mut message: Message) -> anyhow::Result<()> {
631
+ message.updated_at = Some(Utc::now());
632
+ let mut guard = self.messages.write().expect("lock poisoned");
633
+ guard.insert(message.id.to_string(), message);
634
+ Ok(())
635
+ }
636
+
637
+ async fn delete_message(&self, message_id: &MessageId) -> anyhow::Result<()> {
638
+ let mut guard = self.messages.write().expect("lock poisoned");
639
+ if let Some(message) = guard.get_mut(&message_id.to_string()) {
640
+ message.deleted_at = Some(Utc::now());
641
+ }
642
+ Ok(())
643
+ }
644
+
645
+ async fn list_messages(
646
+ &self,
647
+ thread_id: &ThreadId,
648
+ include_deleted: bool,
649
+ ) -> anyhow::Result<Vec<Message>> {
650
+ let guard = self.messages.read().expect("lock poisoned");
651
+ let mut messages: Vec<_> = guard
652
+ .values()
653
+ .filter(|m| {
654
+ m.thread_id == *thread_id
655
+ && (include_deleted || m.deleted_at.is_none())
656
+ })
657
+ .cloned()
658
+ .collect();
659
+
660
+ // Sort by created_at ascending
661
+ messages.sort_by(|a, b| a.created_at.cmp(&b.created_at));
662
+
663
+ Ok(messages)
664
+ }
665
+
666
+ async fn get_messages_by_execution(
667
+ &self,
668
+ execution_id: &ExecutionId,
669
+ ) -> anyhow::Result<Vec<Message>> {
670
+ let guard = self.messages.read().expect("lock poisoned");
671
+ let messages: Vec<_> = guard
672
+ .values()
673
+ .filter(|m| m.execution_id.as_ref() == Some(execution_id))
674
+ .cloned()
675
+ .collect();
676
+ Ok(messages)
677
+ }
678
+ }
679
+
680
+ #[cfg(test)]
681
+ mod tests {
682
+ use super::*;
683
+
684
+ fn test_tenant() -> TenantId {
685
+ TenantId::from_string("tenant_test")
686
+ }
687
+
688
+ fn test_user() -> UserId {
689
+ UserId::from_string("user_test")
690
+ }
691
+
692
+ #[tokio::test]
693
+ async fn test_create_thread() {
694
+ let store = InMemoryMessageStore::new();
695
+ let thread = Thread::new(test_tenant(), test_user());
696
+ let id = thread.id.clone();
697
+
698
+ store.create_thread(thread).await.unwrap();
699
+
700
+ let loaded = store.get_thread(&id).await.unwrap();
701
+ assert!(loaded.is_some());
702
+ assert_eq!(loaded.unwrap().id, id);
703
+ }
704
+
705
+ #[tokio::test]
706
+ async fn test_soft_delete_thread() {
707
+ let store = InMemoryMessageStore::new();
708
+ let thread = Thread::new(test_tenant(), test_user());
709
+ let id = thread.id.clone();
710
+
711
+ store.create_thread(thread).await.unwrap();
712
+ store.delete_thread(&id).await.unwrap();
713
+
714
+ let loaded = store.get_thread(&id).await.unwrap().unwrap();
715
+ assert!(loaded.is_deleted());
716
+ }
717
+
718
+ #[tokio::test]
719
+ async fn test_list_threads_excludes_deleted() {
720
+ let store = InMemoryMessageStore::new();
721
+ let tenant = test_tenant();
722
+ let user = test_user();
723
+
724
+ // Create two threads
725
+ let thread1 = Thread::new(tenant.clone(), user.clone());
726
+ let thread2 = Thread::new(tenant.clone(), user.clone());
727
+ let id2 = thread2.id.clone();
728
+
729
+ store.create_thread(thread1).await.unwrap();
730
+ store.create_thread(thread2).await.unwrap();
731
+
732
+ // Delete one
733
+ store.delete_thread(&id2).await.unwrap();
734
+
735
+ // List should only return 1
736
+ let threads = store.list_threads(&tenant, &user, 100, 0).await.unwrap();
737
+ assert_eq!(threads.len(), 1);
738
+ }
739
+
740
+ #[tokio::test]
741
+ async fn test_create_message() {
742
+ let store = InMemoryMessageStore::new();
743
+ let thread = Thread::new(test_tenant(), test_user());
744
+ let thread_id = thread.id.clone();
745
+ store.create_thread(thread).await.unwrap();
746
+
747
+ let message = Message::user(thread_id.clone(), "Hello!");
748
+ let msg_id = message.id.clone();
749
+ store.create_message(message).await.unwrap();
750
+
751
+ let loaded = store.get_message(&msg_id).await.unwrap();
752
+ assert!(loaded.is_some());
753
+ assert_eq!(loaded.unwrap().content, "Hello!");
754
+ }
755
+
756
+ #[tokio::test]
757
+ async fn test_message_parent_chain() {
758
+ let store = InMemoryMessageStore::new();
759
+ let thread = Thread::new(test_tenant(), test_user());
760
+ let thread_id = thread.id.clone();
761
+ store.create_thread(thread).await.unwrap();
762
+
763
+ // User message
764
+ let user_msg = Message::user(thread_id.clone(), "What's the weather?");
765
+ let user_msg_id = user_msg.id.clone();
766
+ store.create_message(user_msg).await.unwrap();
767
+
768
+ // Assistant response
769
+ let exec_id = ExecutionId::new();
770
+ let assistant_msg = Message::assistant(
771
+ thread_id.clone(),
772
+ exec_id,
773
+ "The weather is sunny.",
774
+ Some(user_msg_id.clone()),
775
+ );
776
+ store.create_message(assistant_msg).await.unwrap();
777
+
778
+ // List messages
779
+ let messages = store.list_messages(&thread_id, false).await.unwrap();
780
+ assert_eq!(messages.len(), 2);
781
+ assert_eq!(messages[0].role, MessageRole::User);
782
+ assert_eq!(messages[1].role, MessageRole::Assistant);
783
+ assert_eq!(messages[1].parent_id, Some(user_msg_id));
784
+ }
785
+
786
+ #[tokio::test]
787
+ async fn test_soft_delete_message() {
788
+ let store = InMemoryMessageStore::new();
789
+ let thread = Thread::new(test_tenant(), test_user());
790
+ let thread_id = thread.id.clone();
791
+ store.create_thread(thread).await.unwrap();
792
+
793
+ let message = Message::user(thread_id.clone(), "Delete me");
794
+ let msg_id = message.id.clone();
795
+ store.create_message(message).await.unwrap();
796
+
797
+ store.delete_message(&msg_id).await.unwrap();
798
+
799
+ // Should not appear in normal list
800
+ let messages = store.list_messages(&thread_id, false).await.unwrap();
801
+ assert_eq!(messages.len(), 0);
802
+
803
+ // Should appear with include_deleted
804
+ let all_messages = store.list_messages(&thread_id, true).await.unwrap();
805
+ assert_eq!(all_messages.len(), 1);
806
+ assert!(all_messages[0].is_deleted());
807
+ }
808
+
809
+ #[tokio::test]
810
+ async fn test_get_messages_by_execution() {
811
+ let store = InMemoryMessageStore::new();
812
+ let thread = Thread::new(test_tenant(), test_user());
813
+ let thread_id = thread.id.clone();
814
+ store.create_thread(thread).await.unwrap();
815
+
816
+ let exec_id = ExecutionId::new();
817
+ let msg = Message::assistant(thread_id, exec_id.clone(), "Response", None);
818
+ store.create_message(msg).await.unwrap();
819
+
820
+ let messages = store.get_messages_by_execution(&exec_id).await.unwrap();
821
+ assert_eq!(messages.len(), 1);
822
+ assert_eq!(messages[0].execution_id, Some(exec_id));
823
+ }
824
+
825
+ #[tokio::test]
826
+ async fn test_message_with_parts() {
827
+ let store = InMemoryMessageStore::new();
828
+ let thread = Thread::new(test_tenant(), test_user());
829
+ let thread_id = thread.id.clone();
830
+ store.create_thread(thread).await.unwrap();
831
+
832
+ let exec_id = ExecutionId::new();
833
+ let parts = vec![
834
+ MessagePart::Reasoning {
835
+ text: "Let me think...".to_string(),
836
+ },
837
+ MessagePart::ToolCall {
838
+ tool_call_id: "tc_123".to_string(),
839
+ tool_name: "get_weather".to_string(),
840
+ args: serde_json::json!({"city": "NYC"}),
841
+ },
842
+ MessagePart::Text {
843
+ text: "The weather is sunny.".to_string(),
844
+ },
845
+ ];
846
+
847
+ let msg = Message::assistant(thread_id, exec_id, "The weather is sunny.", None)
848
+ .with_parts(parts);
849
+ let msg_id = msg.id.clone();
850
+ store.create_message(msg).await.unwrap();
851
+
852
+ let loaded = store.get_message(&msg_id).await.unwrap().unwrap();
853
+ assert_eq!(loaded.parts.len(), 3);
854
+ }
855
+
856
+ #[tokio::test]
857
+ async fn test_message_metadata() {
858
+ let store = InMemoryMessageStore::new();
859
+ let thread = Thread::new(test_tenant(), test_user());
860
+ let thread_id = thread.id.clone();
861
+ store.create_thread(thread).await.unwrap();
862
+
863
+ let exec_id = ExecutionId::new();
864
+ let metadata = MessageMetadata {
865
+ model: Some("gpt-4o".to_string()),
866
+ provider: Some("azure".to_string()),
867
+ duration_ms: Some(1500),
868
+ token_usage: Some(TokenUsage::new(100, 200)),
869
+ stats: Some(ExecutionStats {
870
+ llm_calls: 2,
871
+ tool_calls: 1,
872
+ sub_agents: 0,
873
+ steps: 3,
874
+ decisions: 1,
875
+ artifacts: 0,
876
+ }),
877
+ finish_reason: Some(FinishReason::Stop),
878
+ ..Default::default()
879
+ };
880
+
881
+ let msg = Message::assistant(thread_id, exec_id, "Response", None)
882
+ .with_metadata(metadata);
883
+ let msg_id = msg.id.clone();
884
+ store.create_message(msg).await.unwrap();
885
+
886
+ let loaded = store.get_message(&msg_id).await.unwrap().unwrap();
887
+ assert_eq!(loaded.metadata.model, Some("gpt-4o".to_string()));
888
+ assert_eq!(loaded.metadata.stats.as_ref().unwrap().llm_calls, 2);
889
+ }
890
+
891
+ #[tokio::test]
892
+ async fn test_thread_updated_on_message() {
893
+ let store = InMemoryMessageStore::new();
894
+ let thread = Thread::new(test_tenant(), test_user());
895
+ let thread_id = thread.id.clone();
896
+ let original_updated = thread.updated_at;
897
+ store.create_thread(thread).await.unwrap();
898
+
899
+ // Small delay to ensure timestamp differs
900
+ tokio::time::sleep(std::time::Duration::from_millis(10)).await;
901
+
902
+ let message = Message::user(thread_id.clone(), "Hello!");
903
+ store.create_message(message).await.unwrap();
904
+
905
+ let loaded_thread = store.get_thread(&thread_id).await.unwrap().unwrap();
906
+ assert!(loaded_thread.updated_at > original_updated);
907
+ }
908
+ }