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.
- package/.env.example +20 -0
- package/.github/workflows/ci.yml +70 -0
- package/.github/workflows/publish.yml +250 -0
- package/.gitmodules +3 -0
- package/Cargo.lock +3584 -0
- package/Cargo.toml +97 -0
- package/crates/enact/Cargo.toml +27 -0
- package/crates/enact/src/lib.rs +60 -0
- package/crates/enact-a2a/Cargo.toml +25 -0
- package/crates/enact-a2a/src/lib.rs +411 -0
- package/crates/enact-channels/Cargo.toml +64 -0
- package/crates/enact-channels/examples/README.md +80 -0
- package/crates/enact-channels/examples/channel_bot.rs +169 -0
- package/crates/enact-channels/examples/telegram-echo.rs +34 -0
- package/crates/enact-channels/examples/whatsapp-echo.rs +142 -0
- package/crates/enact-channels/src/config.rs +213 -0
- package/crates/enact-channels/src/lib.rs +25 -0
- package/crates/enact-channels/src/runtime.rs +237 -0
- package/crates/enact-channels/src/security/mod.rs +5 -0
- package/crates/enact-channels/src/security/pairing.rs +205 -0
- package/crates/enact-channels/src/teams.rs +601 -0
- package/crates/enact-channels/src/telegram.rs +2833 -0
- package/crates/enact-channels/src/traits.rs +200 -0
- package/crates/enact-channels/src/webhook.rs +262 -0
- package/crates/enact-channels/src/whatsapp.rs +310 -0
- package/crates/enact-cli/Cargo.toml +40 -0
- package/crates/enact-cli/src/commands/doctor.rs +62 -0
- package/crates/enact-cli/src/commands/mod.rs +3 -0
- package/crates/enact-cli/src/commands/run.rs +69 -0
- package/crates/enact-cli/src/commands/serve.rs +81 -0
- package/crates/enact-cli/src/config.rs +2 -0
- package/crates/enact-cli/src/main.rs +79 -0
- package/crates/enact-config/Cargo.toml +36 -0
- package/crates/enact-config/ENV_VAR_MAPPING.md +135 -0
- package/crates/enact-config/QUICK_REFERENCE.md +92 -0
- package/crates/enact-config/README.md +107 -0
- package/crates/enact-config/TESTING.md +161 -0
- package/crates/enact-config/examples/test-env-vars.rs +100 -0
- package/crates/enact-config/src/config.rs +399 -0
- package/crates/enact-config/src/encrypted_store.rs +211 -0
- package/crates/enact-config/src/lib.rs +298 -0
- package/crates/enact-config/src/secrets.rs +149 -0
- package/crates/enact-config/src/sync.rs +260 -0
- package/crates/enact-config/test-env-vars.sh +34 -0
- package/crates/enact-config/tests/README.md +99 -0
- package/crates/enact-config/tests/config_integration_test.rs +202 -0
- package/crates/enact-config/tests/security_test.rs +140 -0
- package/crates/enact-context/Cargo.toml +41 -0
- package/crates/enact-context/src/budget.rs +314 -0
- package/crates/enact-context/src/calibrator.rs +535 -0
- package/crates/enact-context/src/compactor.rs +392 -0
- package/crates/enact-context/src/condenser.rs +826 -0
- package/crates/enact-context/src/lib.rs +94 -0
- package/crates/enact-context/src/segment.rs +238 -0
- package/crates/enact-context/src/step_context.rs +645 -0
- package/crates/enact-context/src/token_counter.rs +148 -0
- package/crates/enact-context/src/window.rs +372 -0
- package/crates/enact-core/Cargo.toml +42 -0
- package/crates/enact-core/README.md +98 -0
- package/crates/enact-core/src/background/executor.rs +524 -0
- package/crates/enact-core/src/background/mod.rs +48 -0
- package/crates/enact-core/src/background/target_binding.rs +390 -0
- package/crates/enact-core/src/background/trigger.rs +511 -0
- package/crates/enact-core/src/callable/callable.rs +152 -0
- package/crates/enact-core/src/callable/composite.rs +817 -0
- package/crates/enact-core/src/callable/graph.rs +104 -0
- package/crates/enact-core/src/callable/llm.rs +211 -0
- package/crates/enact-core/src/callable/mod.rs +64 -0
- package/crates/enact-core/src/callable/registry.rs +206 -0
- package/crates/enact-core/src/context/execution_context.rs +757 -0
- package/crates/enact-core/src/context/invocation.rs +99 -0
- package/crates/enact-core/src/context/mod.rs +50 -0
- package/crates/enact-core/src/context/tenant.rs +175 -0
- package/crates/enact-core/src/context/trace.rs +127 -0
- package/crates/enact-core/src/flow/conditional.rs +293 -0
- package/crates/enact-core/src/flow/mod.rs +43 -0
- package/crates/enact-core/src/flow/parallel.rs +437 -0
- package/crates/enact-core/src/flow/repeat.rs +534 -0
- package/crates/enact-core/src/flow/sequential.rs +248 -0
- package/crates/enact-core/src/graph/checkpoint.rs +79 -0
- package/crates/enact-core/src/graph/checkpoint_store.rs +76 -0
- package/crates/enact-core/src/graph/compiled.rs +189 -0
- package/crates/enact-core/src/graph/edge.rs +59 -0
- package/crates/enact-core/src/graph/graph_schema.rs +218 -0
- package/crates/enact-core/src/graph/loader.rs +155 -0
- package/crates/enact-core/src/graph/mod.rs +18 -0
- package/crates/enact-core/src/graph/node/function.rs +49 -0
- package/crates/enact-core/src/graph/node/mod.rs +48 -0
- package/crates/enact-core/src/graph/schema.rs +62 -0
- package/crates/enact-core/src/inbox/message.rs +405 -0
- package/crates/enact-core/src/inbox/mod.rs +31 -0
- package/crates/enact-core/src/inbox/store.rs +355 -0
- package/crates/enact-core/src/kernel/artifact/filesystem.rs +546 -0
- package/crates/enact-core/src/kernel/artifact/metadata.rs +283 -0
- package/crates/enact-core/src/kernel/artifact/mod.rs +27 -0
- package/crates/enact-core/src/kernel/artifact/store.rs +427 -0
- package/crates/enact-core/src/kernel/enforcement.rs +1315 -0
- package/crates/enact-core/src/kernel/error.rs +1200 -0
- package/crates/enact-core/src/kernel/event.rs +1394 -0
- package/crates/enact-core/src/kernel/execution_model.rs +831 -0
- package/crates/enact-core/src/kernel/execution_state.rs +189 -0
- package/crates/enact-core/src/kernel/execution_strategy.rs +117 -0
- package/crates/enact-core/src/kernel/ids.rs +2086 -0
- package/crates/enact-core/src/kernel/interrupt.rs +125 -0
- package/crates/enact-core/src/kernel/kernel.rs +1283 -0
- package/crates/enact-core/src/kernel/mod.rs +205 -0
- package/crates/enact-core/src/kernel/persistence/event_store.rs +270 -0
- package/crates/enact-core/src/kernel/persistence/message_store.rs +908 -0
- package/crates/enact-core/src/kernel/persistence/mod.rs +102 -0
- package/crates/enact-core/src/kernel/persistence/state_store.rs +228 -0
- package/crates/enact-core/src/kernel/persistence/vector_store.rs +299 -0
- package/crates/enact-core/src/kernel/reducer.rs +808 -0
- package/crates/enact-core/src/kernel/replay.rs +153 -0
- package/crates/enact-core/src/lib.rs +413 -0
- package/crates/enact-core/src/memory/episodic.rs +0 -0
- package/crates/enact-core/src/memory/mod.rs +6 -0
- package/crates/enact-core/src/memory/semantic.rs +0 -0
- package/crates/enact-core/src/memory/trait.rs +0 -0
- package/crates/enact-core/src/memory/vector_db.rs +0 -0
- package/crates/enact-core/src/memory/working.rs +0 -0
- package/crates/enact-core/src/policy/execution_policy.rs +292 -0
- package/crates/enact-core/src/policy/filters.rs +458 -0
- package/crates/enact-core/src/policy/input_processor.rs +407 -0
- package/crates/enact-core/src/policy/long_running.rs +134 -0
- package/crates/enact-core/src/policy/mod.rs +193 -0
- package/crates/enact-core/src/policy/pii_input.rs +274 -0
- package/crates/enact-core/src/policy/tenant_policy.rs +453 -0
- package/crates/enact-core/src/policy/tool_policy.rs +407 -0
- package/crates/enact-core/src/providers/mod.rs +63 -0
- package/crates/enact-core/src/providers/trait.rs +292 -0
- package/crates/enact-core/src/runner/callbacks.rs +6 -0
- package/crates/enact-core/src/runner/execution_runner.rs +476 -0
- package/crates/enact-core/src/runner/loop.rs +117 -0
- package/crates/enact-core/src/runner/mod.rs +58 -0
- package/crates/enact-core/src/runner/protected_runner.rs +280 -0
- package/crates/enact-core/src/signal/inmemory.rs +231 -0
- package/crates/enact-core/src/signal/mod.rs +108 -0
- package/crates/enact-core/src/streaming/event_logger.rs +195 -0
- package/crates/enact-core/src/streaming/event_stream.rs +1423 -0
- package/crates/enact-core/src/streaming/mod.rs +108 -0
- package/crates/enact-core/src/streaming/pause_cancel.rs +0 -0
- package/crates/enact-core/src/streaming/protected_emitter.rs +173 -0
- package/crates/enact-core/src/streaming/protection/context.rs +136 -0
- package/crates/enact-core/src/streaming/protection/encryption.rs +289 -0
- package/crates/enact-core/src/streaming/protection/mod.rs +43 -0
- package/crates/enact-core/src/streaming/protection/pii_protection.rs +243 -0
- package/crates/enact-core/src/streaming/protection/processor.rs +166 -0
- package/crates/enact-core/src/streaming/sse.rs +0 -0
- package/crates/enact-core/src/telemetry/exporter.rs +0 -0
- package/crates/enact-core/src/telemetry/init.rs +0 -0
- package/crates/enact-core/src/telemetry/mod.rs +49 -0
- package/crates/enact-core/src/telemetry/spans.rs +245 -0
- package/crates/enact-core/src/tool/agent_tool.rs +177 -0
- package/crates/enact-core/src/tool/browser/mod.rs +0 -0
- package/crates/enact-core/src/tool/browser/webdriver.rs +0 -0
- package/crates/enact-core/src/tool/cost.rs +247 -0
- package/crates/enact-core/src/tool/discovery.rs +0 -0
- package/crates/enact-core/src/tool/dispatcher.rs +347 -0
- package/crates/enact-core/src/tool/filesystem.rs +231 -0
- package/crates/enact-core/src/tool/function.rs +99 -0
- package/crates/enact-core/src/tool/git.rs +162 -0
- package/crates/enact-core/src/tool/http.rs +214 -0
- package/crates/enact-core/src/tool/mcp/client.rs +0 -0
- package/crates/enact-core/src/tool/mcp/mod.rs +0 -0
- package/crates/enact-core/src/tool/mod.rs +51 -0
- package/crates/enact-core/src/tool/reasoning/debugging.rs +0 -0
- package/crates/enact-core/src/tool/reasoning/mcts.rs +0 -0
- package/crates/enact-core/src/tool/reasoning/mod.rs +0 -0
- package/crates/enact-core/src/tool/reasoning/sequential.rs +0 -0
- package/crates/enact-core/src/tool/sandbox/dagger.rs +0 -0
- package/crates/enact-core/src/tool/sandbox/mod.rs +0 -0
- package/crates/enact-core/src/tool/shell.rs +147 -0
- package/crates/enact-core/src/tool/trait.rs +33 -0
- package/crates/enact-core/src/tool/web_search.rs +277 -0
- package/crates/enact-core/src/util/config.rs +0 -0
- package/crates/enact-core/src/util/errors.rs +0 -0
- package/crates/enact-core/src/util/mod.rs +6 -0
- package/crates/enact-core/tests/airgapped_e2e_test.rs +291 -0
- package/crates/enact-core/tests/e2e_agentic_loop.rs +119 -0
- package/crates/enact-core/tests/e2e_test.rs +259 -0
- package/crates/enact-core/tests/graph_test.rs +130 -0
- package/crates/enact-core/tests/stream_event_id_validation.rs +435 -0
- package/crates/enact-cron/Cargo.toml +28 -0
- package/crates/enact-cron/src/lib.rs +44 -0
- package/crates/enact-cron/src/schedule.rs +156 -0
- package/crates/enact-cron/src/store.rs +589 -0
- package/crates/enact-cron/src/types.rs +148 -0
- package/crates/enact-gateway/Cargo.toml +31 -0
- package/crates/enact-gateway/README.md +30 -0
- package/crates/enact-gateway/examples/whatsapp-gateway-runner-mock.rs +59 -0
- package/crates/enact-gateway/examples/whatsapp-gateway.rs +42 -0
- package/crates/enact-gateway/src/lib.rs +582 -0
- package/crates/enact-mcp/Cargo.toml +24 -0
- package/crates/enact-mcp/src/lib.rs +178 -0
- package/crates/enact-memory/Cargo.toml +25 -0
- package/crates/enact-memory/src/backend.rs +20 -0
- package/crates/enact-memory/src/chunker.rs +230 -0
- package/crates/enact-memory/src/embeddings.rs +221 -0
- package/crates/enact-memory/src/lib.rs +67 -0
- package/crates/enact-memory/src/markdown.rs +127 -0
- package/crates/enact-memory/src/none.rs +61 -0
- package/crates/enact-memory/src/sqlite.rs +276 -0
- package/crates/enact-memory/src/traits.rs +65 -0
- package/crates/enact-memory/src/vector.rs +198 -0
- package/crates/enact-oauth/Cargo.toml +27 -0
- package/crates/enact-oauth/src/lib.rs +584 -0
- package/crates/enact-observability/Cargo.toml +22 -0
- package/crates/enact-observability/src/lib.rs +197 -0
- package/crates/enact-providers/Cargo.toml +33 -0
- package/crates/enact-providers/examples/hello-agent.rs +33 -0
- package/crates/enact-providers/src/anthropic.rs +182 -0
- package/crates/enact-providers/src/azure.rs +96 -0
- package/crates/enact-providers/src/bridge.rs +221 -0
- package/crates/enact-providers/src/gemini.rs +227 -0
- package/crates/enact-providers/src/http.rs +78 -0
- package/crates/enact-providers/src/lib.rs +53 -0
- package/crates/enact-providers/src/openai_compatible.rs +167 -0
- package/crates/enact-providers/src/openrouter.rs +33 -0
- package/crates/enact-runner/Cargo.toml +24 -0
- package/crates/enact-runner/README.md +76 -0
- package/crates/enact-runner/src/compaction.rs +225 -0
- package/crates/enact-runner/src/config.rs +118 -0
- package/crates/enact-runner/src/lib.rs +63 -0
- package/crates/enact-runner/src/loop_driver.rs +414 -0
- package/crates/enact-runner/src/parser.rs +421 -0
- package/crates/enact-runner/src/retry.rs +262 -0
- package/crates/enact-runner/tests/integration.rs +278 -0
- package/crates/enact-security/Cargo.toml +22 -0
- package/crates/enact-security/src/audit.rs +375 -0
- package/crates/enact-security/src/lib.rs +37 -0
- package/crates/enact-security/src/policy.rs +406 -0
- package/crates/enact-skills/Cargo.toml +25 -0
- package/crates/enact-skills/src/lib.rs +506 -0
- package/crates/enact-tools/Cargo.toml +22 -0
- package/crates/enact-tools/src/file_read.rs +166 -0
- package/crates/enact-tools/src/file_write.rs +216 -0
- package/crates/enact-tools/src/git_operations.rs +513 -0
- package/crates/enact-tools/src/http_request.rs +417 -0
- package/crates/enact-tools/src/lib.rs +104 -0
- package/crates/enact-tools/src/security.rs +227 -0
- package/crates/enact-tools/src/shell.rs +191 -0
- package/crates/enact-tools/src/traits.rs +159 -0
- package/docs/Makefile +74 -0
- package/docs/config.toml +62 -0
- package/docs/content/_index.md +174 -0
- package/docs/content/a2a/_index.md +431 -0
- package/docs/content/api/_index.md +323 -0
- package/docs/content/channels/_index.md +160 -0
- package/docs/content/channels/teams.md +205 -0
- package/docs/content/channels/telegram.md +182 -0
- package/docs/content/channels/webhook.md +423 -0
- package/docs/content/channels/whatsapp.md +240 -0
- package/docs/content/cli/_index.md +261 -0
- package/docs/content/concepts/_index.md +273 -0
- package/docs/content/configuration/_index.md +241 -0
- package/docs/content/cron/_index.md +248 -0
- package/docs/content/developers/_index.md +278 -0
- package/docs/content/getting-started/_index.md +180 -0
- package/docs/content/installation/_index.md +186 -0
- package/docs/content/installation/uninstall.md +101 -0
- package/docs/content/installation/updating.md +120 -0
- package/docs/content/mcp/_index.md +215 -0
- package/docs/content/memory/_index.md +163 -0
- package/docs/content/oauth/_index.md +515 -0
- package/docs/content/providers/_index.md +206 -0
- package/docs/content/roadmap/_index.md +199 -0
- package/docs/content/security/_index.md +219 -0
- package/docs/content/skills/_index.md +228 -0
- package/docs/content/tools/_index.md +485 -0
- package/docs/content/troubleshooting/_index.md +259 -0
- package/docs/content/yaml-schema/_index.md +294 -0
- package/docs/static/giallo-dark.css +91 -0
- package/docs/static/giallo-light.css +91 -0
- package/docs/themes/tanuki/.github/workflows/deploy.yml +44 -0
- package/docs/themes/tanuki/LICENSE +21 -0
- package/docs/themes/tanuki/README.md +166 -0
- package/docs/themes/tanuki/examples/blog/config.toml +58 -0
- package/docs/themes/tanuki/examples/blog/content/_index.md +4 -0
- package/docs/themes/tanuki/examples/blog/content/about.md +33 -0
- package/docs/themes/tanuki/examples/blog/content/blog/_index.md +7 -0
- package/docs/themes/tanuki/examples/blog/content/blog/api-design-best-practices.md +245 -0
- package/docs/themes/tanuki/examples/blog/content/blog/building-accessible-websites.md +147 -0
- package/docs/themes/tanuki/examples/blog/content/blog/css-grid-vs-flexbox.md +165 -0
- package/docs/themes/tanuki/examples/blog/content/blog/customizing-catppuccin-colors.md +137 -0
- package/docs/themes/tanuki/examples/blog/content/blog/dark-mode-best-practices.md +82 -0
- package/docs/themes/tanuki/examples/blog/content/blog/docker-essentials.md +301 -0
- package/docs/themes/tanuki/examples/blog/content/blog/getting-started-with-zola.md +129 -0
- package/docs/themes/tanuki/examples/blog/content/blog/git-workflow-for-content.md +112 -0
- package/docs/themes/tanuki/examples/blog/content/blog/introduction-to-webassembly.md +183 -0
- package/docs/themes/tanuki/examples/blog/content/blog/modern-javascript-features.md +234 -0
- package/docs/themes/tanuki/examples/blog/content/blog/testing-strategies.md +311 -0
- package/docs/themes/tanuki/examples/blog/content/blog/typography-for-developers.md +104 -0
- package/docs/themes/tanuki/examples/blog/content/blog/welcome-to-tanuki.md +67 -0
- package/docs/themes/tanuki/examples/blog/content/blog/why-static-sites.md +85 -0
- package/docs/themes/tanuki/examples/blog/content/projects.md +64 -0
- package/docs/themes/tanuki/examples/book/config.toml +17 -0
- package/docs/themes/tanuki/examples/book/content/_index.md +12 -0
- package/docs/themes/tanuki/examples/book/content/chapter-1.md +90 -0
- package/docs/themes/tanuki/examples/book/content/chapter-2.md +143 -0
- package/docs/themes/tanuki/examples/book/content/chapter-3.md +217 -0
- package/docs/themes/tanuki/examples/book/content/chapter-4.md +224 -0
- package/docs/themes/tanuki/examples/book/content/chapter-5.md +297 -0
- package/docs/themes/tanuki/examples/book/content/print.md +6 -0
- package/docs/themes/tanuki/examples/docs/config.toml +28 -0
- package/docs/themes/tanuki/examples/docs/content/_index.md +20 -0
- package/docs/themes/tanuki/examples/docs/content/components.md +156 -0
- package/docs/themes/tanuki/examples/docs/content/configuration.md +94 -0
- package/docs/themes/tanuki/examples/docs/content/customization.md +202 -0
- package/docs/themes/tanuki/examples/docs/content/deployment.md +204 -0
- package/docs/themes/tanuki/examples/docs/content/installation.md +59 -0
- package/docs/themes/tanuki/examples/docs/content/print.md +6 -0
- package/docs/themes/tanuki/examples/docs/static/img/tanuki-icon.avif +0 -0
- package/docs/themes/tanuki/examples/index.html +2104 -0
- package/docs/themes/tanuki/mise.toml +108 -0
- package/docs/themes/tanuki/sass/base/_catppuccin.scss +164 -0
- package/docs/themes/tanuki/sass/base/_fonts.scss +64 -0
- package/docs/themes/tanuki/sass/base/_reset.scss +152 -0
- package/docs/themes/tanuki/sass/base/_typography.scss +523 -0
- package/docs/themes/tanuki/sass/components/_buttons.scss +209 -0
- package/docs/themes/tanuki/sass/components/_code.scss +457 -0
- package/docs/themes/tanuki/sass/components/_landing.scss +633 -0
- package/docs/themes/tanuki/sass/components/_layout.scss +294 -0
- package/docs/themes/tanuki/sass/components/_navigation.scss +1200 -0
- package/docs/themes/tanuki/sass/components/_print.scss +237 -0
- package/docs/themes/tanuki/sass/components/_search.scss +224 -0
- package/docs/themes/tanuki/sass/components/_sidebar.scss +473 -0
- package/docs/themes/tanuki/sass/components/_theme-toggle.scss +186 -0
- package/docs/themes/tanuki/sass/modes/_blog.scss +366 -0
- package/docs/themes/tanuki/sass/modes/_product.scss +875 -0
- package/docs/themes/tanuki/sass/modes/_raskell.scss +1696 -0
- package/docs/themes/tanuki/sass/patterns/_buttons.scss +183 -0
- package/docs/themes/tanuki/sass/patterns/_cards.scss +144 -0
- package/docs/themes/tanuki/sass/patterns/_index.scss +9 -0
- package/docs/themes/tanuki/sass/patterns/_lists.scss +259 -0
- package/docs/themes/tanuki/sass/patterns/_sections.scss +243 -0
- package/docs/themes/tanuki/sass/style.scss +47 -0
- package/docs/themes/tanuki/sass/tokens/_colors.scss +139 -0
- package/docs/themes/tanuki/sass/tokens/_spacing.scss +100 -0
- package/docs/themes/tanuki/sass/tokens/_typography.scss +186 -0
- package/docs/themes/tanuki/screenshot.png +0 -0
- package/docs/themes/tanuki/sentinel.kdl +59 -0
- package/docs/themes/tanuki/static/elasticlunr.min.js +10 -0
- package/docs/themes/tanuki/static/fonts/GEIST-LICENSE.txt +92 -0
- package/docs/themes/tanuki/static/fonts/Geist-Variable.woff2 +0 -0
- package/docs/themes/tanuki/static/fonts/GeistMono-Variable.woff2 +0 -0
- package/docs/themes/tanuki/static/img/tanuki-icon.avif +0 -0
- package/docs/themes/tanuki/static/img/tanuki-icon.png +0 -0
- package/docs/themes/tanuki/static/js/anchors.js +18 -0
- package/docs/themes/tanuki/static/js/app.js +274 -0
- package/docs/themes/tanuki/static/js/code.js +394 -0
- package/docs/themes/tanuki/static/js/navigation.js +778 -0
- package/docs/themes/tanuki/static/js/scroll-to-top.js +33 -0
- package/docs/themes/tanuki/static/js/search-raskell.js +240 -0
- package/docs/themes/tanuki/static/js/search.js +215 -0
- package/docs/themes/tanuki/static/js/theme.js +169 -0
- package/docs/themes/tanuki/static/syntax-dark.css +151 -0
- package/docs/themes/tanuki/static/syntax-light.css +151 -0
- package/docs/themes/tanuki/static/wasm/sentinel_playground_wasm.js +486 -0
- package/docs/themes/tanuki/static/wasm/sentinel_playground_wasm_bg.wasm +0 -0
- package/docs/themes/tanuki/templates/404.html +52 -0
- package/docs/themes/tanuki/templates/base.html +428 -0
- package/docs/themes/tanuki/templates/blog.html +66 -0
- package/docs/themes/tanuki/templates/home.html +108 -0
- package/docs/themes/tanuki/templates/index.html +178 -0
- package/docs/themes/tanuki/templates/landing.html +168 -0
- package/docs/themes/tanuki/templates/macros/nav.html +128 -0
- package/docs/themes/tanuki/templates/macros/posts.html +101 -0
- package/docs/themes/tanuki/templates/macros/ui.html +159 -0
- package/docs/themes/tanuki/templates/page.html +135 -0
- package/docs/themes/tanuki/templates/partials/footer.html +38 -0
- package/docs/themes/tanuki/templates/partials/header.html +366 -0
- package/docs/themes/tanuki/templates/partials/nav-buttons.html +55 -0
- package/docs/themes/tanuki/templates/partials/nav-overlay.html +81 -0
- package/docs/themes/tanuki/templates/partials/page-toc-panel.html +43 -0
- package/docs/themes/tanuki/templates/partials/search.html +52 -0
- package/docs/themes/tanuki/templates/partials/sidebar.html +107 -0
- package/docs/themes/tanuki/templates/partials/theme-toggle.html +35 -0
- package/docs/themes/tanuki/templates/partials/toc-overlay.html +146 -0
- package/docs/themes/tanuki/templates/partials/version-picker.html +38 -0
- package/docs/themes/tanuki/templates/print.html +244 -0
- package/docs/themes/tanuki/templates/section.html +186 -0
- package/docs/themes/tanuki/templates/taxonomy_list.html +18 -0
- package/docs/themes/tanuki/templates/taxonomy_single.html +31 -0
- package/docs/themes/tanuki/theme.toml +58 -0
- package/examples/hello-agent.rs +55 -0
- package/package.json +36 -0
- package/proto/config.proto +60 -0
- package/proto/events.proto +0 -0
- 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
|
+
}
|