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,421 @@
1
+ //! Multi-format tool call parser
2
+ //!
3
+ //! Parses tool calls from LLM output across multiple formats:
4
+ //! JSON → XML → Markdown (priority order).
5
+ //!
6
+ //! Ported from zeroclaw's `parse_tool_calls` which handles JSON, XML,
7
+ //! Markdown, and GLM-style formats for maximum resilience.
8
+
9
+ use serde::{Deserialize, Serialize};
10
+ use serde_json::Value;
11
+
12
+ /// A parsed tool call extracted from LLM output.
13
+ #[derive(Debug, Clone, Serialize, Deserialize)]
14
+ pub struct ParsedToolCall {
15
+ /// Tool name to invoke
16
+ pub name: String,
17
+ /// Arguments as a JSON value
18
+ pub arguments: Value,
19
+ /// Which format was used to detect this call
20
+ pub format: ToolCallFormat,
21
+ }
22
+
23
+ /// The format in which the tool call was detected.
24
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25
+ pub enum ToolCallFormat {
26
+ /// Standard JSON: `{"tool_call": {"name": "...", "arguments": {...}}}`
27
+ JsonObject,
28
+ /// JSON array: `[{"name": "...", "arguments": {...}}]`
29
+ JsonArray,
30
+ /// XML: `<tool_call><name>...</name><arguments>...</arguments></tool_call>`
31
+ Xml,
32
+ /// Markdown code block: `` ```tool_call\n{"name": "...", ...}\n``` ``
33
+ Markdown,
34
+ }
35
+
36
+ /// Result of parsing: extracted text (non-tool-call content) and tool calls.
37
+ pub struct ParseResult {
38
+ /// Text content that is NOT a tool call
39
+ pub text: String,
40
+ /// Parsed tool calls found in the response
41
+ pub tool_calls: Vec<ParsedToolCall>,
42
+ }
43
+
44
+ /// Parse tool calls from an LLM response string.
45
+ ///
46
+ /// Tries formats in priority order: JSON → XML → Markdown.
47
+ /// Returns all detected tool calls and the remaining text.
48
+ pub fn parse(response: &str) -> ParseResult {
49
+ // 1. Try JSON object format
50
+ if let Some(result) = try_parse_json_object(response) {
51
+ return result;
52
+ }
53
+
54
+ // 2. Try JSON array format
55
+ if let Some(result) = try_parse_json_array(response) {
56
+ return result;
57
+ }
58
+
59
+ // 3. Try XML format
60
+ if let Some(result) = try_parse_xml(response) {
61
+ return result;
62
+ }
63
+
64
+ // 4. Try Markdown code block format
65
+ if let Some(result) = try_parse_markdown(response) {
66
+ return result;
67
+ }
68
+
69
+ // No tool calls found — entire response is text
70
+ ParseResult {
71
+ text: response.to_string(),
72
+ tool_calls: vec![],
73
+ }
74
+ }
75
+
76
+ /// Try parsing as a JSON object: `{"tool_call": {"name": "...", "arguments": {...}}}`
77
+ /// Also handles: `{"tool_calls": [...]}`
78
+ fn try_parse_json_object(response: &str) -> Option<ParseResult> {
79
+ let trimmed = response.trim();
80
+
81
+ // Must start with '{' to be a JSON object
82
+ if !trimmed.starts_with('{') {
83
+ return None;
84
+ }
85
+
86
+ let parsed: Value = serde_json::from_str(trimmed).ok()?;
87
+
88
+ // Format 1: {"tool_call": {"name": "...", "arguments": {...}}}
89
+ if let Some(tc) = parsed.get("tool_call") {
90
+ let name = tc.get("name")?.as_str()?.to_string();
91
+ let arguments = tc
92
+ .get("arguments")
93
+ .cloned()
94
+ .unwrap_or(Value::Object(Default::default()));
95
+ return Some(ParseResult {
96
+ text: String::new(),
97
+ tool_calls: vec![ParsedToolCall {
98
+ name,
99
+ arguments,
100
+ format: ToolCallFormat::JsonObject,
101
+ }],
102
+ });
103
+ }
104
+
105
+ // Format 2: {"tool_calls": [{"name": "...", "arguments": {...}}, ...]}
106
+ if let Some(tcs) = parsed.get("tool_calls").and_then(|v| v.as_array()) {
107
+ let calls: Vec<ParsedToolCall> = tcs
108
+ .iter()
109
+ .filter_map(|tc| {
110
+ let name = tc.get("name")?.as_str()?.to_string();
111
+ let arguments = tc
112
+ .get("arguments")
113
+ .cloned()
114
+ .unwrap_or(Value::Object(Default::default()));
115
+ Some(ParsedToolCall {
116
+ name,
117
+ arguments,
118
+ format: ToolCallFormat::JsonObject,
119
+ })
120
+ })
121
+ .collect();
122
+
123
+ if !calls.is_empty() {
124
+ return Some(ParseResult {
125
+ text: String::new(),
126
+ tool_calls: calls,
127
+ });
128
+ }
129
+ }
130
+
131
+ // Format 3: {"name": "...", "arguments": {...}} (bare tool call)
132
+ if let (Some(name), Some(_)) = (
133
+ parsed.get("name").and_then(|v| v.as_str()),
134
+ parsed.get("arguments"),
135
+ ) {
136
+ return Some(ParseResult {
137
+ text: String::new(),
138
+ tool_calls: vec![ParsedToolCall {
139
+ name: name.to_string(),
140
+ arguments: parsed
141
+ .get("arguments")
142
+ .cloned()
143
+ .unwrap_or(Value::Object(Default::default())),
144
+ format: ToolCallFormat::JsonObject,
145
+ }],
146
+ });
147
+ }
148
+
149
+ None
150
+ }
151
+
152
+ /// Try parsing as a JSON array: `[{"name": "...", "arguments": {...}}, ...]`
153
+ fn try_parse_json_array(response: &str) -> Option<ParseResult> {
154
+ let trimmed = response.trim();
155
+
156
+ if !trimmed.starts_with('[') {
157
+ return None;
158
+ }
159
+
160
+ let parsed: Vec<Value> = serde_json::from_str(trimmed).ok()?;
161
+
162
+ let calls: Vec<ParsedToolCall> = parsed
163
+ .iter()
164
+ .filter_map(|tc| {
165
+ let name = tc.get("name")?.as_str()?.to_string();
166
+ let arguments = tc
167
+ .get("arguments")
168
+ .cloned()
169
+ .unwrap_or(Value::Object(Default::default()));
170
+ Some(ParsedToolCall {
171
+ name,
172
+ arguments,
173
+ format: ToolCallFormat::JsonArray,
174
+ })
175
+ })
176
+ .collect();
177
+
178
+ if calls.is_empty() {
179
+ return None;
180
+ }
181
+
182
+ Some(ParseResult {
183
+ text: String::new(),
184
+ tool_calls: calls,
185
+ })
186
+ }
187
+
188
+ /// Try parsing XML format:
189
+ /// `<tool_call><name>tool_name</name><arguments>{"key": "value"}</arguments></tool_call>`
190
+ fn try_parse_xml(response: &str) -> Option<ParseResult> {
191
+ let mut calls = Vec::new();
192
+ let mut text_parts = Vec::new();
193
+ let mut remaining = response;
194
+
195
+ while let Some(start) = remaining.find("<tool_call>") {
196
+ // Collect text before the tool call
197
+ let before = &remaining[..start];
198
+ if !before.trim().is_empty() {
199
+ text_parts.push(before.trim().to_string());
200
+ }
201
+
202
+ let after_start = &remaining[start + "<tool_call>".len()..];
203
+ let end = after_start.find("</tool_call>")?;
204
+ let inner = &after_start[..end];
205
+
206
+ // Extract name
207
+ let name_start = inner.find("<name>")? + "<name>".len();
208
+ let name_end = inner.find("</name>")?;
209
+ let name = inner[name_start..name_end].trim().to_string();
210
+
211
+ // Extract arguments
212
+ let args = if let Some(args_start_pos) = inner.find("<arguments>") {
213
+ let args_content_start = args_start_pos + "<arguments>".len();
214
+ if let Some(args_end_pos) = inner.find("</arguments>") {
215
+ let args_str = inner[args_content_start..args_end_pos].trim();
216
+ serde_json::from_str(args_str).unwrap_or(Value::Object(Default::default()))
217
+ } else {
218
+ Value::Object(Default::default())
219
+ }
220
+ } else {
221
+ Value::Object(Default::default())
222
+ };
223
+
224
+ calls.push(ParsedToolCall {
225
+ name,
226
+ arguments: args,
227
+ format: ToolCallFormat::Xml,
228
+ });
229
+
230
+ remaining = &after_start[end + "</tool_call>".len()..];
231
+ }
232
+
233
+ // Collect remaining text
234
+ if !remaining.trim().is_empty() {
235
+ text_parts.push(remaining.trim().to_string());
236
+ }
237
+
238
+ if calls.is_empty() {
239
+ return None;
240
+ }
241
+
242
+ Some(ParseResult {
243
+ text: text_parts.join("\n"),
244
+ tool_calls: calls,
245
+ })
246
+ }
247
+
248
+ /// Try parsing Markdown code block format:
249
+ /// ````tool_call
250
+ /// {"name": "tool_name", "arguments": {"key": "value"}}
251
+ /// ````
252
+ fn try_parse_markdown(response: &str) -> Option<ParseResult> {
253
+ let mut calls = Vec::new();
254
+ let mut text_parts = Vec::new();
255
+ let mut remaining = response;
256
+
257
+ let fence_patterns = ["```tool_call\n", "```tool_call\r\n"];
258
+
259
+ loop {
260
+ let mut found = false;
261
+ for pattern in &fence_patterns {
262
+ if let Some(start) = remaining.find(pattern) {
263
+ // Collect text before the code block
264
+ let before = &remaining[..start];
265
+ if !before.trim().is_empty() {
266
+ text_parts.push(before.trim().to_string());
267
+ }
268
+
269
+ let content_start = start + pattern.len();
270
+ let after_content = &remaining[content_start..];
271
+
272
+ if let Some(end) = after_content.find("```") {
273
+ let block_content = after_content[..end].trim();
274
+
275
+ // Parse the JSON inside the code block
276
+ if let Ok(parsed) = serde_json::from_str::<Value>(block_content) {
277
+ if let Some(name) = parsed.get("name").and_then(|v| v.as_str()) {
278
+ let arguments = parsed
279
+ .get("arguments")
280
+ .cloned()
281
+ .unwrap_or(Value::Object(Default::default()));
282
+ calls.push(ParsedToolCall {
283
+ name: name.to_string(),
284
+ arguments,
285
+ format: ToolCallFormat::Markdown,
286
+ });
287
+ }
288
+ }
289
+
290
+ remaining = &after_content[end + "```".len()..];
291
+ found = true;
292
+ break;
293
+ }
294
+ }
295
+ }
296
+
297
+ if !found {
298
+ break;
299
+ }
300
+ }
301
+
302
+ // Collect remaining text
303
+ if !remaining.trim().is_empty() {
304
+ text_parts.push(remaining.trim().to_string());
305
+ }
306
+
307
+ if calls.is_empty() {
308
+ return None;
309
+ }
310
+
311
+ Some(ParseResult {
312
+ text: text_parts.join("\n"),
313
+ tool_calls: calls,
314
+ })
315
+ }
316
+
317
+ #[cfg(test)]
318
+ mod tests {
319
+ use super::*;
320
+
321
+ // ============ JSON Object Tests ============
322
+
323
+ #[test]
324
+ fn test_parse_json_tool_call() {
325
+ let input = r#"{"tool_call": {"name": "search", "arguments": {"query": "rust"}}}"#;
326
+ let result = parse(input);
327
+ assert_eq!(result.tool_calls.len(), 1);
328
+ assert_eq!(result.tool_calls[0].name, "search");
329
+ assert_eq!(result.tool_calls[0].format, ToolCallFormat::JsonObject);
330
+ assert_eq!(result.tool_calls[0].arguments["query"], "rust");
331
+ }
332
+
333
+ #[test]
334
+ fn test_parse_json_tool_calls_array() {
335
+ let input = r#"{"tool_calls": [
336
+ {"name": "search", "arguments": {"query": "rust"}},
337
+ {"name": "read_file", "arguments": {"path": "/tmp/test"}}
338
+ ]}"#;
339
+ let result = parse(input);
340
+ assert_eq!(result.tool_calls.len(), 2);
341
+ assert_eq!(result.tool_calls[0].name, "search");
342
+ assert_eq!(result.tool_calls[1].name, "read_file");
343
+ }
344
+
345
+ #[test]
346
+ fn test_parse_bare_json_tool_call() {
347
+ let input = r#"{"name": "search", "arguments": {"query": "rust"}}"#;
348
+ let result = parse(input);
349
+ assert_eq!(result.tool_calls.len(), 1);
350
+ assert_eq!(result.tool_calls[0].name, "search");
351
+ }
352
+
353
+ // ============ JSON Array Tests ============
354
+
355
+ #[test]
356
+ fn test_parse_json_array() {
357
+ let input = r#"[{"name": "search", "arguments": {"query": "rust"}}]"#;
358
+ let result = parse(input);
359
+ assert_eq!(result.tool_calls.len(), 1);
360
+ assert_eq!(result.tool_calls[0].format, ToolCallFormat::JsonArray);
361
+ }
362
+
363
+ // ============ XML Tests ============
364
+
365
+ #[test]
366
+ fn test_parse_xml() {
367
+ let input = r#"Let me search for that.
368
+ <tool_call><name>search</name><arguments>{"query": "rust"}</arguments></tool_call>"#;
369
+ let result = parse(input);
370
+ assert_eq!(result.tool_calls.len(), 1);
371
+ assert_eq!(result.tool_calls[0].name, "search");
372
+ assert_eq!(result.tool_calls[0].format, ToolCallFormat::Xml);
373
+ assert!(result.text.contains("Let me search"));
374
+ }
375
+
376
+ #[test]
377
+ fn test_parse_multiple_xml() {
378
+ let input = r#"<tool_call><name>search</name><arguments>{"q": "a"}</arguments></tool_call>
379
+ <tool_call><name>read</name><arguments>{"path": "b"}</arguments></tool_call>"#;
380
+ let result = parse(input);
381
+ assert_eq!(result.tool_calls.len(), 2);
382
+ }
383
+
384
+ // ============ Markdown Tests ============
385
+
386
+ #[test]
387
+ fn test_parse_markdown() {
388
+ let input = "Here's what I'll do:\n```tool_call\n{\"name\": \"search\", \"arguments\": {\"query\": \"rust\"}}\n```\n";
389
+ let result = parse(input);
390
+ assert_eq!(result.tool_calls.len(), 1);
391
+ assert_eq!(result.tool_calls[0].name, "search");
392
+ assert_eq!(result.tool_calls[0].format, ToolCallFormat::Markdown);
393
+ assert!(result.text.contains("Here's what I'll do"));
394
+ }
395
+
396
+ // ============ No Tool Call Tests ============
397
+
398
+ #[test]
399
+ fn test_parse_plain_text() {
400
+ let input = "This is just a normal response with no tool calls.";
401
+ let result = parse(input);
402
+ assert!(result.tool_calls.is_empty());
403
+ assert_eq!(result.text, input);
404
+ }
405
+
406
+ #[test]
407
+ fn test_parse_empty() {
408
+ let result = parse("");
409
+ assert!(result.tool_calls.is_empty());
410
+ }
411
+
412
+ // ============ Priority Tests ============
413
+
414
+ #[test]
415
+ fn test_json_takes_priority_over_xml() {
416
+ // If the response is valid JSON with tool_call, JSON wins
417
+ let input = r#"{"tool_call": {"name": "search", "arguments": {}}}"#;
418
+ let result = parse(input);
419
+ assert_eq!(result.tool_calls[0].format, ToolCallFormat::JsonObject);
420
+ }
421
+ }
@@ -0,0 +1,262 @@
1
+ //! Retry handler with exponential backoff and error classification
2
+ //!
3
+ //! Classifies errors as retryable (rate-limit, network, transient) vs
4
+ //! fatal (auth, invalid request, tool not found) and manages backoff.
5
+ //! Ported from patterns in zeroclaw and openclaw.
6
+
7
+ use crate::config::RetryConfig;
8
+ use std::time::Duration;
9
+
10
+ /// Error classification for retry decisions.
11
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
12
+ pub enum ErrorKind {
13
+ /// Rate limited by provider — always retryable
14
+ RateLimited,
15
+ /// Network/connection error — retryable
16
+ NetworkError,
17
+ /// Transient server error (5xx) — retryable
18
+ ServerError,
19
+ /// Authentication failure — NOT retryable
20
+ AuthError,
21
+ /// Invalid request (bad input, schema mismatch) — NOT retryable
22
+ InvalidRequest,
23
+ /// Tool not found — NOT retryable
24
+ ToolNotFound,
25
+ /// Context window exceeded — needs compaction, not retry
26
+ ContextOverflow,
27
+ /// Unknown error — retryable with caution
28
+ Unknown,
29
+ }
30
+
31
+ impl ErrorKind {
32
+ /// Whether this error class is retryable.
33
+ pub fn is_retryable(&self) -> bool {
34
+ matches!(
35
+ self,
36
+ ErrorKind::RateLimited
37
+ | ErrorKind::NetworkError
38
+ | ErrorKind::ServerError
39
+ | ErrorKind::Unknown
40
+ )
41
+ }
42
+ }
43
+
44
+ /// Classifies an error string into an `ErrorKind`.
45
+ ///
46
+ /// Uses heuristic keyword matching on error messages.
47
+ /// This is intentionally simple — real providers should return structured errors.
48
+ pub fn classify_error(error_msg: &str) -> ErrorKind {
49
+ let lower = error_msg.to_lowercase();
50
+
51
+ if lower.contains("rate limit") || lower.contains("429") || lower.contains("too many requests")
52
+ {
53
+ ErrorKind::RateLimited
54
+ } else if lower.contains("connection")
55
+ || lower.contains("timeout")
56
+ || lower.contains("network")
57
+ || lower.contains("dns")
58
+ {
59
+ ErrorKind::NetworkError
60
+ } else if lower.contains("500")
61
+ || lower.contains("502")
62
+ || lower.contains("503")
63
+ || lower.contains("internal server error")
64
+ || lower.contains("service unavailable")
65
+ {
66
+ ErrorKind::ServerError
67
+ } else if lower.contains("auth")
68
+ || lower.contains("unauthorized")
69
+ || lower.contains("401")
70
+ || lower.contains("403")
71
+ || lower.contains("forbidden")
72
+ || lower.contains("invalid api key")
73
+ {
74
+ ErrorKind::AuthError
75
+ } else if lower.contains("context")
76
+ && (lower.contains("length") || lower.contains("window") || lower.contains("exceeded"))
77
+ {
78
+ ErrorKind::ContextOverflow
79
+ } else if lower.contains("not found") && lower.contains("tool") {
80
+ ErrorKind::ToolNotFound
81
+ } else if lower.contains("invalid")
82
+ || lower.contains("malformed")
83
+ || lower.contains("400")
84
+ || lower.contains("bad request")
85
+ {
86
+ ErrorKind::InvalidRequest
87
+ } else {
88
+ ErrorKind::Unknown
89
+ }
90
+ }
91
+
92
+ /// Manages retry state and computes backoff delays.
93
+ pub struct RetryHandler {
94
+ config: RetryConfig,
95
+ attempt: u32,
96
+ }
97
+
98
+ impl RetryHandler {
99
+ /// Create a new retry handler from config.
100
+ pub fn new(config: RetryConfig) -> Self {
101
+ Self { config, attempt: 0 }
102
+ }
103
+
104
+ /// Check if we should retry the given error.
105
+ /// Returns `Some(delay)` if retryable, `None` if fatal or max retries exceeded.
106
+ pub fn should_retry(&mut self, error_msg: &str) -> Option<Duration> {
107
+ let kind = classify_error(error_msg);
108
+
109
+ if !kind.is_retryable() {
110
+ tracing::warn!(
111
+ error_kind = ?kind,
112
+ "Non-retryable error, failing immediately"
113
+ );
114
+ return None;
115
+ }
116
+
117
+ if self.attempt >= self.config.max_retries {
118
+ tracing::warn!(
119
+ attempt = self.attempt,
120
+ max = self.config.max_retries,
121
+ "Max retries exceeded"
122
+ );
123
+ return None;
124
+ }
125
+
126
+ let delay = self.next_delay();
127
+ self.attempt += 1;
128
+
129
+ tracing::info!(
130
+ attempt = self.attempt,
131
+ delay_ms = delay.as_millis() as u64,
132
+ error_kind = ?kind,
133
+ "Retrying after transient error"
134
+ );
135
+
136
+ Some(delay)
137
+ }
138
+
139
+ /// Compute the next backoff delay using exponential backoff.
140
+ fn next_delay(&self) -> Duration {
141
+ let base = self.config.initial_delay.as_millis() as f64;
142
+ let multiplied = base * self.config.backoff_multiplier.powi(self.attempt as i32);
143
+ let capped = multiplied.min(self.config.max_delay.as_millis() as f64);
144
+ Duration::from_millis(capped as u64)
145
+ }
146
+
147
+ /// Reset retry state (call after a successful operation).
148
+ pub fn reset(&mut self) {
149
+ self.attempt = 0;
150
+ }
151
+ }
152
+
153
+ #[cfg(test)]
154
+ mod tests {
155
+ use super::*;
156
+
157
+ #[test]
158
+ fn test_classify_rate_limit() {
159
+ assert_eq!(
160
+ classify_error("Rate limit exceeded"),
161
+ ErrorKind::RateLimited
162
+ );
163
+ assert_eq!(
164
+ classify_error("HTTP 429 Too Many Requests"),
165
+ ErrorKind::RateLimited
166
+ );
167
+ }
168
+
169
+ #[test]
170
+ fn test_classify_network() {
171
+ assert_eq!(
172
+ classify_error("Connection refused"),
173
+ ErrorKind::NetworkError
174
+ );
175
+ assert_eq!(
176
+ classify_error("Request timeout after 30s"),
177
+ ErrorKind::NetworkError
178
+ );
179
+ }
180
+
181
+ #[test]
182
+ fn test_classify_auth() {
183
+ assert_eq!(classify_error("401 Unauthorized"), ErrorKind::AuthError);
184
+ assert_eq!(
185
+ classify_error("Invalid API key provided"),
186
+ ErrorKind::AuthError
187
+ );
188
+ }
189
+
190
+ #[test]
191
+ fn test_classify_context_overflow() {
192
+ assert_eq!(
193
+ classify_error("Context length exceeded: 128000 tokens"),
194
+ ErrorKind::ContextOverflow
195
+ );
196
+ }
197
+
198
+ #[test]
199
+ fn test_classify_unknown() {
200
+ assert_eq!(
201
+ classify_error("Something weird happened"),
202
+ ErrorKind::Unknown
203
+ );
204
+ }
205
+
206
+ #[test]
207
+ fn test_retryable() {
208
+ assert!(ErrorKind::RateLimited.is_retryable());
209
+ assert!(ErrorKind::NetworkError.is_retryable());
210
+ assert!(ErrorKind::ServerError.is_retryable());
211
+ assert!(ErrorKind::Unknown.is_retryable());
212
+ assert!(!ErrorKind::AuthError.is_retryable());
213
+ assert!(!ErrorKind::InvalidRequest.is_retryable());
214
+ assert!(!ErrorKind::ContextOverflow.is_retryable());
215
+ }
216
+
217
+ #[test]
218
+ fn test_retry_handler_backoff() {
219
+ let config = RetryConfig {
220
+ max_retries: 3,
221
+ initial_delay: Duration::from_secs(1),
222
+ max_delay: Duration::from_secs(30),
223
+ backoff_multiplier: 2.0,
224
+ };
225
+ let mut handler = RetryHandler::new(config);
226
+
227
+ // First retry: 1s
228
+ let delay = handler.should_retry("rate limit exceeded").unwrap();
229
+ assert_eq!(delay, Duration::from_secs(1));
230
+
231
+ // Second retry: 2s
232
+ let delay = handler.should_retry("rate limit exceeded").unwrap();
233
+ assert_eq!(delay, Duration::from_secs(2));
234
+
235
+ // Third retry: 4s
236
+ let delay = handler.should_retry("rate limit exceeded").unwrap();
237
+ assert_eq!(delay, Duration::from_secs(4));
238
+
239
+ // Fourth: exceeded max_retries=3
240
+ assert!(handler.should_retry("rate limit exceeded").is_none());
241
+ }
242
+
243
+ #[test]
244
+ fn test_retry_handler_non_retryable() {
245
+ let mut handler = RetryHandler::new(RetryConfig::default());
246
+ assert!(handler.should_retry("401 Unauthorized").is_none());
247
+ }
248
+
249
+ #[test]
250
+ fn test_retry_handler_reset() {
251
+ let mut handler = RetryHandler::new(RetryConfig {
252
+ max_retries: 1,
253
+ ..Default::default()
254
+ });
255
+
256
+ handler.should_retry("rate limit").unwrap();
257
+ assert!(handler.should_retry("rate limit").is_none());
258
+
259
+ handler.reset();
260
+ assert!(handler.should_retry("rate limit").is_some());
261
+ }
262
+ }