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,1200 @@
1
+ //! Execution Error Taxonomy - Deterministic Failure Semantics
2
+ //!
3
+ //! This module defines the formal `ExecutionError` model that allows the kernel
4
+ //! to make deterministic decisions about retries, billing, and failure reporting.
5
+ //!
6
+ //! ## Design Principles
7
+ //!
8
+ //! 1. **No Generic Errors**: Every error MUST be categorized
9
+ //! 2. **Retry Policies Are Explicit**: Each error declares its retry behavior
10
+ //! 3. **Idempotency Is Tracked**: Side-effect operations declare idempotency requirements
11
+ //! 4. **Backoff Is Deterministic**: Retry timing is calculated, not random
12
+ //!
13
+ //! ## Error Categories
14
+ //!
15
+ //! - `LlmError`: LLM provider errors (usually retryable with backoff)
16
+ //! - `ToolError`: Tool execution errors (may be retryable)
17
+ //! - `PolicyViolation`: Policy/guardrail violation (NEVER retryable)
18
+ //! - `Timeout`: Execution timeout (retryable with extended timeout)
19
+ //! - `QuotaExceeded`: Resource quota exceeded (NEVER retryable)
20
+ //! - `KernelInternal`: Internal kernel error (NEVER retryable)
21
+ //! - `ValidationError`: Input validation error (NEVER retryable)
22
+ //! - `NetworkError`: Network/connectivity error (usually retryable)
23
+ //!
24
+ //! @see docs/feat-02-error-taxonomy.md
25
+ //! @see packages/enact-schemas/src/execution.schemas.ts
26
+
27
+ use super::ids::StepId;
28
+ use serde::{Deserialize, Serialize};
29
+ use std::time::Duration;
30
+
31
+ // =============================================================================
32
+ // Error Categories
33
+ // =============================================================================
34
+
35
+ /// High-level error categories for deterministic recovery
36
+ ///
37
+ /// Categories determine retry behavior and are the primary classification
38
+ /// for all execution failures.
39
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
40
+ #[serde(rename_all = "PascalCase")]
41
+ pub enum ExecutionErrorCategory {
42
+ /// LLM provider error (rate limit, content filter, context overflow)
43
+ LlmError,
44
+ /// Tool execution error (tool crashed, invalid output)
45
+ ToolError,
46
+ /// Policy/guardrail violation (FATAL - never retry)
47
+ PolicyViolation,
48
+ /// Execution timeout (step or wall clock)
49
+ Timeout,
50
+ /// Resource quota exceeded (FATAL - never retry)
51
+ QuotaExceeded,
52
+ /// Internal kernel error (FATAL - never retry)
53
+ KernelInternal,
54
+ /// Input validation error (FATAL - never retry)
55
+ ValidationError,
56
+ /// Network/connectivity error (usually retryable)
57
+ NetworkError,
58
+ }
59
+
60
+ impl ExecutionErrorCategory {
61
+ /// Check if this category is fatal (never retryable)
62
+ pub fn is_fatal(&self) -> bool {
63
+ matches!(
64
+ self,
65
+ Self::PolicyViolation | Self::QuotaExceeded | Self::KernelInternal | Self::ValidationError
66
+ )
67
+ }
68
+
69
+ /// Get the default retry policy for this category
70
+ pub fn default_retry_policy(&self) -> RetryPolicy {
71
+ match self {
72
+ Self::LlmError => RetryPolicy {
73
+ retryable: true,
74
+ max_retries: 3,
75
+ backoff_strategy: BackoffStrategy::Exponential,
76
+ base_delay: Duration::from_millis(1000),
77
+ max_delay: Duration::from_millis(30000),
78
+ requires_idempotency_key: false,
79
+ },
80
+ Self::ToolError => RetryPolicy {
81
+ retryable: true,
82
+ max_retries: 2,
83
+ backoff_strategy: BackoffStrategy::Constant,
84
+ base_delay: Duration::from_millis(500),
85
+ max_delay: Duration::from_millis(5000),
86
+ requires_idempotency_key: true, // Tools may have side effects
87
+ },
88
+ Self::PolicyViolation => RetryPolicy::fatal(),
89
+ Self::Timeout => RetryPolicy {
90
+ retryable: true,
91
+ max_retries: 1,
92
+ backoff_strategy: BackoffStrategy::Constant,
93
+ base_delay: Duration::ZERO,
94
+ max_delay: Duration::ZERO,
95
+ requires_idempotency_key: true,
96
+ },
97
+ Self::QuotaExceeded => RetryPolicy::fatal(),
98
+ Self::KernelInternal => RetryPolicy::fatal(),
99
+ Self::ValidationError => RetryPolicy::fatal(),
100
+ Self::NetworkError => RetryPolicy {
101
+ retryable: true,
102
+ max_retries: 3,
103
+ backoff_strategy: BackoffStrategy::Exponential,
104
+ base_delay: Duration::from_millis(500),
105
+ max_delay: Duration::from_millis(15000),
106
+ requires_idempotency_key: true,
107
+ },
108
+ }
109
+ }
110
+ }
111
+
112
+ impl std::fmt::Display for ExecutionErrorCategory {
113
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114
+ match self {
115
+ Self::LlmError => write!(f, "LlmError"),
116
+ Self::ToolError => write!(f, "ToolError"),
117
+ Self::PolicyViolation => write!(f, "PolicyViolation"),
118
+ Self::Timeout => write!(f, "Timeout"),
119
+ Self::QuotaExceeded => write!(f, "QuotaExceeded"),
120
+ Self::KernelInternal => write!(f, "KernelInternal"),
121
+ Self::ValidationError => write!(f, "ValidationError"),
122
+ Self::NetworkError => write!(f, "NetworkError"),
123
+ }
124
+ }
125
+ }
126
+
127
+ // =============================================================================
128
+ // Backoff Strategy
129
+ // =============================================================================
130
+
131
+ /// How to space out retry attempts
132
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
133
+ #[serde(rename_all = "lowercase")]
134
+ pub enum BackoffStrategy {
135
+ /// No delay between retries
136
+ #[default]
137
+ None,
138
+ /// Fixed delay between retries
139
+ Constant,
140
+ /// Linearly increasing delay (base * attempt)
141
+ Linear,
142
+ /// Exponentially increasing delay (base * 2^(attempt-1))
143
+ Exponential,
144
+ }
145
+
146
+ impl BackoffStrategy {
147
+ /// Calculate delay for a given attempt number (1-indexed)
148
+ pub fn calculate_delay(&self, base: Duration, attempt: u32, max: Duration) -> Duration {
149
+ let delay = match self {
150
+ Self::None => Duration::ZERO,
151
+ Self::Constant => base,
152
+ Self::Linear => base * attempt,
153
+ Self::Exponential => {
154
+ let multiplier = 2u64.saturating_pow(attempt.saturating_sub(1));
155
+ base.saturating_mul(multiplier as u32)
156
+ }
157
+ };
158
+ std::cmp::min(delay, max)
159
+ }
160
+ }
161
+
162
+ // =============================================================================
163
+ // Retry Policy
164
+ // =============================================================================
165
+
166
+ /// Deterministic retry behavior for an error
167
+ ///
168
+ /// The kernel uses this to decide:
169
+ /// 1. Should we retry? (retryable)
170
+ /// 2. How many times? (max_retries)
171
+ /// 3. How long to wait? (backoff_strategy + base_delay)
172
+ /// 4. Is the operation idempotent? (requires_idempotency_key)
173
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174
+ pub struct RetryPolicy {
175
+ /// Whether this error is retryable at all
176
+ pub retryable: bool,
177
+
178
+ /// Maximum number of retry attempts (0 = no retries)
179
+ pub max_retries: u32,
180
+
181
+ /// Backoff strategy for spacing retries
182
+ pub backoff_strategy: BackoffStrategy,
183
+
184
+ /// Base delay for backoff calculation
185
+ #[serde(with = "duration_millis")]
186
+ pub base_delay: Duration,
187
+
188
+ /// Maximum delay (caps exponential backoff)
189
+ #[serde(with = "duration_millis")]
190
+ pub max_delay: Duration,
191
+
192
+ /// Whether the operation requires an idempotency key for safe retry
193
+ /// If true, the kernel MUST generate an IdempotencyKey before retrying
194
+ pub requires_idempotency_key: bool,
195
+ }
196
+
197
+ impl RetryPolicy {
198
+ /// Create a fatal (non-retryable) policy
199
+ pub fn fatal() -> Self {
200
+ Self {
201
+ retryable: false,
202
+ max_retries: 0,
203
+ backoff_strategy: BackoffStrategy::None,
204
+ base_delay: Duration::ZERO,
205
+ max_delay: Duration::ZERO,
206
+ requires_idempotency_key: false,
207
+ }
208
+ }
209
+
210
+ /// Create a simple retryable policy
211
+ pub fn retryable(max_retries: u32) -> Self {
212
+ Self {
213
+ retryable: true,
214
+ max_retries,
215
+ backoff_strategy: BackoffStrategy::Exponential,
216
+ base_delay: Duration::from_millis(1000),
217
+ max_delay: Duration::from_millis(30000),
218
+ requires_idempotency_key: false,
219
+ }
220
+ }
221
+
222
+ /// Calculate delay for a given attempt number
223
+ pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
224
+ self.backoff_strategy
225
+ .calculate_delay(self.base_delay, attempt, self.max_delay)
226
+ }
227
+
228
+ /// Check if a retry should be attempted for the given attempt number
229
+ pub fn should_retry(&self, attempt: u32) -> bool {
230
+ self.retryable && attempt <= self.max_retries
231
+ }
232
+ }
233
+
234
+ impl Default for RetryPolicy {
235
+ fn default() -> Self {
236
+ Self::fatal()
237
+ }
238
+ }
239
+
240
+ // =============================================================================
241
+ // LLM Error Codes
242
+ // =============================================================================
243
+
244
+ /// Specific LLM provider error codes
245
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246
+ #[serde(rename_all = "snake_case")]
247
+ pub enum LlmErrorCode {
248
+ /// 429 - Too many requests
249
+ RateLimit,
250
+ /// Context window exceeded
251
+ ContextOverflow,
252
+ /// Content blocked by safety filter
253
+ ContentFiltered,
254
+ /// Malformed request
255
+ InvalidRequest,
256
+ /// Authentication/authorization failed
257
+ AuthFailed,
258
+ /// Model not available
259
+ ModelUnavailable,
260
+ /// Generic provider error
261
+ ProviderError,
262
+ }
263
+
264
+ impl LlmErrorCode {
265
+ /// Check if this error code is typically retryable
266
+ pub fn is_retryable(&self) -> bool {
267
+ matches!(
268
+ self,
269
+ Self::RateLimit | Self::ModelUnavailable | Self::ProviderError
270
+ )
271
+ }
272
+ }
273
+
274
+ impl std::fmt::Display for LlmErrorCode {
275
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276
+ match self {
277
+ Self::RateLimit => write!(f, "rate_limit"),
278
+ Self::ContextOverflow => write!(f, "context_overflow"),
279
+ Self::ContentFiltered => write!(f, "content_filtered"),
280
+ Self::InvalidRequest => write!(f, "invalid_request"),
281
+ Self::AuthFailed => write!(f, "auth_failed"),
282
+ Self::ModelUnavailable => write!(f, "model_unavailable"),
283
+ Self::ProviderError => write!(f, "provider_error"),
284
+ }
285
+ }
286
+ }
287
+
288
+ // =============================================================================
289
+ // Tool Error Codes
290
+ // =============================================================================
291
+
292
+ /// Specific tool error codes
293
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
294
+ #[serde(rename_all = "snake_case")]
295
+ pub enum ToolErrorCode {
296
+ /// Tool not registered
297
+ NotFound,
298
+ /// Tool not allowed by policy
299
+ PermissionDenied,
300
+ /// Invalid tool arguments
301
+ InvalidInput,
302
+ /// Tool crashed or returned error
303
+ ExecutionFailed,
304
+ /// Tool execution timed out
305
+ Timeout,
306
+ /// Tool returned invalid output
307
+ OutputInvalid,
308
+ }
309
+
310
+ impl ToolErrorCode {
311
+ /// Check if this error code is typically retryable
312
+ pub fn is_retryable(&self) -> bool {
313
+ matches!(self, Self::Timeout | Self::ExecutionFailed)
314
+ }
315
+ }
316
+
317
+ impl std::fmt::Display for ToolErrorCode {
318
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319
+ match self {
320
+ Self::NotFound => write!(f, "not_found"),
321
+ Self::PermissionDenied => write!(f, "permission_denied"),
322
+ Self::InvalidInput => write!(f, "invalid_input"),
323
+ Self::ExecutionFailed => write!(f, "execution_failed"),
324
+ Self::Timeout => write!(f, "timeout"),
325
+ Self::OutputInvalid => write!(f, "output_invalid"),
326
+ }
327
+ }
328
+ }
329
+
330
+ // =============================================================================
331
+ // Execution Error
332
+ // =============================================================================
333
+
334
+ /// The primary error type for all execution failures
335
+ ///
336
+ /// This structured error enables:
337
+ /// 1. Deterministic retry decisions
338
+ /// 2. Accurate billing (distinguishes user errors from system errors)
339
+ /// 3. Compliance narratives (full audit trail)
340
+ /// 4. HTTP/gRPC status code mapping
341
+ #[derive(Debug, Clone, Serialize, Deserialize)]
342
+ pub struct ExecutionError {
343
+ /// High-level error category
344
+ pub category: ExecutionErrorCategory,
345
+
346
+ /// Human-readable error message
347
+ pub message: String,
348
+
349
+ /// Retry policy for this error
350
+ pub retry_policy: RetryPolicy,
351
+
352
+ /// Specific error code within the category
353
+ pub code: Option<String>,
354
+
355
+ /// Attempt number (1-indexed, for tracking retries)
356
+ pub attempt: u32,
357
+
358
+ /// Step ID where the error occurred
359
+ pub step_id: Option<StepId>,
360
+
361
+ /// Provider name (for LLM/Tool errors)
362
+ pub provider: Option<String>,
363
+
364
+ /// HTTP status code (if applicable)
365
+ pub http_status: Option<u16>,
366
+
367
+ /// Additional structured details
368
+ pub details: Option<serde_json::Value>,
369
+
370
+ /// Timestamp when error occurred (milliseconds since epoch)
371
+ pub occurred_at: i64,
372
+ }
373
+
374
+ impl ExecutionError {
375
+ /// Create a new ExecutionError with default retry policy for the category
376
+ pub fn new(category: ExecutionErrorCategory, message: impl Into<String>) -> Self {
377
+ Self {
378
+ category,
379
+ message: message.into(),
380
+ retry_policy: category.default_retry_policy(),
381
+ code: None,
382
+ attempt: 1,
383
+ step_id: None,
384
+ provider: None,
385
+ http_status: None,
386
+ details: None,
387
+ occurred_at: chrono::Utc::now().timestamp_millis(),
388
+ }
389
+ }
390
+
391
+ // Builder methods
392
+
393
+ /// Set the error code
394
+ pub fn with_code(mut self, code: impl Into<String>) -> Self {
395
+ self.code = Some(code.into());
396
+ self
397
+ }
398
+
399
+ /// Set the attempt number
400
+ pub fn with_attempt(mut self, attempt: u32) -> Self {
401
+ self.attempt = attempt;
402
+ self
403
+ }
404
+
405
+ /// Set the step ID
406
+ pub fn with_step_id(mut self, step_id: StepId) -> Self {
407
+ self.step_id = Some(step_id);
408
+ self
409
+ }
410
+
411
+ /// Set the provider name
412
+ pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
413
+ self.provider = Some(provider.into());
414
+ self
415
+ }
416
+
417
+ /// Set the HTTP status code
418
+ pub fn with_http_status(mut self, status: u16) -> Self {
419
+ self.http_status = Some(status);
420
+ self
421
+ }
422
+
423
+ /// Set additional details
424
+ pub fn with_details(mut self, details: serde_json::Value) -> Self {
425
+ self.details = Some(details);
426
+ self
427
+ }
428
+
429
+ /// Override the retry policy
430
+ pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
431
+ self.retry_policy = policy;
432
+ self
433
+ }
434
+
435
+ // Convenience constructors
436
+
437
+ /// Create an LLM error
438
+ pub fn llm(code: LlmErrorCode, message: impl Into<String>) -> Self {
439
+ Self::new(ExecutionErrorCategory::LlmError, message)
440
+ .with_code(code.to_string())
441
+ }
442
+
443
+ /// Create a tool error
444
+ pub fn tool(code: ToolErrorCode, message: impl Into<String>) -> Self {
445
+ Self::new(ExecutionErrorCategory::ToolError, message)
446
+ .with_code(code.to_string())
447
+ }
448
+
449
+ /// Create a policy violation error
450
+ pub fn policy_violation(message: impl Into<String>) -> Self {
451
+ Self::new(ExecutionErrorCategory::PolicyViolation, message)
452
+ }
453
+
454
+ /// Create a timeout error
455
+ pub fn timeout(message: impl Into<String>) -> Self {
456
+ Self::new(ExecutionErrorCategory::Timeout, message)
457
+ }
458
+
459
+ /// Create a quota exceeded error
460
+ pub fn quota_exceeded(message: impl Into<String>) -> Self {
461
+ Self::new(ExecutionErrorCategory::QuotaExceeded, message)
462
+ }
463
+
464
+ /// Create a kernel internal error
465
+ pub fn kernel_internal(message: impl Into<String>) -> Self {
466
+ Self::new(ExecutionErrorCategory::KernelInternal, message)
467
+ }
468
+
469
+ /// Create a validation error
470
+ pub fn validation(message: impl Into<String>) -> Self {
471
+ Self::new(ExecutionErrorCategory::ValidationError, message)
472
+ }
473
+
474
+ /// Create a network error
475
+ pub fn network(message: impl Into<String>) -> Self {
476
+ Self::new(ExecutionErrorCategory::NetworkError, message)
477
+ }
478
+
479
+ // Query methods
480
+
481
+ /// Check if this error is retryable
482
+ pub fn is_retryable(&self) -> bool {
483
+ self.retry_policy.retryable
484
+ }
485
+
486
+ /// Check if this error is fatal (will never be retried)
487
+ pub fn is_fatal(&self) -> bool {
488
+ self.category.is_fatal()
489
+ }
490
+
491
+ /// Check if a retry should be attempted
492
+ pub fn should_retry(&self) -> bool {
493
+ self.retry_policy.should_retry(self.attempt)
494
+ }
495
+
496
+ /// Get the delay before the next retry attempt
497
+ pub fn retry_delay(&self) -> Duration {
498
+ self.retry_policy.delay_for_attempt(self.attempt)
499
+ }
500
+
501
+ /// Create a new error for the next retry attempt
502
+ pub fn next_attempt(mut self) -> Self {
503
+ self.attempt += 1;
504
+ self.occurred_at = chrono::Utc::now().timestamp_millis();
505
+ self
506
+ }
507
+
508
+ /// Map to HTTP status code
509
+ pub fn to_http_status(&self) -> u16 {
510
+ if let Some(status) = self.http_status {
511
+ return status;
512
+ }
513
+
514
+ match self.category {
515
+ ExecutionErrorCategory::LlmError => 502, // Bad Gateway
516
+ ExecutionErrorCategory::ToolError => 500, // Internal Server Error
517
+ ExecutionErrorCategory::PolicyViolation => 403, // Forbidden
518
+ ExecutionErrorCategory::Timeout => 504, // Gateway Timeout
519
+ ExecutionErrorCategory::QuotaExceeded => 429, // Too Many Requests
520
+ ExecutionErrorCategory::KernelInternal => 500, // Internal Server Error
521
+ ExecutionErrorCategory::ValidationError => 400, // Bad Request
522
+ ExecutionErrorCategory::NetworkError => 503, // Service Unavailable
523
+ }
524
+ }
525
+ }
526
+
527
+ impl std::fmt::Display for ExecutionError {
528
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
529
+ write!(f, "[{}] {}", self.category, self.message)?;
530
+ if let Some(code) = &self.code {
531
+ write!(f, " ({})", code)?;
532
+ }
533
+ if self.attempt > 1 {
534
+ write!(f, " [attempt {}]", self.attempt)?;
535
+ }
536
+ Ok(())
537
+ }
538
+ }
539
+
540
+ impl std::error::Error for ExecutionError {}
541
+
542
+ // Implement From for common error types
543
+
544
+ impl From<reqwest::Error> for ExecutionError {
545
+ fn from(err: reqwest::Error) -> Self {
546
+ if err.is_timeout() {
547
+ Self::timeout(format!("HTTP request timed out: {}", err))
548
+ } else if err.is_connect() {
549
+ Self::network(format!("Connection failed: {}", err))
550
+ } else if err.is_status() {
551
+ let status = err.status().map(|s| s.as_u16()).unwrap_or(500);
552
+ Self::network(format!("HTTP error: {}", err))
553
+ .with_http_status(status)
554
+ } else {
555
+ Self::network(format!("HTTP error: {}", err))
556
+ }
557
+ }
558
+ }
559
+
560
+ impl From<serde_json::Error> for ExecutionError {
561
+ fn from(err: serde_json::Error) -> Self {
562
+ Self::validation(format!("JSON error: {}", err))
563
+ }
564
+ }
565
+
566
+ impl From<std::io::Error> for ExecutionError {
567
+ fn from(err: std::io::Error) -> Self {
568
+ match err.kind() {
569
+ std::io::ErrorKind::TimedOut => Self::timeout(format!("IO timeout: {}", err)),
570
+ std::io::ErrorKind::ConnectionRefused
571
+ | std::io::ErrorKind::ConnectionReset
572
+ | std::io::ErrorKind::ConnectionAborted => {
573
+ Self::network(format!("Connection error: {}", err))
574
+ }
575
+ _ => Self::kernel_internal(format!("IO error: {}", err)),
576
+ }
577
+ }
578
+ }
579
+
580
+ // =============================================================================
581
+ // Serde helpers for Duration
582
+ // =============================================================================
583
+
584
+ mod duration_millis {
585
+ use serde::{Deserialize, Deserializer, Serializer};
586
+ use std::time::Duration;
587
+
588
+ pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
589
+ where
590
+ S: Serializer,
591
+ {
592
+ serializer.serialize_u64(duration.as_millis() as u64)
593
+ }
594
+
595
+ pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
596
+ where
597
+ D: Deserializer<'de>,
598
+ {
599
+ let millis = u64::deserialize(deserializer)?;
600
+ Ok(Duration::from_millis(millis))
601
+ }
602
+ }
603
+
604
+ // =============================================================================
605
+ // Tests
606
+ // =============================================================================
607
+
608
+ #[cfg(test)]
609
+ mod tests {
610
+ use super::*;
611
+
612
+ #[test]
613
+ fn test_error_categories_fatality() {
614
+ assert!(ExecutionErrorCategory::PolicyViolation.is_fatal());
615
+ assert!(ExecutionErrorCategory::QuotaExceeded.is_fatal());
616
+ assert!(ExecutionErrorCategory::KernelInternal.is_fatal());
617
+ assert!(ExecutionErrorCategory::ValidationError.is_fatal());
618
+
619
+ assert!(!ExecutionErrorCategory::LlmError.is_fatal());
620
+ assert!(!ExecutionErrorCategory::ToolError.is_fatal());
621
+ assert!(!ExecutionErrorCategory::Timeout.is_fatal());
622
+ assert!(!ExecutionErrorCategory::NetworkError.is_fatal());
623
+ }
624
+
625
+ #[test]
626
+ fn test_default_retry_policies() {
627
+ let llm_policy = ExecutionErrorCategory::LlmError.default_retry_policy();
628
+ assert!(llm_policy.retryable);
629
+ assert_eq!(llm_policy.max_retries, 3);
630
+ assert_eq!(llm_policy.backoff_strategy, BackoffStrategy::Exponential);
631
+
632
+ let fatal_policy = ExecutionErrorCategory::PolicyViolation.default_retry_policy();
633
+ assert!(!fatal_policy.retryable);
634
+ assert_eq!(fatal_policy.max_retries, 0);
635
+ }
636
+
637
+ #[test]
638
+ fn test_exponential_backoff() {
639
+ let strategy = BackoffStrategy::Exponential;
640
+ let base = Duration::from_millis(1000);
641
+ let max = Duration::from_millis(30000);
642
+
643
+ assert_eq!(strategy.calculate_delay(base, 1, max), Duration::from_millis(1000));
644
+ assert_eq!(strategy.calculate_delay(base, 2, max), Duration::from_millis(2000));
645
+ assert_eq!(strategy.calculate_delay(base, 3, max), Duration::from_millis(4000));
646
+ assert_eq!(strategy.calculate_delay(base, 4, max), Duration::from_millis(8000));
647
+ assert_eq!(strategy.calculate_delay(base, 5, max), Duration::from_millis(16000));
648
+ // Should cap at max
649
+ assert_eq!(strategy.calculate_delay(base, 6, max), Duration::from_millis(30000));
650
+ }
651
+
652
+ #[test]
653
+ fn test_execution_error_creation() {
654
+ let error = ExecutionError::llm(LlmErrorCode::RateLimit, "Too many requests")
655
+ .with_provider("azure")
656
+ .with_http_status(429);
657
+
658
+ assert_eq!(error.category, ExecutionErrorCategory::LlmError);
659
+ assert_eq!(error.code, Some("rate_limit".to_string()));
660
+ assert_eq!(error.provider, Some("azure".to_string()));
661
+ assert_eq!(error.http_status, Some(429));
662
+ assert!(error.is_retryable());
663
+ assert!(!error.is_fatal());
664
+ }
665
+
666
+ #[test]
667
+ fn test_should_retry() {
668
+ let mut error = ExecutionError::llm(LlmErrorCode::RateLimit, "Rate limited");
669
+
670
+ // Default max_retries is 3
671
+ assert!(error.should_retry()); // attempt 1
672
+ error = error.next_attempt();
673
+ assert!(error.should_retry()); // attempt 2
674
+ error = error.next_attempt();
675
+ assert!(error.should_retry()); // attempt 3
676
+ error = error.next_attempt();
677
+ assert!(!error.should_retry()); // attempt 4 - exceeds max
678
+ }
679
+
680
+ #[test]
681
+ fn test_fatal_error_never_retries() {
682
+ let error = ExecutionError::policy_violation("Content blocked");
683
+
684
+ assert!(!error.is_retryable());
685
+ assert!(error.is_fatal());
686
+ assert!(!error.should_retry());
687
+ }
688
+
689
+ #[test]
690
+ fn test_http_status_mapping() {
691
+ assert_eq!(ExecutionError::policy_violation("test").to_http_status(), 403);
692
+ assert_eq!(ExecutionError::quota_exceeded("test").to_http_status(), 429);
693
+ assert_eq!(ExecutionError::timeout("test").to_http_status(), 504);
694
+ assert_eq!(ExecutionError::validation("test").to_http_status(), 400);
695
+ }
696
+
697
+ #[test]
698
+ fn test_error_serialization() {
699
+ let error = ExecutionError::llm(LlmErrorCode::RateLimit, "Too many requests")
700
+ .with_provider("azure");
701
+
702
+ let json = serde_json::to_string(&error).unwrap();
703
+ let parsed: ExecutionError = serde_json::from_str(&json).unwrap();
704
+
705
+ assert_eq!(parsed.category, error.category);
706
+ assert_eq!(parsed.message, error.message);
707
+ assert_eq!(parsed.code, error.code);
708
+ assert_eq!(parsed.provider, error.provider);
709
+ }
710
+
711
+ // =========================================================================
712
+ // ExecutionErrorCategory Tests
713
+ // =========================================================================
714
+
715
+ #[test]
716
+ fn test_error_category_display() {
717
+ assert_eq!(format!("{}", ExecutionErrorCategory::LlmError), "LlmError");
718
+ assert_eq!(format!("{}", ExecutionErrorCategory::ToolError), "ToolError");
719
+ assert_eq!(format!("{}", ExecutionErrorCategory::PolicyViolation), "PolicyViolation");
720
+ assert_eq!(format!("{}", ExecutionErrorCategory::Timeout), "Timeout");
721
+ assert_eq!(format!("{}", ExecutionErrorCategory::QuotaExceeded), "QuotaExceeded");
722
+ assert_eq!(format!("{}", ExecutionErrorCategory::KernelInternal), "KernelInternal");
723
+ assert_eq!(format!("{}", ExecutionErrorCategory::ValidationError), "ValidationError");
724
+ assert_eq!(format!("{}", ExecutionErrorCategory::NetworkError), "NetworkError");
725
+ }
726
+
727
+ #[test]
728
+ fn test_error_category_serde() {
729
+ let categories = vec![
730
+ ExecutionErrorCategory::LlmError,
731
+ ExecutionErrorCategory::ToolError,
732
+ ExecutionErrorCategory::PolicyViolation,
733
+ ExecutionErrorCategory::Timeout,
734
+ ExecutionErrorCategory::QuotaExceeded,
735
+ ExecutionErrorCategory::KernelInternal,
736
+ ExecutionErrorCategory::ValidationError,
737
+ ExecutionErrorCategory::NetworkError,
738
+ ];
739
+
740
+ for cat in categories {
741
+ let json = serde_json::to_string(&cat).unwrap();
742
+ let parsed: ExecutionErrorCategory = serde_json::from_str(&json).unwrap();
743
+ assert_eq!(cat, parsed);
744
+ }
745
+ }
746
+
747
+ // =========================================================================
748
+ // BackoffStrategy Tests
749
+ // =========================================================================
750
+
751
+ #[test]
752
+ fn test_backoff_none() {
753
+ let strategy = BackoffStrategy::None;
754
+ let base = Duration::from_millis(1000);
755
+ let max = Duration::from_millis(30000);
756
+
757
+ assert_eq!(strategy.calculate_delay(base, 1, max), Duration::ZERO);
758
+ assert_eq!(strategy.calculate_delay(base, 5, max), Duration::ZERO);
759
+ }
760
+
761
+ #[test]
762
+ fn test_backoff_constant() {
763
+ let strategy = BackoffStrategy::Constant;
764
+ let base = Duration::from_millis(500);
765
+ let max = Duration::from_millis(10000);
766
+
767
+ assert_eq!(strategy.calculate_delay(base, 1, max), Duration::from_millis(500));
768
+ assert_eq!(strategy.calculate_delay(base, 5, max), Duration::from_millis(500));
769
+ }
770
+
771
+ #[test]
772
+ fn test_backoff_linear() {
773
+ let strategy = BackoffStrategy::Linear;
774
+ let base = Duration::from_millis(1000);
775
+ let max = Duration::from_millis(30000);
776
+
777
+ assert_eq!(strategy.calculate_delay(base, 1, max), Duration::from_millis(1000));
778
+ assert_eq!(strategy.calculate_delay(base, 2, max), Duration::from_millis(2000));
779
+ assert_eq!(strategy.calculate_delay(base, 3, max), Duration::from_millis(3000));
780
+ // Should cap at max
781
+ assert_eq!(strategy.calculate_delay(base, 100, max), Duration::from_millis(30000));
782
+ }
783
+
784
+ #[test]
785
+ fn test_backoff_default() {
786
+ assert_eq!(BackoffStrategy::default(), BackoffStrategy::None);
787
+ }
788
+
789
+ #[test]
790
+ fn test_backoff_serde() {
791
+ let strategies = vec![
792
+ BackoffStrategy::None,
793
+ BackoffStrategy::Constant,
794
+ BackoffStrategy::Linear,
795
+ BackoffStrategy::Exponential,
796
+ ];
797
+
798
+ for strat in strategies {
799
+ let json = serde_json::to_string(&strat).unwrap();
800
+ let parsed: BackoffStrategy = serde_json::from_str(&json).unwrap();
801
+ assert_eq!(strat, parsed);
802
+ }
803
+ }
804
+
805
+ // =========================================================================
806
+ // RetryPolicy Tests
807
+ // =========================================================================
808
+
809
+ #[test]
810
+ fn test_retry_policy_fatal() {
811
+ let policy = RetryPolicy::fatal();
812
+ assert!(!policy.retryable);
813
+ assert_eq!(policy.max_retries, 0);
814
+ assert!(!policy.should_retry(1));
815
+ }
816
+
817
+ #[test]
818
+ fn test_retry_policy_retryable() {
819
+ let policy = RetryPolicy::retryable(5);
820
+ assert!(policy.retryable);
821
+ assert_eq!(policy.max_retries, 5);
822
+ assert_eq!(policy.backoff_strategy, BackoffStrategy::Exponential);
823
+ }
824
+
825
+ #[test]
826
+ fn test_retry_policy_delay_for_attempt() {
827
+ let policy = RetryPolicy {
828
+ retryable: true,
829
+ max_retries: 3,
830
+ backoff_strategy: BackoffStrategy::Constant,
831
+ base_delay: Duration::from_millis(500),
832
+ max_delay: Duration::from_millis(5000),
833
+ requires_idempotency_key: false,
834
+ };
835
+
836
+ assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(500));
837
+ assert_eq!(policy.delay_for_attempt(2), Duration::from_millis(500));
838
+ }
839
+
840
+ #[test]
841
+ fn test_retry_policy_should_retry() {
842
+ let policy = RetryPolicy::retryable(3);
843
+ assert!(policy.should_retry(1));
844
+ assert!(policy.should_retry(2));
845
+ assert!(policy.should_retry(3));
846
+ assert!(!policy.should_retry(4));
847
+ }
848
+
849
+ #[test]
850
+ fn test_retry_policy_default() {
851
+ let policy = RetryPolicy::default();
852
+ assert!(!policy.retryable);
853
+ assert_eq!(policy.max_retries, 0);
854
+ }
855
+
856
+ #[test]
857
+ fn test_retry_policy_serde() {
858
+ let policy = RetryPolicy::retryable(3);
859
+ let json = serde_json::to_string(&policy).unwrap();
860
+ let parsed: RetryPolicy = serde_json::from_str(&json).unwrap();
861
+ assert_eq!(policy.retryable, parsed.retryable);
862
+ assert_eq!(policy.max_retries, parsed.max_retries);
863
+ }
864
+
865
+ // =========================================================================
866
+ // LlmErrorCode Tests
867
+ // =========================================================================
868
+
869
+ #[test]
870
+ fn test_llm_error_code_retryable() {
871
+ assert!(LlmErrorCode::RateLimit.is_retryable());
872
+ assert!(LlmErrorCode::ModelUnavailable.is_retryable());
873
+ assert!(LlmErrorCode::ProviderError.is_retryable());
874
+
875
+ assert!(!LlmErrorCode::ContextOverflow.is_retryable());
876
+ assert!(!LlmErrorCode::ContentFiltered.is_retryable());
877
+ assert!(!LlmErrorCode::InvalidRequest.is_retryable());
878
+ assert!(!LlmErrorCode::AuthFailed.is_retryable());
879
+ }
880
+
881
+ #[test]
882
+ fn test_llm_error_code_display() {
883
+ assert_eq!(format!("{}", LlmErrorCode::RateLimit), "rate_limit");
884
+ assert_eq!(format!("{}", LlmErrorCode::ContextOverflow), "context_overflow");
885
+ assert_eq!(format!("{}", LlmErrorCode::ContentFiltered), "content_filtered");
886
+ assert_eq!(format!("{}", LlmErrorCode::InvalidRequest), "invalid_request");
887
+ assert_eq!(format!("{}", LlmErrorCode::AuthFailed), "auth_failed");
888
+ assert_eq!(format!("{}", LlmErrorCode::ModelUnavailable), "model_unavailable");
889
+ assert_eq!(format!("{}", LlmErrorCode::ProviderError), "provider_error");
890
+ }
891
+
892
+ #[test]
893
+ fn test_llm_error_code_serde() {
894
+ let codes = vec![
895
+ LlmErrorCode::RateLimit,
896
+ LlmErrorCode::ContextOverflow,
897
+ LlmErrorCode::ContentFiltered,
898
+ LlmErrorCode::InvalidRequest,
899
+ LlmErrorCode::AuthFailed,
900
+ LlmErrorCode::ModelUnavailable,
901
+ LlmErrorCode::ProviderError,
902
+ ];
903
+
904
+ for code in codes {
905
+ let json = serde_json::to_string(&code).unwrap();
906
+ let parsed: LlmErrorCode = serde_json::from_str(&json).unwrap();
907
+ assert_eq!(code, parsed);
908
+ }
909
+ }
910
+
911
+ // =========================================================================
912
+ // ToolErrorCode Tests
913
+ // =========================================================================
914
+
915
+ #[test]
916
+ fn test_tool_error_code_retryable() {
917
+ assert!(ToolErrorCode::Timeout.is_retryable());
918
+ assert!(ToolErrorCode::ExecutionFailed.is_retryable());
919
+
920
+ assert!(!ToolErrorCode::NotFound.is_retryable());
921
+ assert!(!ToolErrorCode::PermissionDenied.is_retryable());
922
+ assert!(!ToolErrorCode::InvalidInput.is_retryable());
923
+ assert!(!ToolErrorCode::OutputInvalid.is_retryable());
924
+ }
925
+
926
+ #[test]
927
+ fn test_tool_error_code_display() {
928
+ assert_eq!(format!("{}", ToolErrorCode::NotFound), "not_found");
929
+ assert_eq!(format!("{}", ToolErrorCode::PermissionDenied), "permission_denied");
930
+ assert_eq!(format!("{}", ToolErrorCode::InvalidInput), "invalid_input");
931
+ assert_eq!(format!("{}", ToolErrorCode::ExecutionFailed), "execution_failed");
932
+ assert_eq!(format!("{}", ToolErrorCode::Timeout), "timeout");
933
+ assert_eq!(format!("{}", ToolErrorCode::OutputInvalid), "output_invalid");
934
+ }
935
+
936
+ #[test]
937
+ fn test_tool_error_code_serde() {
938
+ let codes = vec![
939
+ ToolErrorCode::NotFound,
940
+ ToolErrorCode::PermissionDenied,
941
+ ToolErrorCode::InvalidInput,
942
+ ToolErrorCode::ExecutionFailed,
943
+ ToolErrorCode::Timeout,
944
+ ToolErrorCode::OutputInvalid,
945
+ ];
946
+
947
+ for code in codes {
948
+ let json = serde_json::to_string(&code).unwrap();
949
+ let parsed: ToolErrorCode = serde_json::from_str(&json).unwrap();
950
+ assert_eq!(code, parsed);
951
+ }
952
+ }
953
+
954
+ // =========================================================================
955
+ // ExecutionError Tests
956
+ // =========================================================================
957
+
958
+ #[test]
959
+ fn test_execution_error_new() {
960
+ let error = ExecutionError::new(ExecutionErrorCategory::LlmError, "Test message");
961
+ assert_eq!(error.category, ExecutionErrorCategory::LlmError);
962
+ assert_eq!(error.message, "Test message");
963
+ assert_eq!(error.attempt, 1);
964
+ assert!(error.retry_policy.retryable);
965
+ }
966
+
967
+ #[test]
968
+ fn test_execution_error_with_code() {
969
+ let error = ExecutionError::new(ExecutionErrorCategory::ToolError, "Test")
970
+ .with_code("custom_code");
971
+ assert_eq!(error.code, Some("custom_code".to_string()));
972
+ }
973
+
974
+ #[test]
975
+ fn test_execution_error_with_attempt() {
976
+ let error = ExecutionError::new(ExecutionErrorCategory::LlmError, "Test")
977
+ .with_attempt(3);
978
+ assert_eq!(error.attempt, 3);
979
+ }
980
+
981
+ #[test]
982
+ fn test_execution_error_with_step_id() {
983
+ let step_id = StepId::from_string("step_test");
984
+ let error = ExecutionError::new(ExecutionErrorCategory::ToolError, "Test")
985
+ .with_step_id(step_id.clone());
986
+ assert_eq!(error.step_id.unwrap().as_str(), "step_test");
987
+ }
988
+
989
+ #[test]
990
+ fn test_execution_error_with_provider() {
991
+ let error = ExecutionError::llm(LlmErrorCode::RateLimit, "Test")
992
+ .with_provider("openai");
993
+ assert_eq!(error.provider, Some("openai".to_string()));
994
+ }
995
+
996
+ #[test]
997
+ fn test_execution_error_with_details() {
998
+ let details = serde_json::json!({"key": "value"});
999
+ let error = ExecutionError::new(ExecutionErrorCategory::ToolError, "Test")
1000
+ .with_details(details.clone());
1001
+ assert_eq!(error.details, Some(details));
1002
+ }
1003
+
1004
+ #[test]
1005
+ fn test_execution_error_with_retry_policy() {
1006
+ let policy = RetryPolicy::retryable(10);
1007
+ let error = ExecutionError::new(ExecutionErrorCategory::ToolError, "Test")
1008
+ .with_retry_policy(policy.clone());
1009
+ assert_eq!(error.retry_policy.max_retries, 10);
1010
+ }
1011
+
1012
+ #[test]
1013
+ fn test_execution_error_convenience_constructors() {
1014
+ let llm = ExecutionError::llm(LlmErrorCode::RateLimit, "LLM error");
1015
+ assert_eq!(llm.category, ExecutionErrorCategory::LlmError);
1016
+
1017
+ let tool = ExecutionError::tool(ToolErrorCode::NotFound, "Tool error");
1018
+ assert_eq!(tool.category, ExecutionErrorCategory::ToolError);
1019
+
1020
+ let policy = ExecutionError::policy_violation("Policy error");
1021
+ assert_eq!(policy.category, ExecutionErrorCategory::PolicyViolation);
1022
+
1023
+ let timeout = ExecutionError::timeout("Timeout error");
1024
+ assert_eq!(timeout.category, ExecutionErrorCategory::Timeout);
1025
+
1026
+ let quota = ExecutionError::quota_exceeded("Quota error");
1027
+ assert_eq!(quota.category, ExecutionErrorCategory::QuotaExceeded);
1028
+
1029
+ let kernel = ExecutionError::kernel_internal("Kernel error");
1030
+ assert_eq!(kernel.category, ExecutionErrorCategory::KernelInternal);
1031
+
1032
+ let validation = ExecutionError::validation("Validation error");
1033
+ assert_eq!(validation.category, ExecutionErrorCategory::ValidationError);
1034
+
1035
+ let network = ExecutionError::network("Network error");
1036
+ assert_eq!(network.category, ExecutionErrorCategory::NetworkError);
1037
+ }
1038
+
1039
+ #[test]
1040
+ fn test_execution_error_is_retryable() {
1041
+ assert!(ExecutionError::llm(LlmErrorCode::RateLimit, "").is_retryable());
1042
+ assert!(ExecutionError::tool(ToolErrorCode::Timeout, "").is_retryable());
1043
+ assert!(ExecutionError::timeout("").is_retryable());
1044
+ assert!(ExecutionError::network("").is_retryable());
1045
+
1046
+ assert!(!ExecutionError::policy_violation("").is_retryable());
1047
+ assert!(!ExecutionError::quota_exceeded("").is_retryable());
1048
+ assert!(!ExecutionError::kernel_internal("").is_retryable());
1049
+ assert!(!ExecutionError::validation("").is_retryable());
1050
+ }
1051
+
1052
+ #[test]
1053
+ fn test_execution_error_is_fatal() {
1054
+ assert!(!ExecutionError::llm(LlmErrorCode::RateLimit, "").is_fatal());
1055
+ assert!(!ExecutionError::timeout("").is_fatal());
1056
+
1057
+ assert!(ExecutionError::policy_violation("").is_fatal());
1058
+ assert!(ExecutionError::quota_exceeded("").is_fatal());
1059
+ assert!(ExecutionError::kernel_internal("").is_fatal());
1060
+ assert!(ExecutionError::validation("").is_fatal());
1061
+ }
1062
+
1063
+ #[test]
1064
+ fn test_execution_error_retry_delay() {
1065
+ let error = ExecutionError::new(ExecutionErrorCategory::LlmError, "Test");
1066
+ let delay = error.retry_delay();
1067
+ assert!(delay > Duration::ZERO);
1068
+ }
1069
+
1070
+ #[test]
1071
+ fn test_execution_error_next_attempt() {
1072
+ let error = ExecutionError::llm(LlmErrorCode::RateLimit, "Test");
1073
+ assert_eq!(error.attempt, 1);
1074
+
1075
+ let error2 = error.next_attempt();
1076
+ assert_eq!(error2.attempt, 2);
1077
+
1078
+ let error3 = error2.next_attempt();
1079
+ assert_eq!(error3.attempt, 3);
1080
+ }
1081
+
1082
+ #[test]
1083
+ fn test_execution_error_to_http_status_all_categories() {
1084
+ assert_eq!(ExecutionError::llm(LlmErrorCode::RateLimit, "").to_http_status(), 502);
1085
+ assert_eq!(ExecutionError::tool(ToolErrorCode::NotFound, "").to_http_status(), 500);
1086
+ assert_eq!(ExecutionError::policy_violation("").to_http_status(), 403);
1087
+ assert_eq!(ExecutionError::timeout("").to_http_status(), 504);
1088
+ assert_eq!(ExecutionError::quota_exceeded("").to_http_status(), 429);
1089
+ assert_eq!(ExecutionError::kernel_internal("").to_http_status(), 500);
1090
+ assert_eq!(ExecutionError::validation("").to_http_status(), 400);
1091
+ assert_eq!(ExecutionError::network("").to_http_status(), 503);
1092
+ }
1093
+
1094
+ #[test]
1095
+ fn test_execution_error_to_http_status_override() {
1096
+ let error = ExecutionError::network("Test").with_http_status(418);
1097
+ assert_eq!(error.to_http_status(), 418);
1098
+ }
1099
+
1100
+ #[test]
1101
+ fn test_execution_error_display() {
1102
+ let error = ExecutionError::llm(LlmErrorCode::RateLimit, "Too many requests");
1103
+ let display = format!("{}", error);
1104
+ assert!(display.contains("LlmError"));
1105
+ assert!(display.contains("Too many requests"));
1106
+ assert!(display.contains("rate_limit"));
1107
+ }
1108
+
1109
+ #[test]
1110
+ fn test_execution_error_display_with_attempt() {
1111
+ let error = ExecutionError::llm(LlmErrorCode::RateLimit, "Test")
1112
+ .with_attempt(3);
1113
+ let display = format!("{}", error);
1114
+ assert!(display.contains("[attempt 3]"));
1115
+ }
1116
+
1117
+ #[test]
1118
+ fn test_execution_error_display_no_attempt_shown_for_first() {
1119
+ let error = ExecutionError::llm(LlmErrorCode::RateLimit, "Test");
1120
+ let display = format!("{}", error);
1121
+ assert!(!display.contains("attempt"));
1122
+ }
1123
+
1124
+ // =========================================================================
1125
+ // From Implementation Tests
1126
+ // =========================================================================
1127
+
1128
+ #[test]
1129
+ fn test_from_serde_json_error() {
1130
+ let json_err = serde_json::from_str::<String>("invalid json").unwrap_err();
1131
+ let error: ExecutionError = json_err.into();
1132
+ assert_eq!(error.category, ExecutionErrorCategory::ValidationError);
1133
+ assert!(error.message.contains("JSON error"));
1134
+ }
1135
+
1136
+ #[test]
1137
+ fn test_from_io_error_timeout() {
1138
+ let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out");
1139
+ let error: ExecutionError = io_err.into();
1140
+ assert_eq!(error.category, ExecutionErrorCategory::Timeout);
1141
+ }
1142
+
1143
+ #[test]
1144
+ fn test_from_io_error_connection_refused() {
1145
+ let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
1146
+ let error: ExecutionError = io_err.into();
1147
+ assert_eq!(error.category, ExecutionErrorCategory::NetworkError);
1148
+ }
1149
+
1150
+ #[test]
1151
+ fn test_from_io_error_connection_reset() {
1152
+ let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
1153
+ let error: ExecutionError = io_err.into();
1154
+ assert_eq!(error.category, ExecutionErrorCategory::NetworkError);
1155
+ }
1156
+
1157
+ #[test]
1158
+ fn test_from_io_error_connection_aborted() {
1159
+ let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionAborted, "aborted");
1160
+ let error: ExecutionError = io_err.into();
1161
+ assert_eq!(error.category, ExecutionErrorCategory::NetworkError);
1162
+ }
1163
+
1164
+ #[test]
1165
+ fn test_from_io_error_other() {
1166
+ let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
1167
+ let error: ExecutionError = io_err.into();
1168
+ assert_eq!(error.category, ExecutionErrorCategory::KernelInternal);
1169
+ }
1170
+
1171
+ // =========================================================================
1172
+ // Default Retry Policy Tests
1173
+ // =========================================================================
1174
+
1175
+ #[test]
1176
+ fn test_default_retry_policy_tool_error() {
1177
+ let policy = ExecutionErrorCategory::ToolError.default_retry_policy();
1178
+ assert!(policy.retryable);
1179
+ assert_eq!(policy.max_retries, 2);
1180
+ assert_eq!(policy.backoff_strategy, BackoffStrategy::Constant);
1181
+ assert!(policy.requires_idempotency_key);
1182
+ }
1183
+
1184
+ #[test]
1185
+ fn test_default_retry_policy_timeout() {
1186
+ let policy = ExecutionErrorCategory::Timeout.default_retry_policy();
1187
+ assert!(policy.retryable);
1188
+ assert_eq!(policy.max_retries, 1);
1189
+ assert!(policy.requires_idempotency_key);
1190
+ }
1191
+
1192
+ #[test]
1193
+ fn test_default_retry_policy_network() {
1194
+ let policy = ExecutionErrorCategory::NetworkError.default_retry_policy();
1195
+ assert!(policy.retryable);
1196
+ assert_eq!(policy.max_retries, 3);
1197
+ assert_eq!(policy.backoff_strategy, BackoffStrategy::Exponential);
1198
+ assert!(policy.requires_idempotency_key);
1199
+ }
1200
+ }