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