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,1283 @@
1
+ //! ExecutionKernel - The core execution engine
2
+ //!
3
+ //! The kernel is the single point of execution. It:
4
+ //! - Owns the Execution state
5
+ //! - Applies actions through the reducer
6
+ //! - Emits events for observers
7
+ //! - Enforces invariants
8
+ //!
9
+ //! All execution MUST go through the kernel.
10
+ //!
11
+ //! ## ⚠️ CODE OWNERSHIP & FORBIDDEN PATTERNS
12
+ //!
13
+ //! **This module is the SINGLE SOURCE OF TRUTH for execution orchestration.**
14
+ //!
15
+ //! ### Code Ownership
16
+ //! - Only `kernel::kernel` (ExecutionKernel) may orchestrate execution
17
+ //! - ExecutionKernel owns the Execution state
18
+ //! - All state transitions MUST go through `kernel::reducer::reduce()`
19
+ //!
20
+ //! ### Explicitly Forbidden Patterns
21
+ //!
22
+ //! These patterns are **forbidden forever**. If any of these happen, Enact loses its "Now" guarantee.
23
+ //!
24
+ //! 1. **Kernel calling providers directly** – Providers are resolved before kernel execution.
25
+ //! - The kernel receives resolved providers via `ExecutionRequest`, not provider names or registry lookups
26
+ //! - No global registries or dynamic discovery in kernel
27
+ //! - Provider resolution happens outside kernel (in runner/control plane)
28
+ //!
29
+ //! 2. **Streaming mutating state** – Streaming only subscribes and delivers events, never mutates execution state.
30
+ //! - Streaming is a read-only observer
31
+ //! - EventEmitter must not have access to ExecutionKernel, Reducer, or ExecutionState for mutation
32
+ //!
33
+ //! 3. **Signals driving execution** – Signals are hints only, never drive state transitions.
34
+ //! - SignalBus implementations must not have access to ExecutionKernel, Reducer, or ExecutionState
35
+ //! - Signals cannot directly trigger state changes
36
+ //!
37
+ //! 4. **Tools bypassing ToolPolicy** – All tool execution MUST go through ToolExecutor.
38
+ //! - ToolExecutor enforces ToolPolicy before every invocation
39
+ //! - No direct tool calls from kernel
40
+ //!
41
+ //! 5. **Context being optional** – TenantContext is REQUIRED for all executions.
42
+ //! - There is no "system" execution without a tenant
43
+ //! - All execution methods must require TenantContext
44
+ //!
45
+ //! 6. **IDs being redefined outside kernel** – Kernel is the ONLY source of truth for IDs.
46
+ //! - No other module may define ExecutionId, StepId, or other execution identifiers
47
+ //! - All IDs must come from `kernel::ids`
48
+ //!
49
+ //! ### Invariants Enforced
50
+ //!
51
+ //! - **Single source of truth**: Only the kernel may mutate `Execution` or `Step` state via `kernel::reducer`
52
+ //! - **Policy-first enforcement**: All policy checks run before external providers; decisions are recorded as events
53
+ //! - **Execution Service Parity**: Enact must always run as a decoupled execution service (HTTP/gRPC)
54
+ //! - **Observable decision points**: Branching decisions emit `Decision` events with evidence artifacts
55
+ //!
56
+ //! @see docs/TECHNICAL/04-KERNEL_INVARIANTS.md
57
+ //!
58
+ //! ## Error Handling (feat-02)
59
+ //!
60
+ //! All failures use `ExecutionError` which provides:
61
+ //! - Deterministic retry decisions
62
+ //! - Structured error categories
63
+ //! - Backoff hints
64
+ //! - Idempotency tracking
65
+
66
+ use super::artifact::{ArtifactStore, ArtifactType, PutArtifactRequest};
67
+ use super::enforcement::{
68
+ EnforcementMiddleware, EnforcementResult, ExecutionUsage, LongRunningExecutionPolicy,
69
+ };
70
+ use super::error::ExecutionError;
71
+ use super::execution_model::Execution;
72
+ use super::reducer::{reduce, ExecutionAction, ReducerError};
73
+ use super::execution_state::{ExecutionState, WaitReason};
74
+ use super::ids::{ArtifactId, ExecutionId, SpawnMode, StepId, StepType};
75
+ use crate::context::TenantContext;
76
+ use crate::graph::{CompiledGraph, NodeState};
77
+ use crate::inbox::{ControlAction, InboxMessage, InboxStore};
78
+ use crate::streaming::{EventEmitter, ProtectedEventEmitter, StreamEvent};
79
+ use std::sync::Arc;
80
+ use std::time::Instant;
81
+ use tokio_util::sync::CancellationToken;
82
+
83
+ /// Action to take after processing inbox messages
84
+ #[derive(Debug, Clone)]
85
+ enum InboxAction {
86
+ /// Continue execution normally
87
+ Continue,
88
+ /// Pause execution
89
+ Pause,
90
+ /// Cancel execution with reason
91
+ Cancel(String),
92
+ }
93
+
94
+ /// ExecutionKernel - the core execution engine
95
+ ///
96
+ /// This is THE place where execution happens. Runner wires things up,
97
+ /// but kernel does the work.
98
+ ///
99
+ /// ## Invariant: TenantContext is REQUIRED
100
+ ///
101
+ /// Every execution MUST have a TenantContext. This ensures:
102
+ /// - Multi-tenant isolation
103
+ /// - Resource limit enforcement
104
+ /// - Billing attribution
105
+ /// - Audit compliance
106
+ pub struct ExecutionKernel {
107
+ /// Current execution state
108
+ execution: Execution,
109
+ /// Tenant context (REQUIRED - enforces multi-tenant isolation)
110
+ tenant_context: TenantContext,
111
+ /// Event emitter for streaming
112
+ emitter: EventEmitter,
113
+ /// Protected event emitter for sensitive content (feat-guardrails)
114
+ ///
115
+ /// When configured, events with potentially sensitive content (step outputs,
116
+ /// tool results) are emitted through this emitter which applies protection
117
+ /// processors (PII masking, content filtering, etc.) before streaming.
118
+ protected_emitter: Option<ProtectedEventEmitter>,
119
+ /// Cancellation token for async cancellation (proper cooperative cancellation)
120
+ cancellation_token: CancellationToken,
121
+ /// Inbox store for mid-execution guidance (INV-INBOX-*)
122
+ inbox: Option<Arc<dyn InboxStore>>,
123
+ /// Artifact store for storing execution artifacts (feat-04)
124
+ artifact_store: Option<Arc<dyn ArtifactStore>>,
125
+ /// Enforcement middleware for resource limits (feat-03)
126
+ ///
127
+ /// Tracks usage (steps, tokens, cost, discovery depth) and enforces limits
128
+ /// before each step execution. Integrated with long-running execution controls.
129
+ enforcement: Arc<EnforcementMiddleware>,
130
+ /// Long-running execution policy (agentic DAG controls)
131
+ ///
132
+ /// Controls discovery depth, discovered step limits, cost thresholds,
133
+ /// and idle timeout for long-running agentic executions.
134
+ long_running_policy: LongRunningExecutionPolicy,
135
+ /// Execution usage tracker (registered with enforcement middleware)
136
+ usage: Option<Arc<ExecutionUsage>>,
137
+ /// SpawnMode - how this execution was spawned (for inbox routing)
138
+ ///
139
+ /// Controls inbox message routing:
140
+ /// - Inline: shares parent's inbox
141
+ /// - Child { inherit_inbox: true }: checks both parent and own inbox
142
+ /// - Child { inherit_inbox: false }: isolated inbox
143
+ ///
144
+ /// @see docs/TECHNICAL/32-SPAWN-MODE.md
145
+ spawn_mode: Option<SpawnMode>,
146
+ /// Parent execution ID (if spawned as child)
147
+ ///
148
+ /// Used for inbox inheritance when spawn_mode is Child with inherit_inbox=true
149
+ parent_execution_id: Option<ExecutionId>,
150
+ }
151
+
152
+ impl ExecutionKernel {
153
+ /// Create a new kernel with a fresh execution
154
+ ///
155
+ /// ## Arguments
156
+ /// * `tenant_context` - REQUIRED tenant context for multi-tenant isolation
157
+ ///
158
+ /// ## Invariant
159
+ /// TenantContext is REQUIRED for all executions. There is no "system" execution
160
+ /// without a tenant - this is enforced at compile time.
161
+ pub fn new(tenant_context: TenantContext) -> Self {
162
+ let mut execution = Execution::new();
163
+ execution.tenant_id = Some(tenant_context.tenant_id.clone());
164
+ Self {
165
+ execution,
166
+ tenant_context,
167
+ emitter: EventEmitter::new(),
168
+ protected_emitter: None,
169
+ cancellation_token: CancellationToken::new(),
170
+ inbox: None,
171
+ artifact_store: None,
172
+ enforcement: Arc::new(EnforcementMiddleware::new()),
173
+ long_running_policy: LongRunningExecutionPolicy::standard(),
174
+ usage: None,
175
+ spawn_mode: None,
176
+ parent_execution_id: None,
177
+ }
178
+ }
179
+
180
+ /// Create a kernel with an existing execution (for replay)
181
+ ///
182
+ /// ## Arguments
183
+ /// * `execution` - Existing execution state to resume
184
+ /// * `tenant_context` - REQUIRED tenant context (must match execution's tenant)
185
+ pub fn with_execution(execution: Execution, tenant_context: TenantContext) -> Self {
186
+ // Note: In production, we should verify execution.tenant_id matches tenant_context.tenant_id
187
+ Self {
188
+ execution,
189
+ tenant_context,
190
+ emitter: EventEmitter::new(),
191
+ protected_emitter: None,
192
+ cancellation_token: CancellationToken::new(),
193
+ inbox: None,
194
+ artifact_store: None,
195
+ enforcement: Arc::new(EnforcementMiddleware::new()),
196
+ long_running_policy: LongRunningExecutionPolicy::standard(),
197
+ usage: None,
198
+ spawn_mode: None,
199
+ parent_execution_id: None,
200
+ }
201
+ }
202
+
203
+ /// Set the protected event emitter for content protection
204
+ ///
205
+ /// When set, events with potentially sensitive content (step outputs,
206
+ /// tool results, etc.) are emitted through this protected emitter which
207
+ /// applies protection processors before streaming.
208
+ ///
209
+ /// ## Usage
210
+ /// ```ignore
211
+ /// use enact_core::streaming::{ProtectedEventEmitter, PiiProtectionProcessor};
212
+ ///
213
+ /// let protected_emitter = ProtectedEventEmitter::new()
214
+ /// .with_processor(Arc::new(PiiProtectionProcessor::new()));
215
+ ///
216
+ /// let kernel = ExecutionKernel::new(tenant_context)
217
+ /// .with_protected_emitter(protected_emitter);
218
+ /// ```
219
+ pub fn with_protected_emitter(mut self, emitter: ProtectedEventEmitter) -> Self {
220
+ self.protected_emitter = Some(emitter);
221
+ self
222
+ }
223
+
224
+ /// Set the inbox store for mid-execution guidance
225
+ ///
226
+ /// When set, the kernel will check the inbox after every step (INV-INBOX-001)
227
+ /// and process messages in priority order (INV-INBOX-002).
228
+ pub fn with_inbox(mut self, inbox: Arc<dyn InboxStore>) -> Self {
229
+ self.inbox = Some(inbox);
230
+ self
231
+ }
232
+
233
+ /// Get the inbox store (if set)
234
+ pub fn inbox(&self) -> Option<&Arc<dyn InboxStore>> {
235
+ self.inbox.as_ref()
236
+ }
237
+
238
+ /// Set the artifact store for storing execution artifacts
239
+ ///
240
+ /// When set, the kernel can store artifacts produced by steps.
241
+ /// Artifacts are emitted as events for audit trail.
242
+ pub fn with_artifact_store(mut self, store: Arc<dyn ArtifactStore>) -> Self {
243
+ self.artifact_store = Some(store);
244
+ self
245
+ }
246
+
247
+ /// Get the artifact store (if set)
248
+ pub fn artifact_store(&self) -> Option<&Arc<dyn ArtifactStore>> {
249
+ self.artifact_store.as_ref()
250
+ }
251
+
252
+ /// Set the enforcement middleware for resource limits
253
+ ///
254
+ /// When set, the kernel uses this enforcement middleware to track usage
255
+ /// and check limits before each step execution.
256
+ pub fn with_enforcement(mut self, enforcement: Arc<EnforcementMiddleware>) -> Self {
257
+ self.enforcement = enforcement;
258
+ self
259
+ }
260
+
261
+ /// Get the enforcement middleware
262
+ pub fn enforcement(&self) -> &Arc<EnforcementMiddleware> {
263
+ &self.enforcement
264
+ }
265
+
266
+ /// Set the long-running execution policy
267
+ ///
268
+ /// Controls discovery depth, discovered step limits, cost thresholds,
269
+ /// and idle timeout for long-running agentic executions.
270
+ pub fn with_long_running_policy(mut self, policy: LongRunningExecutionPolicy) -> Self {
271
+ self.long_running_policy = policy;
272
+ self
273
+ }
274
+
275
+ /// Get the long-running execution policy
276
+ pub fn long_running_policy(&self) -> &LongRunningExecutionPolicy {
277
+ &self.long_running_policy
278
+ }
279
+
280
+ /// Set the spawn mode for this execution
281
+ ///
282
+ /// Controls inbox message routing:
283
+ /// - Inline: shares parent's inbox (same ExecutionId)
284
+ /// - Child { inherit_inbox: true }: checks both parent and own inbox
285
+ /// - Child { inherit_inbox: false }: isolated inbox
286
+ ///
287
+ /// @see docs/TECHNICAL/32-SPAWN-MODE.md
288
+ pub fn with_spawn_mode(mut self, spawn_mode: SpawnMode) -> Self {
289
+ self.spawn_mode = Some(spawn_mode);
290
+ self
291
+ }
292
+
293
+ /// Get the spawn mode (if set)
294
+ pub fn spawn_mode(&self) -> Option<&SpawnMode> {
295
+ self.spawn_mode.as_ref()
296
+ }
297
+
298
+ /// Set the parent execution ID for child executions
299
+ ///
300
+ /// Used for inbox inheritance when spawn_mode is Child with inherit_inbox=true.
301
+ /// Must be set when spawning child executions that need to inherit parent's inbox.
302
+ pub fn with_parent_execution_id(mut self, parent_id: ExecutionId) -> Self {
303
+ self.parent_execution_id = Some(parent_id);
304
+ self
305
+ }
306
+
307
+ /// Get the parent execution ID (if set)
308
+ pub fn parent_execution_id(&self) -> Option<&ExecutionId> {
309
+ self.parent_execution_id.as_ref()
310
+ }
311
+
312
+ /// Get current usage snapshot for this execution
313
+ pub fn usage_snapshot(&self) -> Option<super::enforcement::UsageSnapshot> {
314
+ self.usage.as_ref().map(|u| super::enforcement::UsageSnapshot::from(u.as_ref()))
315
+ }
316
+
317
+ /// Register this execution with the enforcement middleware
318
+ ///
319
+ /// Call this at the start of execution to begin tracking usage.
320
+ /// Returns the usage tracker for this execution.
321
+ pub async fn register_for_enforcement(&mut self) -> Arc<ExecutionUsage> {
322
+ let usage = self.enforcement
323
+ .register_execution(
324
+ self.execution.id.clone(),
325
+ self.tenant_context.tenant_id.clone(),
326
+ )
327
+ .await;
328
+ self.usage = Some(Arc::clone(&usage));
329
+ usage
330
+ }
331
+
332
+ /// Unregister this execution from the enforcement middleware
333
+ ///
334
+ /// Call this at the end of execution to stop tracking usage.
335
+ pub async fn unregister_from_enforcement(&self) {
336
+ self.enforcement.unregister_execution(&self.execution.id).await;
337
+ }
338
+
339
+ /// Check all resource limits before executing a step
340
+ ///
341
+ /// This checks:
342
+ /// - Basic limits (steps, tokens, wall time)
343
+ /// - Long-running limits (discovery depth, discovered steps, cost, idle)
344
+ ///
345
+ /// Returns an error if any limit is exceeded.
346
+ pub async fn check_limits_before_step(&self) -> Result<(), ExecutionError> {
347
+ // Check basic resource limits
348
+ let basic_result = self.enforcement
349
+ .check_all_limits(&self.execution.id, &self.tenant_context.limits)
350
+ .await;
351
+
352
+ if let EnforcementResult::Blocked(violation) = basic_result {
353
+ return Err(violation.to_error());
354
+ }
355
+
356
+ // Check long-running execution limits
357
+ let long_running_result = self.enforcement
358
+ .check_long_running_limits(&self.execution.id, &self.long_running_policy)
359
+ .await;
360
+
361
+ if let EnforcementResult::Blocked(violation) = long_running_result {
362
+ return Err(violation.to_error());
363
+ }
364
+
365
+ // Emit warning events if approaching limits
366
+ if self.enforcement.emit_warning_events_enabled() {
367
+ if let Some(warning) = match (&basic_result, &long_running_result) {
368
+ (EnforcementResult::Warning(w), _) => Some(w),
369
+ (_, EnforcementResult::Warning(w)) => Some(w),
370
+ _ => None,
371
+ } {
372
+ tracing::warn!(
373
+ execution_id = %self.execution.id,
374
+ warning_type = ?warning.warning_type,
375
+ usage_percent = warning.usage_percent,
376
+ message = %warning.message,
377
+ "Enforcement warning"
378
+ );
379
+ self.emitter.emit(StreamEvent::policy_decision_warn(
380
+ &self.execution.id,
381
+ None,
382
+ "enforcement",
383
+ warning.message.clone(),
384
+ ));
385
+ }
386
+ }
387
+ Ok(())
388
+ }
389
+
390
+ /// Record step completion with the enforcement middleware
391
+ pub async fn record_step_completed(&self) {
392
+ self.enforcement.record_step(&self.execution.id).await;
393
+ }
394
+
395
+ /// Record token usage with the enforcement middleware
396
+ pub async fn record_token_usage(&self, input_tokens: u32, output_tokens: u32) {
397
+ self.enforcement
398
+ .record_tokens(&self.execution.id, input_tokens, output_tokens)
399
+ .await;
400
+ }
401
+
402
+ /// Record cost with the enforcement middleware
403
+ pub async fn record_cost(&self, cost_usd: f64) {
404
+ self.enforcement
405
+ .record_cost(&self.execution.id, cost_usd)
406
+ .await;
407
+ }
408
+
409
+ /// Record a discovered step with the enforcement middleware
410
+ pub async fn record_discovered_step(&self) {
411
+ self.enforcement
412
+ .record_discovered_step(&self.execution.id)
413
+ .await;
414
+ }
415
+
416
+ /// Push discovery depth (entering a sub-agent execution)
417
+ pub async fn push_discovery_depth(&self) {
418
+ self.enforcement
419
+ .push_discovery_depth(&self.execution.id)
420
+ .await;
421
+ }
422
+
423
+ /// Pop discovery depth (exiting a sub-agent execution)
424
+ pub async fn pop_discovery_depth(&self) {
425
+ self.enforcement
426
+ .pop_discovery_depth(&self.execution.id)
427
+ .await;
428
+ }
429
+
430
+ /// Store an artifact produced by a step
431
+ ///
432
+ /// This method:
433
+ /// 1. Stores the artifact in the artifact store
434
+ /// 2. Emits an ArtifactCreated event for audit trail
435
+ /// 3. Returns the artifact ID
436
+ ///
437
+ /// ## Arguments
438
+ /// * `step_id` - The step that produced this artifact
439
+ /// * `name` - Name of the artifact
440
+ /// * `artifact_type` - Type of artifact
441
+ /// * `content` - Raw content bytes
442
+ ///
443
+ /// ## Returns
444
+ /// The generated ArtifactId, or None if no artifact store is configured
445
+ pub async fn store_artifact(
446
+ &self,
447
+ step_id: &StepId,
448
+ name: impl Into<String>,
449
+ artifact_type: ArtifactType,
450
+ content: Vec<u8>,
451
+ ) -> Option<ArtifactId> {
452
+ let store = self.artifact_store.as_ref()?;
453
+
454
+ let request = PutArtifactRequest::new(
455
+ self.execution.id.clone(),
456
+ step_id.clone(),
457
+ name,
458
+ artifact_type.clone(),
459
+ content,
460
+ );
461
+
462
+ match store.put(request).await {
463
+ Ok(response) => {
464
+ // Emit ArtifactCreated event for audit trail
465
+ let artifact_type_str = format!("{:?}", artifact_type);
466
+ self.emitter.emit(StreamEvent::artifact_created(
467
+ &self.execution.id,
468
+ step_id,
469
+ &response.artifact_id,
470
+ artifact_type_str,
471
+ ));
472
+
473
+ tracing::debug!(
474
+ execution_id = %self.execution.id,
475
+ step_id = %step_id,
476
+ artifact_id = %response.artifact_id,
477
+ "Artifact stored"
478
+ );
479
+
480
+ Some(response.artifact_id)
481
+ }
482
+ Err(e) => {
483
+ tracing::warn!(
484
+ execution_id = %self.execution.id,
485
+ step_id = %step_id,
486
+ error = %e,
487
+ "Failed to store artifact"
488
+ );
489
+ None
490
+ }
491
+ }
492
+ }
493
+
494
+ /// Store a text artifact (convenience method)
495
+ pub async fn store_text_artifact(
496
+ &self,
497
+ step_id: &StepId,
498
+ name: impl Into<String>,
499
+ content: impl Into<String>,
500
+ ) -> Option<ArtifactId> {
501
+ self.store_artifact(step_id, name, ArtifactType::Text, content.into().into_bytes()).await
502
+ }
503
+
504
+ /// Store a JSON artifact (convenience method)
505
+ pub async fn store_json_artifact(
506
+ &self,
507
+ step_id: &StepId,
508
+ name: impl Into<String>,
509
+ value: &serde_json::Value,
510
+ ) -> Option<ArtifactId> {
511
+ let content = serde_json::to_vec_pretty(value).ok()?;
512
+ self.store_artifact(step_id, name, ArtifactType::Json, content).await
513
+ }
514
+
515
+ /// Get the tenant context
516
+ pub fn tenant_context(&self) -> &TenantContext {
517
+ &self.tenant_context
518
+ }
519
+
520
+ /// Get the execution ID
521
+ pub fn execution_id(&self) -> &ExecutionId {
522
+ &self.execution.id
523
+ }
524
+
525
+ /// Get the current execution state
526
+ pub fn state(&self) -> ExecutionState {
527
+ self.execution.state
528
+ }
529
+
530
+ /// Get the event emitter
531
+ pub fn emitter(&self) -> &EventEmitter {
532
+ &self.emitter
533
+ }
534
+
535
+ /// Get the execution reference
536
+ pub fn execution(&self) -> &Execution {
537
+ &self.execution
538
+ }
539
+
540
+ /// Check if cancelled
541
+ ///
542
+ /// Uses CancellationToken for proper async cancellation support.
543
+ pub fn is_cancelled(&self) -> bool {
544
+ self.cancellation_token.is_cancelled()
545
+ }
546
+
547
+ /// Cancel the execution
548
+ ///
549
+ /// This triggers cooperative cancellation of all async operations.
550
+ /// The actual state transition happens through dispatch.
551
+ pub fn cancel(&self, _reason: impl Into<String>) {
552
+ self.cancellation_token.cancel();
553
+ // Note: actual state transition happens through dispatch
554
+ }
555
+
556
+ /// Get a child cancellation token
557
+ ///
558
+ /// Child tokens are cancelled when the parent is cancelled,
559
+ /// but cancelling a child doesn't affect the parent.
560
+ pub fn child_cancellation_token(&self) -> CancellationToken {
561
+ self.cancellation_token.child_token()
562
+ }
563
+
564
+ /// Get the cancellation token for use in async operations
565
+ ///
566
+ /// Use this with `tokio::select!` to make async operations cancellable:
567
+ /// ```ignore
568
+ /// tokio::select! {
569
+ /// _ = token.cancelled() => { /* handle cancellation */ }
570
+ /// result = some_async_operation() => { /* handle result */ }
571
+ /// }
572
+ /// ```
573
+ pub fn cancellation_token(&self) -> &CancellationToken {
574
+ &self.cancellation_token
575
+ }
576
+
577
+ /// Dispatch an action to the reducer
578
+ ///
579
+ /// This is the ONLY way to change execution state.
580
+ pub fn dispatch(&mut self, action: ExecutionAction) -> Result<(), ReducerError> {
581
+ // Apply through reducer
582
+ reduce(&mut self.execution, action.clone())?;
583
+
584
+ // Emit corresponding event
585
+ self.emit_event_for_action(&action);
586
+
587
+ Ok(())
588
+ }
589
+
590
+ /// Start execution
591
+ pub fn start(&mut self) -> Result<(), ReducerError> {
592
+ self.dispatch(ExecutionAction::Start)
593
+ }
594
+
595
+ /// Begin a step
596
+ pub fn begin_step(
597
+ &mut self,
598
+ step_type: StepType,
599
+ name: impl Into<String>,
600
+ parent_step_id: Option<StepId>,
601
+ ) -> Result<StepId, ReducerError> {
602
+ let step_id = StepId::new();
603
+ self.dispatch(ExecutionAction::StepStarted {
604
+ step_id: step_id.clone(),
605
+ parent_step_id,
606
+ step_type,
607
+ name: name.into(),
608
+ source: None,
609
+ })?;
610
+ Ok(step_id)
611
+ }
612
+
613
+ /// Complete a step
614
+ pub fn complete_step(
615
+ &mut self,
616
+ step_id: StepId,
617
+ output: Option<String>,
618
+ duration_ms: u64,
619
+ ) -> Result<(), ReducerError> {
620
+ self.dispatch(ExecutionAction::StepCompleted {
621
+ step_id,
622
+ output,
623
+ duration_ms,
624
+ })
625
+ }
626
+
627
+ /// Fail a step with a structured error (feat-02)
628
+ pub fn fail_step(&mut self, step_id: StepId, error: ExecutionError) -> Result<(), ReducerError> {
629
+ self.dispatch(ExecutionAction::StepFailed {
630
+ step_id,
631
+ error,
632
+ })
633
+ }
634
+
635
+ /// Fail a step with a simple message (creates a KernelInternal error)
636
+ pub fn fail_step_with_message(&mut self, step_id: StepId, message: impl Into<String>) -> Result<(), ReducerError> {
637
+ self.fail_step(step_id, ExecutionError::kernel_internal(message))
638
+ }
639
+
640
+ /// Pause execution
641
+ pub fn pause(&mut self, reason: impl Into<String>) -> Result<(), ReducerError> {
642
+ self.dispatch(ExecutionAction::Pause {
643
+ reason: reason.into(),
644
+ })
645
+ }
646
+
647
+ /// Resume execution
648
+ pub fn resume(&mut self) -> Result<(), ReducerError> {
649
+ self.dispatch(ExecutionAction::Resume)
650
+ }
651
+
652
+ /// Enter waiting state
653
+ pub fn wait_for(&mut self, reason: WaitReason) -> Result<(), ReducerError> {
654
+ self.dispatch(ExecutionAction::Wait { reason })
655
+ }
656
+
657
+ /// Signal that external input was received
658
+ pub fn input_received(&mut self) -> Result<(), ReducerError> {
659
+ self.dispatch(ExecutionAction::InputReceived)
660
+ }
661
+
662
+ /// Complete execution
663
+ pub fn complete(&mut self, output: Option<String>) -> Result<(), ReducerError> {
664
+ self.dispatch(ExecutionAction::Complete { output })
665
+ }
666
+
667
+ /// Fail execution with a structured error (feat-02)
668
+ pub fn fail(&mut self, error: ExecutionError) -> Result<(), ReducerError> {
669
+ self.dispatch(ExecutionAction::Fail { error })
670
+ }
671
+
672
+ /// Fail execution with a simple message (creates a KernelInternal error)
673
+ pub fn fail_with_message(&mut self, message: impl Into<String>) -> Result<(), ReducerError> {
674
+ self.fail(ExecutionError::kernel_internal(message))
675
+ }
676
+
677
+ /// Cancel execution
678
+ pub fn cancel_execution(&mut self, reason: impl Into<String>) -> Result<(), ReducerError> {
679
+ self.dispatch(ExecutionAction::Cancel {
680
+ reason: reason.into(),
681
+ })
682
+ }
683
+
684
+ /// Execute a compiled graph
685
+ ///
686
+ /// ## Invariants
687
+ /// - INV-INBOX-001: Inbox is checked after every step
688
+ /// - INV-INBOX-002: Control messages are processed first (via priority_order)
689
+ /// - INV-INBOX-003: Inbox events are emitted for audit trail
690
+ ///
691
+ /// ## Async Cancellation
692
+ /// Uses tokio::select! with CancellationToken for cooperative cancellation.
693
+ /// Node execution can be interrupted cleanly if cancellation is requested.
694
+ pub async fn execute_graph(
695
+ &mut self,
696
+ graph: &CompiledGraph,
697
+ input: &str,
698
+ ) -> anyhow::Result<NodeState> {
699
+ // Start execution
700
+ self.start()?;
701
+
702
+ let mut state = NodeState::from_str(input);
703
+ let mut current_node = graph.entry_point().to_string();
704
+
705
+ // Get cancellation token for use in select!
706
+ let cancel_token = self.cancellation_token.clone();
707
+
708
+ loop {
709
+ // Check for cancellation before each step
710
+ if self.is_cancelled() {
711
+ self.cancel_execution("Cancelled by user")?;
712
+ anyhow::bail!("Execution cancelled");
713
+ }
714
+
715
+ // Get the node
716
+ let node = graph
717
+ .get_node(&current_node)
718
+ .ok_or_else(|| anyhow::anyhow!("Node '{}' not found", current_node))?;
719
+
720
+ // Begin step
721
+ let step_start = Instant::now();
722
+ let step_id = self.begin_step(
723
+ StepType::FunctionNode,
724
+ current_node.clone(),
725
+ None,
726
+ )?;
727
+
728
+ // Execute node with cancellation support using tokio::select!
729
+ // This allows long-running operations to be interrupted
730
+ let node_future = node.execute(state.clone());
731
+ let result = tokio::select! {
732
+ biased; // Check cancellation first
733
+
734
+ _ = cancel_token.cancelled() => {
735
+ // Cancellation requested during node execution
736
+ let error = ExecutionError::kernel_internal("Cancelled during step execution")
737
+ .with_step_id(step_id.clone());
738
+ self.fail_step(step_id, error)?;
739
+ self.cancel_execution("Cancelled during step execution")?;
740
+ anyhow::bail!("Execution cancelled during step");
741
+ }
742
+ result = node_future => result,
743
+ };
744
+
745
+ let duration_ms = step_start.elapsed().as_millis() as u64;
746
+
747
+ match result {
748
+ Ok(new_state) => {
749
+ state = new_state;
750
+ self.complete_step(
751
+ step_id,
752
+ Some(state.as_str().unwrap_or_default().to_string()),
753
+ duration_ms,
754
+ )?;
755
+ }
756
+ Err(e) => {
757
+ let error = ExecutionError::kernel_internal(e.to_string())
758
+ .with_step_id(step_id.clone());
759
+ self.fail_step(step_id.clone(), error.clone())?;
760
+ self.fail(error)?;
761
+ return Err(e);
762
+ }
763
+ }
764
+
765
+ // INV-INBOX-001: Check inbox after every step
766
+ if let Some(action) = self.check_inbox()? {
767
+ match action {
768
+ InboxAction::Pause => {
769
+ // Pause has already been applied via dispatch in check_inbox()
770
+ // In a full implementation, we would suspend here and wait for resume
771
+ // For now, we log and continue - the state machine is already in Paused
772
+ tracing::info!(
773
+ execution_id = %self.execution.id,
774
+ "Execution paused via inbox, continuing in paused state"
775
+ );
776
+ }
777
+ InboxAction::Cancel(reason) => {
778
+ self.cancel_execution(&reason)?;
779
+ anyhow::bail!("Execution cancelled via inbox: {}", reason);
780
+ }
781
+ InboxAction::Continue => {
782
+ // Continue execution normally
783
+ }
784
+ }
785
+ }
786
+
787
+ // Get next nodes
788
+ let output = state.as_str().unwrap_or_default();
789
+ let next = graph.get_next(&current_node, output);
790
+
791
+ if next.is_empty() {
792
+ break;
793
+ }
794
+
795
+ match &next[0] {
796
+ crate::graph::EdgeTarget::End => break,
797
+ crate::graph::EdgeTarget::Node(n) => {
798
+ current_node = n.clone();
799
+ }
800
+ }
801
+ }
802
+
803
+ // Complete execution
804
+ self.complete(Some(state.as_str().unwrap_or_default().to_string()))?;
805
+
806
+ Ok(state)
807
+ }
808
+
809
+ /// Check inbox for messages and process them
810
+ ///
811
+ /// ## Invariants
812
+ /// - INV-INBOX-001: Called after every step
813
+ /// - INV-INBOX-002: Control messages processed first (via drain_messages sorting)
814
+ /// - INV-INBOX-003: All messages emit events for audit trail
815
+ /// - INV-SPAWN-002: Inbox inheritance based on SpawnMode
816
+ ///
817
+ /// ## SpawnMode Routing (@see docs/TECHNICAL/32-SPAWN-MODE.md)
818
+ ///
819
+ /// - Inline mode (default): Check current execution's inbox
820
+ /// - Child { inherit_inbox: true }: Check both parent and own inbox
821
+ /// - Child { inherit_inbox: false }: Check only own inbox (isolated)
822
+ fn check_inbox(&mut self) -> Result<Option<InboxAction>, ReducerError> {
823
+ let inbox = match &self.inbox {
824
+ Some(inbox) => inbox.clone(),
825
+ None => return Ok(None),
826
+ };
827
+
828
+ // Determine which execution IDs to check based on SpawnMode
829
+ let execution_ids_to_check = self.get_inbox_execution_ids();
830
+
831
+ // Fast path: check if any inbox has messages
832
+ let has_messages = execution_ids_to_check.iter().any(|id| {
833
+ inbox.has_control_messages(id) || !inbox.is_empty(id)
834
+ });
835
+
836
+ if !has_messages {
837
+ return Ok(None);
838
+ }
839
+
840
+ // Drain messages from all applicable inboxes, sorted by priority (INV-INBOX-002)
841
+ let messages: Vec<InboxMessage> = execution_ids_to_check
842
+ .iter()
843
+ .flat_map(|id| inbox.drain_messages(id))
844
+ .collect();
845
+
846
+ let mut action = InboxAction::Continue;
847
+
848
+ for message in messages {
849
+ // INV-INBOX-003: Emit event for audit trail
850
+ self.emit_inbox_event(&message);
851
+
852
+ match message {
853
+ InboxMessage::Control(ctrl) => {
854
+ match ctrl.action {
855
+ ControlAction::Pause => {
856
+ tracing::info!(
857
+ execution_id = %self.execution.id,
858
+ actor = %ctrl.actor,
859
+ reason = ?ctrl.reason,
860
+ "Inbox: Pause requested"
861
+ );
862
+ self.pause(ctrl.reason.unwrap_or_else(|| "Paused via inbox".to_string()))?;
863
+ action = InboxAction::Pause;
864
+ }
865
+ ControlAction::Resume => {
866
+ tracing::info!(
867
+ execution_id = %self.execution.id,
868
+ actor = %ctrl.actor,
869
+ "Inbox: Resume requested"
870
+ );
871
+ self.resume()?;
872
+ action = InboxAction::Continue;
873
+ }
874
+ ControlAction::Cancel => {
875
+ let reason = ctrl.reason.unwrap_or_else(|| "Cancelled via inbox".to_string());
876
+ tracing::info!(
877
+ execution_id = %self.execution.id,
878
+ actor = %ctrl.actor,
879
+ reason = %reason,
880
+ "Inbox: Cancel requested"
881
+ );
882
+ return Ok(Some(InboxAction::Cancel(reason)));
883
+ }
884
+ ControlAction::Checkpoint => {
885
+ tracing::info!(
886
+ execution_id = %self.execution.id,
887
+ "Inbox: Checkpoint requested"
888
+ );
889
+ // Checkpoint is handled by the runner, not the kernel
890
+ }
891
+ ControlAction::Compact => {
892
+ tracing::info!(
893
+ execution_id = %self.execution.id,
894
+ "Inbox: Compact requested"
895
+ );
896
+ // Compact is handled by the runner, not the kernel
897
+ }
898
+ }
899
+ }
900
+ InboxMessage::Guidance(guidance) => {
901
+ tracing::info!(
902
+ execution_id = %self.execution.id,
903
+ from = ?guidance.from,
904
+ priority = ?guidance.priority,
905
+ content = %guidance.content,
906
+ "Inbox: Guidance received"
907
+ );
908
+ // Guidance is logged but not acted upon in the kernel
909
+ // The agent/LLM layer processes guidance
910
+ }
911
+ InboxMessage::Evidence(evidence) => {
912
+ tracing::info!(
913
+ execution_id = %self.execution.id,
914
+ source = ?evidence.source,
915
+ impact = ?evidence.impact,
916
+ title = %evidence.title,
917
+ "Inbox: Evidence received"
918
+ );
919
+ // Evidence is logged but not acted upon in the kernel
920
+ // The agent/LLM layer processes evidence
921
+ }
922
+ InboxMessage::A2a(a2a) => {
923
+ tracing::debug!(
924
+ execution_id = %self.execution.id,
925
+ from_agent = %a2a.from_agent,
926
+ message_type = %a2a.message_type,
927
+ "Inbox: A2A message received"
928
+ );
929
+ // A2A messages are logged but not acted upon in the kernel
930
+ // The agent/LLM layer processes A2A messages
931
+ }
932
+ }
933
+ }
934
+
935
+ Ok(Some(action))
936
+ }
937
+
938
+ /// Emit an event for an inbox message (INV-INBOX-003)
939
+ fn emit_inbox_event(&self, message: &InboxMessage) {
940
+ let event = StreamEvent::inbox_message(
941
+ &self.execution.id,
942
+ message.id(),
943
+ message.message_type(),
944
+ );
945
+ self.emitter.emit(event);
946
+ }
947
+
948
+ /// Determine which execution IDs to check for inbox messages based on SpawnMode
949
+ ///
950
+ /// ## SpawnMode Routing Rules (INV-SPAWN-002)
951
+ ///
952
+ /// - Inline mode: Check current execution's inbox (same as default)
953
+ /// - Child { inherit_inbox: true }: Check both parent and own inbox
954
+ /// - Child { inherit_inbox: false }: Check only own inbox (isolated)
955
+ /// - No spawn_mode: Default to current execution only
956
+ ///
957
+ /// @see docs/TECHNICAL/32-SPAWN-MODE.md
958
+ #[cfg_attr(test, allow(dead_code))]
959
+ pub(crate) fn get_inbox_execution_ids(&self) -> Vec<ExecutionId> {
960
+ match &self.spawn_mode {
961
+ // Inline mode: use current execution only (inline shares parent's ExecutionId anyway)
962
+ Some(SpawnMode::Inline) => {
963
+ vec![self.execution.id.clone()]
964
+ }
965
+ // Child mode with inherit_inbox: check both parent and own inbox
966
+ Some(SpawnMode::Child { inherit_inbox: true, .. }) => {
967
+ let mut ids = vec![self.execution.id.clone()];
968
+ if let Some(parent_id) = &self.parent_execution_id {
969
+ ids.push(parent_id.clone());
970
+ tracing::debug!(
971
+ execution_id = %self.execution.id,
972
+ parent_id = %parent_id,
973
+ "Checking both parent and own inbox (inherit_inbox=true)"
974
+ );
975
+ }
976
+ ids
977
+ }
978
+ // Child mode without inherit_inbox: isolated inbox
979
+ Some(SpawnMode::Child { inherit_inbox: false, .. }) => {
980
+ tracing::debug!(
981
+ execution_id = %self.execution.id,
982
+ "Using isolated inbox (inherit_inbox=false)"
983
+ );
984
+ vec![self.execution.id.clone()]
985
+ }
986
+ // No spawn mode set: default to current execution
987
+ None => {
988
+ vec![self.execution.id.clone()]
989
+ }
990
+ }
991
+ }
992
+
993
+ /// Emit a stream event corresponding to an action
994
+ fn emit_event_for_action(&self, action: &ExecutionAction) {
995
+ let event = match action {
996
+ ExecutionAction::Start => {
997
+ StreamEvent::execution_start(&self.execution.id)
998
+ }
999
+ ExecutionAction::StepStarted {
1000
+ step_id,
1001
+ step_type,
1002
+ name,
1003
+ ..
1004
+ } => StreamEvent::step_start(&self.execution.id, step_id, step_type.clone(), name.clone()),
1005
+ ExecutionAction::StepCompleted {
1006
+ step_id,
1007
+ output,
1008
+ duration_ms,
1009
+ } => StreamEvent::step_end(&self.execution.id, step_id, output.clone(), *duration_ms),
1010
+ ExecutionAction::StepFailed { step_id, error } => {
1011
+ StreamEvent::step_failed(&self.execution.id, step_id, error.clone())
1012
+ }
1013
+ ExecutionAction::Pause { reason } => {
1014
+ StreamEvent::execution_paused(&self.execution.id, reason.clone())
1015
+ }
1016
+ ExecutionAction::Resume => StreamEvent::execution_resumed(&self.execution.id),
1017
+ ExecutionAction::Complete { output } => {
1018
+ let duration = self.execution.duration_ms().unwrap_or(0);
1019
+ StreamEvent::execution_end(&self.execution.id, output.clone(), duration)
1020
+ }
1021
+ ExecutionAction::Fail { error } => {
1022
+ StreamEvent::execution_failed(&self.execution.id, error.clone())
1023
+ }
1024
+ ExecutionAction::Cancel { reason } => {
1025
+ StreamEvent::execution_cancelled(&self.execution.id, reason.clone())
1026
+ }
1027
+ ExecutionAction::Wait { .. } | ExecutionAction::InputReceived => {
1028
+ // No specific stream events for these yet
1029
+ return;
1030
+ }
1031
+ };
1032
+
1033
+ self.emitter.emit(event);
1034
+ }
1035
+
1036
+ // =========================================================================
1037
+ // Protected Event Emission (P2 #2: ProtectedEventEmitter Integration)
1038
+ // =========================================================================
1039
+
1040
+ /// Emit an event with protection processing
1041
+ ///
1042
+ /// If a protected emitter is configured, the event passes through the
1043
+ /// protection pipeline before being emitted. Otherwise, falls back to
1044
+ /// the regular emitter.
1045
+ ///
1046
+ /// Use this for events that may contain sensitive content (step outputs,
1047
+ /// tool results, etc.).
1048
+ pub async fn emit_protected(&self, event: StreamEvent) -> anyhow::Result<()> {
1049
+ if let Some(protected) = &self.protected_emitter {
1050
+ protected.emit(event).await?;
1051
+ } else {
1052
+ self.emitter.emit(event);
1053
+ }
1054
+ Ok(())
1055
+ }
1056
+
1057
+ /// Emit an event without protection (control events, etc.)
1058
+ ///
1059
+ /// Use for events that are guaranteed safe (control signals, execution
1060
+ /// lifecycle events without content).
1061
+ pub fn emit_unprotected(&self, event: StreamEvent) {
1062
+ if let Some(protected) = &self.protected_emitter {
1063
+ protected.emit_unprotected(event);
1064
+ } else {
1065
+ self.emitter.emit(event);
1066
+ }
1067
+ }
1068
+
1069
+ /// Check if protected emitter is configured
1070
+ pub fn has_protected_emitter(&self) -> bool {
1071
+ self.protected_emitter.is_some()
1072
+ }
1073
+
1074
+ /// Get the protected emitter (if configured)
1075
+ pub fn protected_emitter(&self) -> Option<&ProtectedEventEmitter> {
1076
+ self.protected_emitter.as_ref()
1077
+ }
1078
+ }
1079
+
1080
+ // Note: ExecutionKernel does NOT implement Default because TenantContext is REQUIRED.
1081
+ // This is intentional - every execution must have a tenant for multi-tenant isolation.
1082
+
1083
+ #[cfg(test)]
1084
+ mod tests {
1085
+ use super::*;
1086
+ use crate::context::ResourceLimits;
1087
+ use crate::TenantId;
1088
+
1089
+ #[tokio::test]
1090
+ async fn emits_warning_event_when_limits_near_threshold() {
1091
+ let limits = ResourceLimits {
1092
+ max_steps: 5,
1093
+ ..Default::default()
1094
+ };
1095
+ let tenant = TenantContext::new(TenantId::new()).with_limits(limits);
1096
+
1097
+ let mut kernel = ExecutionKernel::new(tenant);
1098
+ kernel.register_for_enforcement().await;
1099
+
1100
+ // Record progress to reach warning threshold (80% at next step)
1101
+ kernel.record_step_completed().await;
1102
+ kernel.record_step_completed().await;
1103
+ kernel.record_step_completed().await;
1104
+
1105
+ kernel.check_limits_before_step().await.unwrap();
1106
+
1107
+ let events = kernel.emitter.drain();
1108
+ assert!(
1109
+ events.iter().any(|e| {
1110
+ matches!(
1111
+ e,
1112
+ StreamEvent::PolicyDecision {
1113
+ decision,
1114
+ tool_name,
1115
+ ..
1116
+ } if decision == "warn" && tool_name == "enforcement"
1117
+ )
1118
+ }),
1119
+ "expected enforcement warning event"
1120
+ );
1121
+ }
1122
+
1123
+ // =========================================================================
1124
+ // SpawnMode Builder Tests
1125
+ // =========================================================================
1126
+
1127
+ #[test]
1128
+ fn test_kernel_with_spawn_mode_inline() {
1129
+ let tenant = TenantContext::new(TenantId::new());
1130
+ let kernel = ExecutionKernel::new(tenant)
1131
+ .with_spawn_mode(SpawnMode::Inline);
1132
+
1133
+ assert!(kernel.spawn_mode().is_some());
1134
+ assert_eq!(*kernel.spawn_mode().unwrap(), SpawnMode::Inline);
1135
+ }
1136
+
1137
+ #[test]
1138
+ fn test_kernel_with_spawn_mode_child() {
1139
+ let tenant = TenantContext::new(TenantId::new());
1140
+ let kernel = ExecutionKernel::new(tenant)
1141
+ .with_spawn_mode(SpawnMode::Child {
1142
+ background: true,
1143
+ inherit_inbox: true,
1144
+ policies: None,
1145
+ });
1146
+
1147
+ assert!(kernel.spawn_mode().is_some());
1148
+ if let Some(SpawnMode::Child { background, inherit_inbox, .. }) = kernel.spawn_mode() {
1149
+ assert!(*background);
1150
+ assert!(*inherit_inbox);
1151
+ } else {
1152
+ panic!("Expected SpawnMode::Child");
1153
+ }
1154
+ }
1155
+
1156
+ #[test]
1157
+ fn test_kernel_with_parent_execution_id() {
1158
+ let tenant = TenantContext::new(TenantId::new());
1159
+ let parent_id = ExecutionId::from_string("exec_parent_123");
1160
+ let kernel = ExecutionKernel::new(tenant)
1161
+ .with_parent_execution_id(parent_id.clone());
1162
+
1163
+ assert!(kernel.parent_execution_id().is_some());
1164
+ assert_eq!(*kernel.parent_execution_id().unwrap(), parent_id);
1165
+ }
1166
+
1167
+ #[test]
1168
+ fn test_kernel_default_no_spawn_mode() {
1169
+ let tenant = TenantContext::new(TenantId::new());
1170
+ let kernel = ExecutionKernel::new(tenant);
1171
+
1172
+ assert!(kernel.spawn_mode().is_none());
1173
+ assert!(kernel.parent_execution_id().is_none());
1174
+ }
1175
+
1176
+ // =========================================================================
1177
+ // Inbox Routing by SpawnMode Tests
1178
+ // =========================================================================
1179
+
1180
+ #[test]
1181
+ fn test_get_inbox_execution_ids_no_spawn_mode() {
1182
+ let tenant = TenantContext::new(TenantId::new());
1183
+ let kernel = ExecutionKernel::new(tenant);
1184
+
1185
+ let ids = kernel.get_inbox_execution_ids();
1186
+ assert_eq!(ids.len(), 1, "Should return only current execution ID");
1187
+ assert_eq!(ids[0], *kernel.execution_id());
1188
+ }
1189
+
1190
+ #[test]
1191
+ fn test_get_inbox_execution_ids_inline_mode() {
1192
+ let tenant = TenantContext::new(TenantId::new());
1193
+ let kernel = ExecutionKernel::new(tenant)
1194
+ .with_spawn_mode(SpawnMode::Inline);
1195
+
1196
+ let ids = kernel.get_inbox_execution_ids();
1197
+ assert_eq!(ids.len(), 1, "Inline mode should check current execution only");
1198
+ assert_eq!(ids[0], *kernel.execution_id());
1199
+ }
1200
+
1201
+ #[test]
1202
+ fn test_get_inbox_execution_ids_child_isolated() {
1203
+ let tenant = TenantContext::new(TenantId::new());
1204
+ let parent_id = ExecutionId::from_string("exec_parent");
1205
+ let kernel = ExecutionKernel::new(tenant)
1206
+ .with_spawn_mode(SpawnMode::Child {
1207
+ background: false,
1208
+ inherit_inbox: false,
1209
+ policies: None,
1210
+ })
1211
+ .with_parent_execution_id(parent_id);
1212
+
1213
+ let ids = kernel.get_inbox_execution_ids();
1214
+ assert_eq!(ids.len(), 1, "Child with inherit_inbox=false should be isolated");
1215
+ assert_eq!(ids[0], *kernel.execution_id(), "Should only check own inbox");
1216
+ }
1217
+
1218
+ #[test]
1219
+ fn test_get_inbox_execution_ids_child_inherit() {
1220
+ let tenant = TenantContext::new(TenantId::new());
1221
+ let parent_id = ExecutionId::from_string("exec_parent_inherit");
1222
+ let kernel = ExecutionKernel::new(tenant)
1223
+ .with_spawn_mode(SpawnMode::Child {
1224
+ background: false,
1225
+ inherit_inbox: true,
1226
+ policies: None,
1227
+ })
1228
+ .with_parent_execution_id(parent_id.clone());
1229
+
1230
+ let ids = kernel.get_inbox_execution_ids();
1231
+ assert_eq!(ids.len(), 2, "Child with inherit_inbox=true should check both inboxes");
1232
+ assert!(ids.contains(kernel.execution_id()), "Should include own execution ID");
1233
+ assert!(ids.contains(&parent_id), "Should include parent execution ID");
1234
+ }
1235
+
1236
+ #[test]
1237
+ fn test_get_inbox_execution_ids_child_inherit_no_parent_id() {
1238
+ let tenant = TenantContext::new(TenantId::new());
1239
+ // Child with inherit_inbox=true but no parent_execution_id set
1240
+ let kernel = ExecutionKernel::new(tenant)
1241
+ .with_spawn_mode(SpawnMode::Child {
1242
+ background: false,
1243
+ inherit_inbox: true,
1244
+ policies: None,
1245
+ });
1246
+
1247
+ let ids = kernel.get_inbox_execution_ids();
1248
+ // Should gracefully handle missing parent_execution_id
1249
+ assert_eq!(ids.len(), 1, "Without parent_execution_id, should only return own ID");
1250
+ assert_eq!(ids[0], *kernel.execution_id());
1251
+ }
1252
+
1253
+ #[test]
1254
+ fn test_get_inbox_execution_ids_child_background_isolated() {
1255
+ let tenant = TenantContext::new(TenantId::new());
1256
+ let kernel = ExecutionKernel::new(tenant)
1257
+ .with_spawn_mode(SpawnMode::Child {
1258
+ background: true, // Background child
1259
+ inherit_inbox: false, // Isolated
1260
+ policies: None,
1261
+ });
1262
+
1263
+ let ids = kernel.get_inbox_execution_ids();
1264
+ assert_eq!(ids.len(), 1, "Background child with isolated inbox");
1265
+ }
1266
+
1267
+ #[test]
1268
+ fn test_get_inbox_execution_ids_child_background_inherit() {
1269
+ let tenant = TenantContext::new(TenantId::new());
1270
+ let parent_id = ExecutionId::from_string("exec_background_parent");
1271
+ let kernel = ExecutionKernel::new(tenant)
1272
+ .with_spawn_mode(SpawnMode::Child {
1273
+ background: true, // Background child
1274
+ inherit_inbox: true, // Inherits parent inbox
1275
+ policies: None,
1276
+ })
1277
+ .with_parent_execution_id(parent_id.clone());
1278
+
1279
+ let ids = kernel.get_inbox_execution_ids();
1280
+ assert_eq!(ids.len(), 2, "Background child can still inherit inbox");
1281
+ assert!(ids.contains(&parent_id));
1282
+ }
1283
+ }