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,1423 @@
|
|
|
1
|
+
//! Streaming Event Protocol
|
|
2
|
+
//!
|
|
3
|
+
//! Standardized event protocol for real-time streaming using SSE (Server-Sent Events).
|
|
4
|
+
//! All events use `data-*` prefix for AI SDK UI compatibility.
|
|
5
|
+
//!
|
|
6
|
+
//! ## Protocol Categories
|
|
7
|
+
//!
|
|
8
|
+
//! ### Standard Text Events (AI SDK compatible)
|
|
9
|
+
//! - `data-text-start` - Start of text generation
|
|
10
|
+
//! - `data-text-delta` - Incremental text chunk
|
|
11
|
+
//! - `data-text-end` - End of text generation
|
|
12
|
+
//!
|
|
13
|
+
//! ### Standard Step Events (AI SDK compatible)
|
|
14
|
+
//! - `data-start-step` - Step boundary start
|
|
15
|
+
//! - `data-finish-step` - Step boundary end
|
|
16
|
+
//!
|
|
17
|
+
//! ### Standard Tool Events (AI SDK compatible)
|
|
18
|
+
//! - `data-tool-input-start` - Tool call started
|
|
19
|
+
//! - `data-tool-input-delta` - Tool input streaming
|
|
20
|
+
//! - `data-tool-input-available` - Tool input complete
|
|
21
|
+
//! - `data-tool-output-available` - Tool result available
|
|
22
|
+
//!
|
|
23
|
+
//! ### Standard Lifecycle Events (AI SDK compatible)
|
|
24
|
+
//! - `data-start` - Message/stream start
|
|
25
|
+
//! - `data-finish` - Message/stream finish
|
|
26
|
+
//! - `data-error` - Error occurred
|
|
27
|
+
//!
|
|
28
|
+
//! ### Custom Execution Events (Enact-specific)
|
|
29
|
+
//! - `data-execution-start` - Execution started
|
|
30
|
+
//! - `data-execution-end` - Execution completed
|
|
31
|
+
//! - `data-execution-failed` - Execution failed
|
|
32
|
+
//! - `data-execution-paused` - Execution paused
|
|
33
|
+
//! - `data-execution-resumed` - Execution resumed
|
|
34
|
+
//! - `data-execution-cancelled` - Execution cancelled
|
|
35
|
+
//!
|
|
36
|
+
//! ### Custom Step Events (Enact-specific)
|
|
37
|
+
//! - `data-step-start` - Step started (with metadata)
|
|
38
|
+
//! - `data-step-end` - Step completed
|
|
39
|
+
//! - `data-step-failed` - Step failed
|
|
40
|
+
//!
|
|
41
|
+
//! ### Custom Artifact Events (Enact-specific)
|
|
42
|
+
//! - `data-artifact-created` - Artifact produced
|
|
43
|
+
//!
|
|
44
|
+
//! @see https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol
|
|
45
|
+
|
|
46
|
+
use super::event_logger::EventLog;
|
|
47
|
+
use crate::kernel::{ArtifactId, ExecutionError, ExecutionId, StepId, StepSourceType, StepType};
|
|
48
|
+
use crate::kernel::{ExecutionContext, ExecutionEvent, ExecutionEventType};
|
|
49
|
+
use futures::Stream;
|
|
50
|
+
use serde::{Deserialize, Serialize};
|
|
51
|
+
use std::pin::Pin;
|
|
52
|
+
use std::sync::Arc;
|
|
53
|
+
|
|
54
|
+
/// Event stream type - async stream of events
|
|
55
|
+
pub type EventStream = Pin<Box<dyn Stream<Item = StreamEvent> + Send>>;
|
|
56
|
+
|
|
57
|
+
// =============================================================================
|
|
58
|
+
// Stream Mode
|
|
59
|
+
// =============================================================================
|
|
60
|
+
|
|
61
|
+
/// StreamMode - Controls what events are included in the stream
|
|
62
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
|
63
|
+
#[serde(rename_all = "snake_case")]
|
|
64
|
+
pub enum StreamMode {
|
|
65
|
+
/// Full event stream - all events including token-level deltas
|
|
66
|
+
#[default]
|
|
67
|
+
Full,
|
|
68
|
+
/// Summary mode - only major events (step start/end, completion)
|
|
69
|
+
Summary,
|
|
70
|
+
/// Control only - only control signals (pause/resume/cancel)
|
|
71
|
+
ControlOnly,
|
|
72
|
+
/// Silent - no streaming (for batch processing)
|
|
73
|
+
Silent,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// Stream Event - Standardized Protocol with data-* prefix
|
|
78
|
+
// =============================================================================
|
|
79
|
+
|
|
80
|
+
/// StreamEvent - All streaming events with `data-*` prefix
|
|
81
|
+
///
|
|
82
|
+
/// Events are tagged for direct SSE serialization.
|
|
83
|
+
/// UI adapters can consume these directly or transform as needed.
|
|
84
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
85
|
+
#[serde(tag = "type")]
|
|
86
|
+
pub enum StreamEvent {
|
|
87
|
+
// =========================================================================
|
|
88
|
+
// Standard AI SDK Events - Text Streaming
|
|
89
|
+
// =========================================================================
|
|
90
|
+
/// Text generation started
|
|
91
|
+
#[serde(rename = "data-text-start")]
|
|
92
|
+
TextStart {
|
|
93
|
+
id: String,
|
|
94
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
95
|
+
execution_id: Option<String>,
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/// Incremental text chunk
|
|
99
|
+
#[serde(rename = "data-text-delta")]
|
|
100
|
+
TextDelta { id: String, delta: String },
|
|
101
|
+
|
|
102
|
+
/// Text generation ended
|
|
103
|
+
#[serde(rename = "data-text-end")]
|
|
104
|
+
TextEnd { id: String },
|
|
105
|
+
|
|
106
|
+
// =========================================================================
|
|
107
|
+
// Standard AI SDK Events - Step Control
|
|
108
|
+
// =========================================================================
|
|
109
|
+
/// Step boundary start (AI SDK step concept)
|
|
110
|
+
#[serde(rename = "data-start-step")]
|
|
111
|
+
StartStep {
|
|
112
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
113
|
+
step_id: Option<String>,
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/// Step boundary end (AI SDK step concept)
|
|
117
|
+
#[serde(rename = "data-finish-step")]
|
|
118
|
+
FinishStep {
|
|
119
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
120
|
+
step_id: Option<String>,
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// =========================================================================
|
|
124
|
+
// Standard AI SDK Events - Tool Calls
|
|
125
|
+
// =========================================================================
|
|
126
|
+
/// Tool input started
|
|
127
|
+
#[serde(rename = "data-tool-input-start")]
|
|
128
|
+
ToolInputStart {
|
|
129
|
+
tool_call_id: String,
|
|
130
|
+
tool_name: String,
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/// Tool input delta (streaming input)
|
|
134
|
+
#[serde(rename = "data-tool-input-delta")]
|
|
135
|
+
ToolInputDelta {
|
|
136
|
+
tool_call_id: String,
|
|
137
|
+
input_text_delta: String,
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
/// Tool input available (complete input)
|
|
141
|
+
#[serde(rename = "data-tool-input-available")]
|
|
142
|
+
ToolInputAvailable {
|
|
143
|
+
tool_call_id: String,
|
|
144
|
+
tool_name: String,
|
|
145
|
+
input: serde_json::Value,
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/// Tool output available
|
|
149
|
+
#[serde(rename = "data-tool-output-available")]
|
|
150
|
+
ToolOutputAvailable {
|
|
151
|
+
tool_call_id: String,
|
|
152
|
+
output: serde_json::Value,
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// =========================================================================
|
|
156
|
+
// Standard AI SDK Events - Lifecycle
|
|
157
|
+
// =========================================================================
|
|
158
|
+
/// Stream/message start
|
|
159
|
+
#[serde(rename = "data-start")]
|
|
160
|
+
Start {
|
|
161
|
+
message_id: String,
|
|
162
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
163
|
+
execution_id: Option<String>,
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/// Stream/message finish
|
|
167
|
+
#[serde(rename = "data-finish")]
|
|
168
|
+
Finish {
|
|
169
|
+
message_id: String,
|
|
170
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
171
|
+
final_output: Option<String>,
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
/// Error occurred
|
|
175
|
+
#[serde(rename = "data-error")]
|
|
176
|
+
Error { error: ExecutionError },
|
|
177
|
+
|
|
178
|
+
// =========================================================================
|
|
179
|
+
// Custom Enact Events - Execution Lifecycle
|
|
180
|
+
// =========================================================================
|
|
181
|
+
/// Execution started
|
|
182
|
+
#[serde(rename = "data-execution-start")]
|
|
183
|
+
ExecutionStart {
|
|
184
|
+
execution_id: String,
|
|
185
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
186
|
+
parent_id: Option<String>,
|
|
187
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
188
|
+
parent_type: Option<String>,
|
|
189
|
+
timestamp: i64,
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/// Execution completed successfully
|
|
193
|
+
#[serde(rename = "data-execution-end")]
|
|
194
|
+
ExecutionEnd {
|
|
195
|
+
execution_id: String,
|
|
196
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
197
|
+
final_output: Option<String>,
|
|
198
|
+
duration_ms: u64,
|
|
199
|
+
timestamp: i64,
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
/// Execution failed
|
|
203
|
+
#[serde(rename = "data-execution-failed")]
|
|
204
|
+
ExecutionFailed {
|
|
205
|
+
execution_id: String,
|
|
206
|
+
error: ExecutionError,
|
|
207
|
+
timestamp: i64,
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/// Execution paused
|
|
211
|
+
#[serde(rename = "data-execution-paused")]
|
|
212
|
+
ExecutionPaused {
|
|
213
|
+
execution_id: String,
|
|
214
|
+
reason: String,
|
|
215
|
+
timestamp: i64,
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
/// Execution resumed
|
|
219
|
+
#[serde(rename = "data-execution-resumed")]
|
|
220
|
+
ExecutionResumed {
|
|
221
|
+
execution_id: String,
|
|
222
|
+
timestamp: i64,
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
/// Execution cancelled
|
|
226
|
+
#[serde(rename = "data-execution-cancelled")]
|
|
227
|
+
ExecutionCancelled {
|
|
228
|
+
execution_id: String,
|
|
229
|
+
reason: String,
|
|
230
|
+
timestamp: i64,
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
// =========================================================================
|
|
234
|
+
// Custom Enact Events - Step Lifecycle
|
|
235
|
+
// =========================================================================
|
|
236
|
+
/// Step started (with full metadata)
|
|
237
|
+
#[serde(rename = "data-step-start")]
|
|
238
|
+
StepStart {
|
|
239
|
+
execution_id: String,
|
|
240
|
+
step_id: String,
|
|
241
|
+
step_type: String,
|
|
242
|
+
name: String,
|
|
243
|
+
timestamp: i64,
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
/// Step completed
|
|
247
|
+
#[serde(rename = "data-step-end")]
|
|
248
|
+
StepEnd {
|
|
249
|
+
execution_id: String,
|
|
250
|
+
step_id: String,
|
|
251
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
252
|
+
output: Option<String>,
|
|
253
|
+
duration_ms: u64,
|
|
254
|
+
timestamp: i64,
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/// Step failed
|
|
258
|
+
#[serde(rename = "data-step-failed")]
|
|
259
|
+
StepFailed {
|
|
260
|
+
execution_id: String,
|
|
261
|
+
step_id: String,
|
|
262
|
+
error: ExecutionError,
|
|
263
|
+
timestamp: i64,
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
/// Step discovered during execution (dynamic discovery for agentic loops)
|
|
267
|
+
#[serde(rename = "data-step-discovered")]
|
|
268
|
+
StepDiscovered {
|
|
269
|
+
execution_id: String,
|
|
270
|
+
step_id: String,
|
|
271
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
272
|
+
discovered_by: Option<String>,
|
|
273
|
+
source_type: String,
|
|
274
|
+
reason: String,
|
|
275
|
+
depth: u32,
|
|
276
|
+
timestamp: i64,
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
// =========================================================================
|
|
280
|
+
// Custom Enact Events - Artifacts
|
|
281
|
+
// =========================================================================
|
|
282
|
+
/// Artifact created
|
|
283
|
+
#[serde(rename = "data-artifact-created")]
|
|
284
|
+
ArtifactCreated {
|
|
285
|
+
execution_id: String,
|
|
286
|
+
step_id: String,
|
|
287
|
+
artifact_id: String,
|
|
288
|
+
artifact_type: String,
|
|
289
|
+
timestamp: i64,
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
// =========================================================================
|
|
293
|
+
// Custom Enact Events - State
|
|
294
|
+
// =========================================================================
|
|
295
|
+
/// State snapshot captured
|
|
296
|
+
#[serde(rename = "data-state-snapshot")]
|
|
297
|
+
StateSnapshot {
|
|
298
|
+
execution_id: String,
|
|
299
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
300
|
+
step_id: Option<String>,
|
|
301
|
+
state: serde_json::Value,
|
|
302
|
+
timestamp: i64,
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
/// Checkpoint saved (Goal-driven persistence)
|
|
306
|
+
#[serde(rename = "data-checkpoint-saved")]
|
|
307
|
+
CheckpointSaved {
|
|
308
|
+
execution_id: String,
|
|
309
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
310
|
+
step_id: Option<String>,
|
|
311
|
+
checkpoint_id: String,
|
|
312
|
+
state_hash: String,
|
|
313
|
+
timestamp: i64,
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
/// Goal evaluated
|
|
317
|
+
#[serde(rename = "data-goal-evaluated")]
|
|
318
|
+
GoalEvaluated {
|
|
319
|
+
execution_id: String,
|
|
320
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
321
|
+
step_id: Option<String>,
|
|
322
|
+
goal_id: String,
|
|
323
|
+
status: String, // "met", "not_met", "progressing"
|
|
324
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
325
|
+
score: Option<f64>,
|
|
326
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
327
|
+
reason: Option<String>,
|
|
328
|
+
timestamp: i64,
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
// =========================================================================
|
|
332
|
+
// Custom Enact Events - Inbox (Mid-Execution Guidance)
|
|
333
|
+
// =========================================================================
|
|
334
|
+
/// Inbox message received (INV-INBOX-003: audit trail)
|
|
335
|
+
#[serde(rename = "data-inbox-message")]
|
|
336
|
+
InboxMessage {
|
|
337
|
+
execution_id: String,
|
|
338
|
+
message_id: String,
|
|
339
|
+
message_type: String,
|
|
340
|
+
timestamp: i64,
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
// =========================================================================
|
|
344
|
+
// Custom Enact Events - Decisions (Audit Trail)
|
|
345
|
+
// =========================================================================
|
|
346
|
+
/// Policy decision made (audit trail for tool policy evaluation)
|
|
347
|
+
#[serde(rename = "data-policy-decision")]
|
|
348
|
+
PolicyDecision {
|
|
349
|
+
execution_id: String,
|
|
350
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
351
|
+
step_id: Option<String>,
|
|
352
|
+
tool_name: String,
|
|
353
|
+
decision: String, // "allow", "deny", "warn"
|
|
354
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
355
|
+
reason: Option<String>,
|
|
356
|
+
timestamp: i64,
|
|
357
|
+
},
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
impl StreamEvent {
|
|
361
|
+
/// Get current timestamp in milliseconds
|
|
362
|
+
fn now() -> i64 {
|
|
363
|
+
std::time::SystemTime::now()
|
|
364
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
365
|
+
.unwrap_or_default()
|
|
366
|
+
.as_millis() as i64
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/// Generate a unique ID for text/step tracking
|
|
370
|
+
fn generate_id() -> String {
|
|
371
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
372
|
+
let nanos = SystemTime::now()
|
|
373
|
+
.duration_since(UNIX_EPOCH)
|
|
374
|
+
.unwrap()
|
|
375
|
+
.as_nanos();
|
|
376
|
+
format!("id_{:x}", nanos)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// =========================================================================
|
|
380
|
+
// Standard AI SDK Event Factories
|
|
381
|
+
// =========================================================================
|
|
382
|
+
|
|
383
|
+
/// Create a text-start event
|
|
384
|
+
pub fn text_start(execution_id: Option<&ExecutionId>) -> Self {
|
|
385
|
+
Self::TextStart {
|
|
386
|
+
id: Self::generate_id(),
|
|
387
|
+
execution_id: execution_id.map(|e| e.as_str().to_string()),
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/// Create a text-delta event
|
|
392
|
+
pub fn text_delta(id: impl Into<String>, delta: impl Into<String>) -> Self {
|
|
393
|
+
Self::TextDelta {
|
|
394
|
+
id: id.into(),
|
|
395
|
+
delta: delta.into(),
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/// Create a text-end event
|
|
400
|
+
pub fn text_end(id: impl Into<String>) -> Self {
|
|
401
|
+
Self::TextEnd { id: id.into() }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/// Create a start event
|
|
405
|
+
pub fn start(message_id: impl Into<String>, execution_id: Option<&ExecutionId>) -> Self {
|
|
406
|
+
Self::Start {
|
|
407
|
+
message_id: message_id.into(),
|
|
408
|
+
execution_id: execution_id.map(|e| e.as_str().to_string()),
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/// Create a finish event
|
|
413
|
+
pub fn finish(message_id: impl Into<String>, final_output: Option<String>) -> Self {
|
|
414
|
+
Self::Finish {
|
|
415
|
+
message_id: message_id.into(),
|
|
416
|
+
final_output,
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/// Create an error event
|
|
421
|
+
pub fn error(error: ExecutionError) -> Self {
|
|
422
|
+
Self::Error { error }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/// Create a start-step event (AI SDK)
|
|
426
|
+
pub fn start_step(step_id: Option<&StepId>) -> Self {
|
|
427
|
+
Self::StartStep {
|
|
428
|
+
step_id: step_id.map(|s| s.as_str().to_string()),
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/// Create a finish-step event (AI SDK)
|
|
433
|
+
pub fn finish_step(step_id: Option<&StepId>) -> Self {
|
|
434
|
+
Self::FinishStep {
|
|
435
|
+
step_id: step_id.map(|s| s.as_str().to_string()),
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// =========================================================================
|
|
440
|
+
// Tool Event Factories
|
|
441
|
+
// =========================================================================
|
|
442
|
+
|
|
443
|
+
/// Create a tool-input-start event
|
|
444
|
+
pub fn tool_input_start(tool_call_id: impl Into<String>, tool_name: impl Into<String>) -> Self {
|
|
445
|
+
Self::ToolInputStart {
|
|
446
|
+
tool_call_id: tool_call_id.into(),
|
|
447
|
+
tool_name: tool_name.into(),
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/// Create a tool-input-available event
|
|
452
|
+
pub fn tool_input_available(
|
|
453
|
+
tool_call_id: impl Into<String>,
|
|
454
|
+
tool_name: impl Into<String>,
|
|
455
|
+
input: serde_json::Value,
|
|
456
|
+
) -> Self {
|
|
457
|
+
Self::ToolInputAvailable {
|
|
458
|
+
tool_call_id: tool_call_id.into(),
|
|
459
|
+
tool_name: tool_name.into(),
|
|
460
|
+
input,
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/// Create a tool-output-available event
|
|
465
|
+
pub fn tool_output_available(
|
|
466
|
+
tool_call_id: impl Into<String>,
|
|
467
|
+
output: serde_json::Value,
|
|
468
|
+
) -> Self {
|
|
469
|
+
Self::ToolOutputAvailable {
|
|
470
|
+
tool_call_id: tool_call_id.into(),
|
|
471
|
+
output,
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// =========================================================================
|
|
476
|
+
// Execution Event Factories
|
|
477
|
+
// =========================================================================
|
|
478
|
+
|
|
479
|
+
/// Create an execution-start event
|
|
480
|
+
pub fn execution_start(execution_id: &ExecutionId) -> Self {
|
|
481
|
+
Self::ExecutionStart {
|
|
482
|
+
execution_id: execution_id.as_str().to_string(),
|
|
483
|
+
parent_id: None,
|
|
484
|
+
parent_type: None,
|
|
485
|
+
timestamp: Self::now(),
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/// Create an execution-start event with parent
|
|
490
|
+
pub fn execution_start_with_parent(
|
|
491
|
+
execution_id: &ExecutionId,
|
|
492
|
+
parent_id: impl Into<String>,
|
|
493
|
+
parent_type: impl Into<String>,
|
|
494
|
+
) -> Self {
|
|
495
|
+
Self::ExecutionStart {
|
|
496
|
+
execution_id: execution_id.as_str().to_string(),
|
|
497
|
+
parent_id: Some(parent_id.into()),
|
|
498
|
+
parent_type: Some(parent_type.into()),
|
|
499
|
+
timestamp: Self::now(),
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/// Create an execution-end event
|
|
504
|
+
pub fn execution_end(
|
|
505
|
+
execution_id: &ExecutionId,
|
|
506
|
+
final_output: Option<String>,
|
|
507
|
+
duration_ms: u64,
|
|
508
|
+
) -> Self {
|
|
509
|
+
Self::ExecutionEnd {
|
|
510
|
+
execution_id: execution_id.as_str().to_string(),
|
|
511
|
+
final_output,
|
|
512
|
+
duration_ms,
|
|
513
|
+
timestamp: Self::now(),
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/// Create an execution-failed event
|
|
518
|
+
pub fn execution_failed(execution_id: &ExecutionId, error: ExecutionError) -> Self {
|
|
519
|
+
Self::ExecutionFailed {
|
|
520
|
+
execution_id: execution_id.as_str().to_string(),
|
|
521
|
+
error,
|
|
522
|
+
timestamp: Self::now(),
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/// Create an execution-paused event
|
|
527
|
+
pub fn execution_paused(execution_id: &ExecutionId, reason: impl Into<String>) -> Self {
|
|
528
|
+
Self::ExecutionPaused {
|
|
529
|
+
execution_id: execution_id.as_str().to_string(),
|
|
530
|
+
reason: reason.into(),
|
|
531
|
+
timestamp: Self::now(),
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/// Create an execution-resumed event
|
|
536
|
+
pub fn execution_resumed(execution_id: &ExecutionId) -> Self {
|
|
537
|
+
Self::ExecutionResumed {
|
|
538
|
+
execution_id: execution_id.as_str().to_string(),
|
|
539
|
+
timestamp: Self::now(),
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/// Create an execution-cancelled event
|
|
544
|
+
pub fn execution_cancelled(execution_id: &ExecutionId, reason: impl Into<String>) -> Self {
|
|
545
|
+
Self::ExecutionCancelled {
|
|
546
|
+
execution_id: execution_id.as_str().to_string(),
|
|
547
|
+
reason: reason.into(),
|
|
548
|
+
timestamp: Self::now(),
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// =========================================================================
|
|
553
|
+
// Step Event Factories
|
|
554
|
+
// =========================================================================
|
|
555
|
+
|
|
556
|
+
/// Create a step-start event
|
|
557
|
+
pub fn step_start(
|
|
558
|
+
execution_id: &ExecutionId,
|
|
559
|
+
step_id: &StepId,
|
|
560
|
+
step_type: StepType,
|
|
561
|
+
name: impl Into<String>,
|
|
562
|
+
) -> Self {
|
|
563
|
+
Self::StepStart {
|
|
564
|
+
execution_id: execution_id.as_str().to_string(),
|
|
565
|
+
step_id: step_id.as_str().to_string(),
|
|
566
|
+
step_type: step_type.to_string(),
|
|
567
|
+
name: name.into(),
|
|
568
|
+
timestamp: Self::now(),
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/// Create a step-end event
|
|
573
|
+
pub fn step_end(
|
|
574
|
+
execution_id: &ExecutionId,
|
|
575
|
+
step_id: &StepId,
|
|
576
|
+
output: Option<String>,
|
|
577
|
+
duration_ms: u64,
|
|
578
|
+
) -> Self {
|
|
579
|
+
Self::StepEnd {
|
|
580
|
+
execution_id: execution_id.as_str().to_string(),
|
|
581
|
+
step_id: step_id.as_str().to_string(),
|
|
582
|
+
output,
|
|
583
|
+
duration_ms,
|
|
584
|
+
timestamp: Self::now(),
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/// Create a step-failed event
|
|
589
|
+
pub fn step_failed(
|
|
590
|
+
execution_id: &ExecutionId,
|
|
591
|
+
step_id: &StepId,
|
|
592
|
+
error: ExecutionError,
|
|
593
|
+
) -> Self {
|
|
594
|
+
Self::StepFailed {
|
|
595
|
+
execution_id: execution_id.as_str().to_string(),
|
|
596
|
+
step_id: step_id.as_str().to_string(),
|
|
597
|
+
error,
|
|
598
|
+
timestamp: Self::now(),
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/// Create a step-discovered event
|
|
603
|
+
///
|
|
604
|
+
/// Emitted when a new step is dynamically discovered during agentic execution.
|
|
605
|
+
/// This enables full audit trails for discovered steps (doc-30 Resource Discovery).
|
|
606
|
+
pub fn step_discovered(
|
|
607
|
+
execution_id: &ExecutionId,
|
|
608
|
+
step_id: &StepId,
|
|
609
|
+
discovered_by: Option<&StepId>,
|
|
610
|
+
source_type: StepSourceType,
|
|
611
|
+
reason: impl Into<String>,
|
|
612
|
+
depth: u32,
|
|
613
|
+
) -> Self {
|
|
614
|
+
Self::StepDiscovered {
|
|
615
|
+
execution_id: execution_id.as_str().to_string(),
|
|
616
|
+
step_id: step_id.as_str().to_string(),
|
|
617
|
+
discovered_by: discovered_by.map(|s| s.as_str().to_string()),
|
|
618
|
+
source_type: format!("{:?}", source_type).to_lowercase(),
|
|
619
|
+
reason: reason.into(),
|
|
620
|
+
depth,
|
|
621
|
+
timestamp: Self::now(),
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// =========================================================================
|
|
626
|
+
// Artifact Event Factories
|
|
627
|
+
// =========================================================================
|
|
628
|
+
|
|
629
|
+
/// Create an artifact-created event
|
|
630
|
+
pub fn artifact_created(
|
|
631
|
+
execution_id: &ExecutionId,
|
|
632
|
+
step_id: &StepId,
|
|
633
|
+
artifact_id: &ArtifactId,
|
|
634
|
+
artifact_type: impl Into<String>,
|
|
635
|
+
) -> Self {
|
|
636
|
+
Self::ArtifactCreated {
|
|
637
|
+
execution_id: execution_id.as_str().to_string(),
|
|
638
|
+
step_id: step_id.as_str().to_string(),
|
|
639
|
+
artifact_id: artifact_id.as_str().to_string(),
|
|
640
|
+
artifact_type: artifact_type.into(),
|
|
641
|
+
timestamp: Self::now(),
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// =========================================================================
|
|
646
|
+
// Inbox Event Factories (INV-INBOX-003: Audit Trail)
|
|
647
|
+
// =========================================================================
|
|
648
|
+
|
|
649
|
+
/// Create an inbox-message event for audit trail
|
|
650
|
+
///
|
|
651
|
+
/// ## Invariant INV-INBOX-003
|
|
652
|
+
/// All inbox messages MUST emit events for audit trail.
|
|
653
|
+
pub fn inbox_message(
|
|
654
|
+
execution_id: &ExecutionId,
|
|
655
|
+
message_id: &str,
|
|
656
|
+
message_type: crate::inbox::InboxMessageType,
|
|
657
|
+
) -> Self {
|
|
658
|
+
Self::InboxMessage {
|
|
659
|
+
execution_id: execution_id.as_str().to_string(),
|
|
660
|
+
message_id: message_id.to_string(),
|
|
661
|
+
message_type: format!("{:?}", message_type).to_lowercase(),
|
|
662
|
+
timestamp: Self::now(),
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// =========================================================================
|
|
667
|
+
// Policy Decision Event Factories (Audit Trail)
|
|
668
|
+
// =========================================================================
|
|
669
|
+
|
|
670
|
+
/// Create a policy-decision event for audit trail
|
|
671
|
+
///
|
|
672
|
+
/// Records tool policy evaluation decisions for compliance and audit.
|
|
673
|
+
pub fn policy_decision_allow(
|
|
674
|
+
execution_id: &ExecutionId,
|
|
675
|
+
step_id: Option<&StepId>,
|
|
676
|
+
tool_name: impl Into<String>,
|
|
677
|
+
) -> Self {
|
|
678
|
+
Self::PolicyDecision {
|
|
679
|
+
execution_id: execution_id.as_str().to_string(),
|
|
680
|
+
step_id: step_id.map(|s| s.as_str().to_string()),
|
|
681
|
+
tool_name: tool_name.into(),
|
|
682
|
+
decision: "allow".to_string(),
|
|
683
|
+
reason: None,
|
|
684
|
+
timestamp: Self::now(),
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/// Create a policy-decision deny event
|
|
689
|
+
pub fn policy_decision_deny(
|
|
690
|
+
execution_id: &ExecutionId,
|
|
691
|
+
step_id: Option<&StepId>,
|
|
692
|
+
tool_name: impl Into<String>,
|
|
693
|
+
reason: impl Into<String>,
|
|
694
|
+
) -> Self {
|
|
695
|
+
Self::PolicyDecision {
|
|
696
|
+
execution_id: execution_id.as_str().to_string(),
|
|
697
|
+
step_id: step_id.map(|s| s.as_str().to_string()),
|
|
698
|
+
tool_name: tool_name.into(),
|
|
699
|
+
decision: "deny".to_string(),
|
|
700
|
+
reason: Some(reason.into()),
|
|
701
|
+
timestamp: Self::now(),
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/// Create a policy-decision warn event
|
|
706
|
+
pub fn policy_decision_warn(
|
|
707
|
+
execution_id: &ExecutionId,
|
|
708
|
+
step_id: Option<&StepId>,
|
|
709
|
+
tool_name: impl Into<String>,
|
|
710
|
+
message: impl Into<String>,
|
|
711
|
+
) -> Self {
|
|
712
|
+
Self::PolicyDecision {
|
|
713
|
+
execution_id: execution_id.as_str().to_string(),
|
|
714
|
+
step_id: step_id.map(|s| s.as_str().to_string()),
|
|
715
|
+
tool_name: tool_name.into(),
|
|
716
|
+
decision: "warn".to_string(),
|
|
717
|
+
reason: Some(message.into()),
|
|
718
|
+
timestamp: Self::now(),
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// =========================================================================
|
|
723
|
+
// Utility Methods
|
|
724
|
+
// =========================================================================
|
|
725
|
+
|
|
726
|
+
/// Check if this is a control event (pause/resume/cancel)
|
|
727
|
+
pub fn is_control_event(&self) -> bool {
|
|
728
|
+
matches!(
|
|
729
|
+
self,
|
|
730
|
+
Self::ExecutionPaused { .. }
|
|
731
|
+
| Self::ExecutionResumed { .. }
|
|
732
|
+
| Self::ExecutionCancelled { .. }
|
|
733
|
+
)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/// Check if this is a delta/streaming event
|
|
737
|
+
pub fn is_delta_event(&self) -> bool {
|
|
738
|
+
matches!(self, Self::TextDelta { .. } | Self::ToolInputDelta { .. })
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/// Check if this is a summary-level event (not a delta)
|
|
742
|
+
pub fn is_summary_event(&self) -> bool {
|
|
743
|
+
!self.is_delta_event()
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/// Serialize to SSE format: `data: {...}\n\n`
|
|
747
|
+
pub fn to_sse(&self) -> String {
|
|
748
|
+
format!(
|
|
749
|
+
"data: {}\n\n",
|
|
750
|
+
serde_json::to_string(self).unwrap_or_default()
|
|
751
|
+
)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/// Create a [DONE] termination signal
|
|
755
|
+
pub fn done() -> String {
|
|
756
|
+
"data: [DONE]\n\n".to_string()
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// =============================================================================
|
|
761
|
+
// Event Emitter
|
|
762
|
+
// =============================================================================
|
|
763
|
+
|
|
764
|
+
/// Event emitter - collects and filters events during execution
|
|
765
|
+
///
|
|
766
|
+
/// Supports optional persistence to EventLog (INV-PERSIST-002: Event Immutability).
|
|
767
|
+
/// When an EventLog is configured, events are persisted before being added to
|
|
768
|
+
/// the in-memory buffer.
|
|
769
|
+
#[derive(Clone)]
|
|
770
|
+
pub struct EventEmitter {
|
|
771
|
+
events: std::sync::Arc<std::sync::Mutex<Vec<StreamEvent>>>,
|
|
772
|
+
mode: StreamMode,
|
|
773
|
+
/// Optional event log for persistence
|
|
774
|
+
event_log: Option<Arc<EventLog>>,
|
|
775
|
+
/// Execution context for persisted events
|
|
776
|
+
execution_context: Option<ExecutionContext>,
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
impl std::fmt::Debug for EventEmitter {
|
|
780
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
781
|
+
f.debug_struct("EventEmitter")
|
|
782
|
+
.field(
|
|
783
|
+
"events_count",
|
|
784
|
+
&self.events.lock().map(|e| e.len()).unwrap_or(0),
|
|
785
|
+
)
|
|
786
|
+
.field("mode", &self.mode)
|
|
787
|
+
.field("has_event_log", &self.event_log.is_some())
|
|
788
|
+
.finish()
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
impl Default for EventEmitter {
|
|
793
|
+
fn default() -> Self {
|
|
794
|
+
Self::new()
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
impl EventEmitter {
|
|
799
|
+
/// Create a new EventEmitter
|
|
800
|
+
pub fn new() -> Self {
|
|
801
|
+
Self {
|
|
802
|
+
events: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
803
|
+
mode: StreamMode::Full,
|
|
804
|
+
event_log: None,
|
|
805
|
+
execution_context: None,
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/// Create a new EventEmitter with a specific mode
|
|
810
|
+
pub fn with_mode(mode: StreamMode) -> Self {
|
|
811
|
+
Self {
|
|
812
|
+
events: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
813
|
+
mode,
|
|
814
|
+
event_log: None,
|
|
815
|
+
execution_context: None,
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/// Create an EventEmitter with persistence support
|
|
820
|
+
///
|
|
821
|
+
/// Events will be persisted to the EventLog before being added to the buffer.
|
|
822
|
+
/// This ensures durability (INV-PERSIST-002).
|
|
823
|
+
pub fn with_persistence(event_log: Arc<EventLog>, execution_id: ExecutionId) -> Self {
|
|
824
|
+
Self {
|
|
825
|
+
events: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
826
|
+
mode: StreamMode::Full,
|
|
827
|
+
event_log: Some(event_log),
|
|
828
|
+
execution_context: Some(ExecutionContext::new(execution_id)),
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/// Set the stream mode
|
|
833
|
+
pub fn set_mode(&mut self, mode: StreamMode) {
|
|
834
|
+
self.mode = mode;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/// Enable persistence with an event log
|
|
838
|
+
pub fn set_event_log(&mut self, event_log: Arc<EventLog>, execution_id: ExecutionId) {
|
|
839
|
+
self.event_log = Some(event_log);
|
|
840
|
+
self.execution_context = Some(ExecutionContext::new(execution_id));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/// Emit an event (filtered by mode)
|
|
844
|
+
///
|
|
845
|
+
/// If persistence is configured, the event is persisted to the EventLog
|
|
846
|
+
/// BEFORE being added to the in-memory buffer. This ensures durability.
|
|
847
|
+
pub fn emit(&self, event: StreamEvent) {
|
|
848
|
+
let should_emit = match self.mode {
|
|
849
|
+
StreamMode::Full => true,
|
|
850
|
+
StreamMode::Summary => event.is_summary_event(),
|
|
851
|
+
StreamMode::ControlOnly => event.is_control_event(),
|
|
852
|
+
StreamMode::Silent => false,
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
if should_emit {
|
|
856
|
+
// Persist to EventLog if configured (async, best-effort for now)
|
|
857
|
+
// In production, this should be awaited before proceeding
|
|
858
|
+
if let (Some(event_log), Some(ctx)) = (&self.event_log, &self.execution_context) {
|
|
859
|
+
if let Some(exec_event) = self.to_execution_event(&event, ctx) {
|
|
860
|
+
let log = Arc::clone(event_log);
|
|
861
|
+
let evt = exec_event;
|
|
862
|
+
// Spawn persistence task (best-effort for non-async emit)
|
|
863
|
+
tokio::spawn(async move {
|
|
864
|
+
if let Err(e) = log.append(evt).await {
|
|
865
|
+
tracing::warn!("Failed to persist event: {}", e);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if let Ok(mut events) = self.events.lock() {
|
|
872
|
+
events.push(event);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/// Convert StreamEvent to ExecutionEvent for persistence
|
|
878
|
+
///
|
|
879
|
+
/// Maps StreamEvent variants to ExecutionEventType.
|
|
880
|
+
/// Some events are skipped as they don't need persistence (deltas, intermediates).
|
|
881
|
+
fn to_execution_event(
|
|
882
|
+
&self,
|
|
883
|
+
stream_event: &StreamEvent,
|
|
884
|
+
ctx: &ExecutionContext,
|
|
885
|
+
) -> Option<ExecutionEvent> {
|
|
886
|
+
let event_type = match stream_event {
|
|
887
|
+
// Execution lifecycle events
|
|
888
|
+
StreamEvent::ExecutionStart { .. } => ExecutionEventType::ExecutionStart,
|
|
889
|
+
StreamEvent::ExecutionEnd { .. } => ExecutionEventType::ExecutionEnd,
|
|
890
|
+
StreamEvent::ExecutionFailed { .. } => ExecutionEventType::ExecutionFailed,
|
|
891
|
+
StreamEvent::ExecutionPaused { .. } => ExecutionEventType::ControlPause,
|
|
892
|
+
StreamEvent::ExecutionResumed { .. } => ExecutionEventType::ControlResume,
|
|
893
|
+
StreamEvent::ExecutionCancelled { .. } => ExecutionEventType::ExecutionCancelled,
|
|
894
|
+
|
|
895
|
+
// Step lifecycle events
|
|
896
|
+
StreamEvent::StepStart { .. } => ExecutionEventType::StepStart,
|
|
897
|
+
StreamEvent::StepEnd { .. } => ExecutionEventType::StepEnd,
|
|
898
|
+
StreamEvent::StepFailed { .. } => ExecutionEventType::StepFailed,
|
|
899
|
+
StreamEvent::StepDiscovered { .. } => ExecutionEventType::StepDiscovered,
|
|
900
|
+
|
|
901
|
+
// Artifact events
|
|
902
|
+
StreamEvent::ArtifactCreated { .. } => ExecutionEventType::ArtifactCreated,
|
|
903
|
+
|
|
904
|
+
// State snapshots
|
|
905
|
+
StreamEvent::StateSnapshot { .. } => ExecutionEventType::StateSnapshot,
|
|
906
|
+
|
|
907
|
+
// Inbox messages (INV-INBOX-003: audit trail)
|
|
908
|
+
StreamEvent::InboxMessage { .. } => ExecutionEventType::InboxMessage,
|
|
909
|
+
|
|
910
|
+
// Policy decisions (audit trail for tool policy evaluation)
|
|
911
|
+
StreamEvent::PolicyDecision { .. } => ExecutionEventType::DecisionMade,
|
|
912
|
+
|
|
913
|
+
StreamEvent::CheckpointSaved { .. } => ExecutionEventType::CheckpointSaved,
|
|
914
|
+
StreamEvent::GoalEvaluated { .. } => ExecutionEventType::GoalEvaluated,
|
|
915
|
+
|
|
916
|
+
// Tool execution (persistence now enabled)
|
|
917
|
+
StreamEvent::ToolInputAvailable { .. } => ExecutionEventType::ToolCallStart,
|
|
918
|
+
StreamEvent::ToolOutputAvailable { .. } => ExecutionEventType::ToolCallEnd,
|
|
919
|
+
|
|
920
|
+
// Skip events that don't need persistence:
|
|
921
|
+
// - Text deltas (streaming intermediates)
|
|
922
|
+
// - Tool input deltas (streaming intermediates)
|
|
923
|
+
// - Generic start/finish (covered by execution lifecycle)
|
|
924
|
+
// - Step boundary markers (covered by step lifecycle)
|
|
925
|
+
StreamEvent::Start { .. }
|
|
926
|
+
| StreamEvent::Finish { .. }
|
|
927
|
+
| StreamEvent::Error { .. }
|
|
928
|
+
| StreamEvent::StartStep { .. }
|
|
929
|
+
| StreamEvent::FinishStep { .. }
|
|
930
|
+
| StreamEvent::TextStart { .. }
|
|
931
|
+
| StreamEvent::TextDelta { .. }
|
|
932
|
+
| StreamEvent::TextEnd { .. }
|
|
933
|
+
| StreamEvent::ToolInputStart { .. }
|
|
934
|
+
| StreamEvent::ToolInputDelta { .. } => return None,
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
let mut context = ctx.clone();
|
|
938
|
+
let mut payload: Option<serde_json::Value> = None;
|
|
939
|
+
let mut duration_ms: Option<u64> = None;
|
|
940
|
+
|
|
941
|
+
match stream_event {
|
|
942
|
+
StreamEvent::ExecutionEnd {
|
|
943
|
+
final_output,
|
|
944
|
+
duration_ms: dur,
|
|
945
|
+
..
|
|
946
|
+
} => {
|
|
947
|
+
duration_ms = Some(*dur);
|
|
948
|
+
if let Some(output) = final_output {
|
|
949
|
+
payload = Some(serde_json::json!({ "output": output }));
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
StreamEvent::ExecutionFailed { error, .. } => {
|
|
953
|
+
payload = serde_json::to_value(error)
|
|
954
|
+
.ok()
|
|
955
|
+
.map(|err| serde_json::json!({ "error": err }));
|
|
956
|
+
}
|
|
957
|
+
StreamEvent::ExecutionCancelled { reason, .. }
|
|
958
|
+
| StreamEvent::ExecutionPaused { reason, .. } => {
|
|
959
|
+
payload = Some(serde_json::json!({ "reason": reason }));
|
|
960
|
+
}
|
|
961
|
+
StreamEvent::StepStart {
|
|
962
|
+
step_id,
|
|
963
|
+
step_type,
|
|
964
|
+
name,
|
|
965
|
+
..
|
|
966
|
+
} => {
|
|
967
|
+
context = context.with_step(StepId::from_string(step_id));
|
|
968
|
+
payload = Some(serde_json::json!({ "step_type": step_type, "name": name }));
|
|
969
|
+
}
|
|
970
|
+
StreamEvent::StepEnd {
|
|
971
|
+
step_id,
|
|
972
|
+
output,
|
|
973
|
+
duration_ms: dur,
|
|
974
|
+
..
|
|
975
|
+
} => {
|
|
976
|
+
context = context.with_step(StepId::from_string(step_id));
|
|
977
|
+
duration_ms = Some(*dur);
|
|
978
|
+
if let Some(out) = output {
|
|
979
|
+
payload = Some(serde_json::json!({ "output": out }));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
StreamEvent::StepFailed { step_id, error, .. } => {
|
|
983
|
+
context = context.with_step(StepId::from_string(step_id));
|
|
984
|
+
payload = serde_json::to_value(error)
|
|
985
|
+
.ok()
|
|
986
|
+
.map(|err| serde_json::json!({ "error": err }));
|
|
987
|
+
}
|
|
988
|
+
StreamEvent::StepDiscovered {
|
|
989
|
+
step_id,
|
|
990
|
+
discovered_by,
|
|
991
|
+
source_type,
|
|
992
|
+
reason,
|
|
993
|
+
depth,
|
|
994
|
+
..
|
|
995
|
+
} => {
|
|
996
|
+
context = context.with_step(StepId::from_string(step_id));
|
|
997
|
+
payload = Some(serde_json::json!({
|
|
998
|
+
"discovered_by": discovered_by,
|
|
999
|
+
"source_type": source_type,
|
|
1000
|
+
"reason": reason,
|
|
1001
|
+
"depth": depth,
|
|
1002
|
+
}));
|
|
1003
|
+
}
|
|
1004
|
+
StreamEvent::ArtifactCreated {
|
|
1005
|
+
step_id,
|
|
1006
|
+
artifact_id,
|
|
1007
|
+
artifact_type,
|
|
1008
|
+
..
|
|
1009
|
+
} => {
|
|
1010
|
+
context = context
|
|
1011
|
+
.with_step(StepId::from_string(step_id))
|
|
1012
|
+
.with_artifact(ArtifactId::from_string(artifact_id));
|
|
1013
|
+
payload = Some(serde_json::json!({ "artifact_type": artifact_type }));
|
|
1014
|
+
}
|
|
1015
|
+
StreamEvent::StateSnapshot { step_id, state, .. } => {
|
|
1016
|
+
if let Some(step) = step_id {
|
|
1017
|
+
context = context.with_step(StepId::from_string(step));
|
|
1018
|
+
}
|
|
1019
|
+
payload = Some(state.clone());
|
|
1020
|
+
}
|
|
1021
|
+
StreamEvent::InboxMessage {
|
|
1022
|
+
message_id,
|
|
1023
|
+
message_type,
|
|
1024
|
+
..
|
|
1025
|
+
} => {
|
|
1026
|
+
payload = Some(serde_json::json!({
|
|
1027
|
+
"message_id": message_id,
|
|
1028
|
+
"message_type": message_type
|
|
1029
|
+
}));
|
|
1030
|
+
}
|
|
1031
|
+
StreamEvent::PolicyDecision {
|
|
1032
|
+
step_id,
|
|
1033
|
+
tool_name,
|
|
1034
|
+
decision,
|
|
1035
|
+
reason,
|
|
1036
|
+
..
|
|
1037
|
+
} => {
|
|
1038
|
+
if let Some(step) = step_id {
|
|
1039
|
+
context = context.with_step(StepId::from_string(step));
|
|
1040
|
+
}
|
|
1041
|
+
payload = Some(serde_json::json!({
|
|
1042
|
+
"tool_name": tool_name,
|
|
1043
|
+
"decision": decision,
|
|
1044
|
+
"tool_name": tool_name,
|
|
1045
|
+
"decision": decision,
|
|
1046
|
+
"reason": reason,
|
|
1047
|
+
}));
|
|
1048
|
+
}
|
|
1049
|
+
StreamEvent::ToolInputAvailable {
|
|
1050
|
+
tool_call_id,
|
|
1051
|
+
tool_name,
|
|
1052
|
+
input,
|
|
1053
|
+
..
|
|
1054
|
+
} => {
|
|
1055
|
+
payload = Some(serde_json::json!({
|
|
1056
|
+
"tool_call_id": tool_call_id,
|
|
1057
|
+
"tool_name": tool_name,
|
|
1058
|
+
"input": input,
|
|
1059
|
+
}));
|
|
1060
|
+
}
|
|
1061
|
+
StreamEvent::ToolOutputAvailable {
|
|
1062
|
+
tool_call_id,
|
|
1063
|
+
output,
|
|
1064
|
+
..
|
|
1065
|
+
} => {
|
|
1066
|
+
payload = Some(serde_json::json!({
|
|
1067
|
+
"tool_call_id": tool_call_id,
|
|
1068
|
+
"output": output,
|
|
1069
|
+
}));
|
|
1070
|
+
}
|
|
1071
|
+
StreamEvent::CheckpointSaved {
|
|
1072
|
+
checkpoint_id,
|
|
1073
|
+
step_id,
|
|
1074
|
+
state_hash,
|
|
1075
|
+
..
|
|
1076
|
+
} => {
|
|
1077
|
+
if let Some(step) = step_id {
|
|
1078
|
+
context = context.with_step(StepId::from_string(step));
|
|
1079
|
+
}
|
|
1080
|
+
payload = Some(serde_json::json!({
|
|
1081
|
+
"checkpoint_id": checkpoint_id,
|
|
1082
|
+
"state_hash": state_hash,
|
|
1083
|
+
}));
|
|
1084
|
+
}
|
|
1085
|
+
StreamEvent::GoalEvaluated {
|
|
1086
|
+
goal_id,
|
|
1087
|
+
step_id,
|
|
1088
|
+
status,
|
|
1089
|
+
score,
|
|
1090
|
+
reason,
|
|
1091
|
+
..
|
|
1092
|
+
} => {
|
|
1093
|
+
if let Some(step) = step_id {
|
|
1094
|
+
context = context.with_step(StepId::from_string(step));
|
|
1095
|
+
}
|
|
1096
|
+
payload = Some(serde_json::json!({
|
|
1097
|
+
"goal_id": goal_id,
|
|
1098
|
+
"status": status,
|
|
1099
|
+
"score": score,
|
|
1100
|
+
"reason": reason,
|
|
1101
|
+
}));
|
|
1102
|
+
}
|
|
1103
|
+
StreamEvent::ExecutionStart { .. } => {}
|
|
1104
|
+
_ => {}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
let mut event = ExecutionEvent::new(event_type, context);
|
|
1108
|
+
if let Some(ms) = duration_ms {
|
|
1109
|
+
event.duration_ms = Some(ms);
|
|
1110
|
+
}
|
|
1111
|
+
if let Some(data) = payload {
|
|
1112
|
+
event = event.with_payload(data);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
Some(event)
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/// Emit an event unconditionally (ignores mode)
|
|
1119
|
+
pub fn emit_force(&self, event: StreamEvent) {
|
|
1120
|
+
if let Ok(mut events) = self.events.lock() {
|
|
1121
|
+
events.push(event);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/// Get all collected events
|
|
1126
|
+
pub fn drain(&self) -> Vec<StreamEvent> {
|
|
1127
|
+
if let Ok(mut events) = self.events.lock() {
|
|
1128
|
+
std::mem::take(&mut *events)
|
|
1129
|
+
} else {
|
|
1130
|
+
vec![]
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/// Get the current stream mode
|
|
1135
|
+
pub fn mode(&self) -> StreamMode {
|
|
1136
|
+
self.mode
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
#[cfg(test)]
|
|
1141
|
+
mod tests {
|
|
1142
|
+
use super::*;
|
|
1143
|
+
use crate::kernel::{ExecutionId, StepId, StepType};
|
|
1144
|
+
|
|
1145
|
+
// ============ StreamMode Tests ============
|
|
1146
|
+
|
|
1147
|
+
#[test]
|
|
1148
|
+
fn test_stream_mode_default() {
|
|
1149
|
+
assert_eq!(StreamMode::default(), StreamMode::Full);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
#[test]
|
|
1153
|
+
fn test_stream_mode_variants() {
|
|
1154
|
+
let modes = vec![
|
|
1155
|
+
StreamMode::Full,
|
|
1156
|
+
StreamMode::Summary,
|
|
1157
|
+
StreamMode::ControlOnly,
|
|
1158
|
+
StreamMode::Silent,
|
|
1159
|
+
];
|
|
1160
|
+
assert_eq!(modes.len(), 4);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// ============ StreamEvent Utility Tests ============
|
|
1164
|
+
|
|
1165
|
+
#[test]
|
|
1166
|
+
fn test_stream_event_is_control_event() {
|
|
1167
|
+
let exec_id = ExecutionId::new();
|
|
1168
|
+
|
|
1169
|
+
// Control events
|
|
1170
|
+
assert!(StreamEvent::execution_paused(&exec_id, "paused").is_control_event());
|
|
1171
|
+
assert!(StreamEvent::execution_resumed(&exec_id).is_control_event());
|
|
1172
|
+
assert!(StreamEvent::execution_cancelled(&exec_id, "cancelled").is_control_event());
|
|
1173
|
+
|
|
1174
|
+
// Non-control events
|
|
1175
|
+
assert!(!StreamEvent::execution_start(&exec_id).is_control_event());
|
|
1176
|
+
assert!(!StreamEvent::text_start(None).is_control_event());
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
#[test]
|
|
1180
|
+
fn test_stream_event_is_delta_event() {
|
|
1181
|
+
// Delta events
|
|
1182
|
+
let delta = StreamEvent::text_delta("id1", "chunk");
|
|
1183
|
+
assert!(delta.is_delta_event());
|
|
1184
|
+
|
|
1185
|
+
// Non-delta events
|
|
1186
|
+
let start = StreamEvent::text_start(None);
|
|
1187
|
+
assert!(!start.is_delta_event());
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
#[test]
|
|
1191
|
+
fn test_stream_event_is_summary_event() {
|
|
1192
|
+
// Summary events (non-delta)
|
|
1193
|
+
let start = StreamEvent::text_start(None);
|
|
1194
|
+
assert!(start.is_summary_event());
|
|
1195
|
+
|
|
1196
|
+
// Delta events are not summary
|
|
1197
|
+
let delta = StreamEvent::text_delta("id1", "chunk");
|
|
1198
|
+
assert!(!delta.is_summary_event());
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
#[test]
|
|
1202
|
+
fn test_stream_event_to_sse() {
|
|
1203
|
+
let event = StreamEvent::text_end("test-id");
|
|
1204
|
+
let sse = event.to_sse();
|
|
1205
|
+
|
|
1206
|
+
assert!(sse.starts_with("data: "));
|
|
1207
|
+
assert!(sse.ends_with("\n\n"));
|
|
1208
|
+
assert!(sse.contains("test-id"));
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
#[test]
|
|
1212
|
+
fn test_stream_event_done() {
|
|
1213
|
+
let done = StreamEvent::done();
|
|
1214
|
+
assert_eq!(done, "data: [DONE]\n\n");
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// ============ StreamEvent Factory Tests ============
|
|
1218
|
+
|
|
1219
|
+
#[test]
|
|
1220
|
+
fn test_stream_event_text_factories() {
|
|
1221
|
+
let exec_id = ExecutionId::new();
|
|
1222
|
+
|
|
1223
|
+
let start = StreamEvent::text_start(Some(&exec_id));
|
|
1224
|
+
assert!(matches!(start, StreamEvent::TextStart { .. }));
|
|
1225
|
+
|
|
1226
|
+
let delta = StreamEvent::text_delta("id1", "hello");
|
|
1227
|
+
assert!(matches!(delta, StreamEvent::TextDelta { delta, .. } if delta == "hello"));
|
|
1228
|
+
|
|
1229
|
+
let end = StreamEvent::text_end("id1");
|
|
1230
|
+
assert!(matches!(end, StreamEvent::TextEnd { .. }));
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
#[test]
|
|
1234
|
+
fn test_stream_event_execution_factories() {
|
|
1235
|
+
let exec_id = ExecutionId::new();
|
|
1236
|
+
|
|
1237
|
+
let start = StreamEvent::execution_start(&exec_id);
|
|
1238
|
+
assert!(matches!(start, StreamEvent::ExecutionStart { .. }));
|
|
1239
|
+
|
|
1240
|
+
let end = StreamEvent::execution_end(&exec_id, Some("output".to_string()), 100);
|
|
1241
|
+
assert!(matches!(
|
|
1242
|
+
end,
|
|
1243
|
+
StreamEvent::ExecutionEnd {
|
|
1244
|
+
duration_ms: 100,
|
|
1245
|
+
..
|
|
1246
|
+
}
|
|
1247
|
+
));
|
|
1248
|
+
|
|
1249
|
+
use crate::kernel::ExecutionError;
|
|
1250
|
+
let error = ExecutionError::kernel_internal("error message");
|
|
1251
|
+
let failed = StreamEvent::execution_failed(&exec_id, error);
|
|
1252
|
+
assert!(matches!(failed, StreamEvent::ExecutionFailed { .. }));
|
|
1253
|
+
|
|
1254
|
+
let paused = StreamEvent::execution_paused(&exec_id, "reason");
|
|
1255
|
+
assert!(matches!(paused, StreamEvent::ExecutionPaused { .. }));
|
|
1256
|
+
|
|
1257
|
+
let resumed = StreamEvent::execution_resumed(&exec_id);
|
|
1258
|
+
assert!(matches!(resumed, StreamEvent::ExecutionResumed { .. }));
|
|
1259
|
+
|
|
1260
|
+
let cancelled = StreamEvent::execution_cancelled(&exec_id, "cancel reason");
|
|
1261
|
+
assert!(matches!(cancelled, StreamEvent::ExecutionCancelled { .. }));
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
#[test]
|
|
1265
|
+
fn test_stream_event_step_factories() {
|
|
1266
|
+
let exec_id = ExecutionId::new();
|
|
1267
|
+
let step_id = StepId::new();
|
|
1268
|
+
|
|
1269
|
+
let start =
|
|
1270
|
+
StreamEvent::step_start(&exec_id, &step_id, StepType::FunctionNode, "test_step");
|
|
1271
|
+
assert!(matches!(start, StreamEvent::StepStart { .. }));
|
|
1272
|
+
|
|
1273
|
+
let end = StreamEvent::step_end(&exec_id, &step_id, Some("output".to_string()), 50);
|
|
1274
|
+
assert!(matches!(
|
|
1275
|
+
end,
|
|
1276
|
+
StreamEvent::StepEnd {
|
|
1277
|
+
duration_ms: 50,
|
|
1278
|
+
..
|
|
1279
|
+
}
|
|
1280
|
+
));
|
|
1281
|
+
|
|
1282
|
+
use crate::kernel::ExecutionError;
|
|
1283
|
+
let error = ExecutionError::kernel_internal("step error");
|
|
1284
|
+
let failed = StreamEvent::step_failed(&exec_id, &step_id, error);
|
|
1285
|
+
assert!(matches!(failed, StreamEvent::StepFailed { .. }));
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
#[test]
|
|
1289
|
+
fn test_stream_event_tool_factories() {
|
|
1290
|
+
let input_start = StreamEvent::tool_input_start("call-123", "web_search");
|
|
1291
|
+
assert!(
|
|
1292
|
+
matches!(input_start, StreamEvent::ToolInputStart { tool_name, .. } if tool_name == "web_search")
|
|
1293
|
+
);
|
|
1294
|
+
|
|
1295
|
+
let input_avail = StreamEvent::tool_input_available(
|
|
1296
|
+
"call-123",
|
|
1297
|
+
"web_search",
|
|
1298
|
+
serde_json::json!({"q": "test"}),
|
|
1299
|
+
);
|
|
1300
|
+
assert!(matches!(
|
|
1301
|
+
input_avail,
|
|
1302
|
+
StreamEvent::ToolInputAvailable { .. }
|
|
1303
|
+
));
|
|
1304
|
+
|
|
1305
|
+
let output_avail =
|
|
1306
|
+
StreamEvent::tool_output_available("call-123", serde_json::json!({"result": "ok"}));
|
|
1307
|
+
assert!(matches!(
|
|
1308
|
+
output_avail,
|
|
1309
|
+
StreamEvent::ToolOutputAvailable { .. }
|
|
1310
|
+
));
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// ============ EventEmitter Tests ============
|
|
1314
|
+
|
|
1315
|
+
#[test]
|
|
1316
|
+
fn test_event_emitter_new() {
|
|
1317
|
+
let emitter = EventEmitter::new();
|
|
1318
|
+
assert_eq!(emitter.mode(), StreamMode::Full);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
#[test]
|
|
1322
|
+
fn test_event_emitter_with_mode() {
|
|
1323
|
+
let emitter = EventEmitter::with_mode(StreamMode::Summary);
|
|
1324
|
+
assert_eq!(emitter.mode(), StreamMode::Summary);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
#[test]
|
|
1328
|
+
fn test_event_emitter_set_mode() {
|
|
1329
|
+
let mut emitter = EventEmitter::new();
|
|
1330
|
+
emitter.set_mode(StreamMode::Silent);
|
|
1331
|
+
assert_eq!(emitter.mode(), StreamMode::Silent);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
#[test]
|
|
1335
|
+
fn test_event_emitter_emit_and_drain() {
|
|
1336
|
+
let emitter = EventEmitter::new();
|
|
1337
|
+
let exec_id = ExecutionId::new();
|
|
1338
|
+
|
|
1339
|
+
emitter.emit(StreamEvent::execution_start(&exec_id));
|
|
1340
|
+
emitter.emit(StreamEvent::execution_end(&exec_id, None, 100));
|
|
1341
|
+
|
|
1342
|
+
let events = emitter.drain();
|
|
1343
|
+
assert_eq!(events.len(), 2);
|
|
1344
|
+
|
|
1345
|
+
// Drain should clear events
|
|
1346
|
+
let events_after = emitter.drain();
|
|
1347
|
+
assert!(events_after.is_empty());
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
#[test]
|
|
1351
|
+
fn test_event_emitter_mode_full() {
|
|
1352
|
+
let emitter = EventEmitter::with_mode(StreamMode::Full);
|
|
1353
|
+
|
|
1354
|
+
// All events should be emitted
|
|
1355
|
+
emitter.emit(StreamEvent::text_delta("id", "chunk"));
|
|
1356
|
+
emitter.emit(StreamEvent::text_end("id"));
|
|
1357
|
+
|
|
1358
|
+
let events = emitter.drain();
|
|
1359
|
+
assert_eq!(events.len(), 2);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
#[test]
|
|
1363
|
+
fn test_event_emitter_mode_summary() {
|
|
1364
|
+
let emitter = EventEmitter::with_mode(StreamMode::Summary);
|
|
1365
|
+
|
|
1366
|
+
// Delta events should be filtered out
|
|
1367
|
+
emitter.emit(StreamEvent::text_delta("id", "chunk"));
|
|
1368
|
+
emitter.emit(StreamEvent::text_end("id"));
|
|
1369
|
+
|
|
1370
|
+
let events = emitter.drain();
|
|
1371
|
+
assert_eq!(events.len(), 1); // Only text_end (summary event)
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
#[test]
|
|
1375
|
+
fn test_event_emitter_mode_control_only() {
|
|
1376
|
+
let emitter = EventEmitter::with_mode(StreamMode::ControlOnly);
|
|
1377
|
+
let exec_id = ExecutionId::new();
|
|
1378
|
+
|
|
1379
|
+
// Non-control events should be filtered out
|
|
1380
|
+
emitter.emit(StreamEvent::execution_start(&exec_id));
|
|
1381
|
+
emitter.emit(StreamEvent::execution_paused(&exec_id, "test"));
|
|
1382
|
+
|
|
1383
|
+
let events = emitter.drain();
|
|
1384
|
+
assert_eq!(events.len(), 1); // Only paused (control event)
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
#[test]
|
|
1388
|
+
fn test_event_emitter_mode_silent() {
|
|
1389
|
+
let emitter = EventEmitter::with_mode(StreamMode::Silent);
|
|
1390
|
+
let exec_id = ExecutionId::new();
|
|
1391
|
+
|
|
1392
|
+
// All events should be filtered out
|
|
1393
|
+
emitter.emit(StreamEvent::execution_start(&exec_id));
|
|
1394
|
+
emitter.emit(StreamEvent::execution_paused(&exec_id, "test"));
|
|
1395
|
+
|
|
1396
|
+
let events = emitter.drain();
|
|
1397
|
+
assert!(events.is_empty());
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
#[test]
|
|
1401
|
+
fn test_event_emitter_emit_force() {
|
|
1402
|
+
let emitter = EventEmitter::with_mode(StreamMode::Silent);
|
|
1403
|
+
let exec_id = ExecutionId::new();
|
|
1404
|
+
|
|
1405
|
+
// Force emit should bypass mode filter
|
|
1406
|
+
emitter.emit_force(StreamEvent::execution_start(&exec_id));
|
|
1407
|
+
|
|
1408
|
+
let events = emitter.drain();
|
|
1409
|
+
assert_eq!(events.len(), 1);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
#[test]
|
|
1413
|
+
fn test_event_emitter_serialization() {
|
|
1414
|
+
use crate::kernel::ExecutionError;
|
|
1415
|
+
let error = ExecutionError::kernel_internal("Test error").with_code("ERR_CODE".to_string());
|
|
1416
|
+
let event = StreamEvent::error(error);
|
|
1417
|
+
let json = serde_json::to_string(&event).unwrap();
|
|
1418
|
+
|
|
1419
|
+
assert!(json.contains("data-error"));
|
|
1420
|
+
assert!(json.contains("Test error"));
|
|
1421
|
+
assert!(json.contains("ERR_CODE"));
|
|
1422
|
+
}
|
|
1423
|
+
}
|