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,908 @@
|
|
|
1
|
+
//! MessageStore - User-facing message persistence
|
|
2
|
+
//!
|
|
3
|
+
//! The MessageStore handles thread/message persistence for UI display.
|
|
4
|
+
//! Unlike EventStore (audit-focused, immutable), MessageStore is:
|
|
5
|
+
//! - **Mutable**: Messages can be soft-deleted/edited
|
|
6
|
+
//! - **User-facing**: AI SDK compatible format
|
|
7
|
+
//! - **Hierarchical**: Thread → Message with parent linkage
|
|
8
|
+
//!
|
|
9
|
+
//! ## Guarantees
|
|
10
|
+
//!
|
|
11
|
+
//! - **Soft Delete**: Messages are never hard-deleted (GDPR compliance)
|
|
12
|
+
//! - **Thread Ownership**: Messages belong to exactly one thread
|
|
13
|
+
//! - **Parent Linkage**: Every message tracks its causal origin
|
|
14
|
+
//!
|
|
15
|
+
//! @see docs/TECHNICAL/20-MESSAGE-PERSISTENCE-STRATEGY.md
|
|
16
|
+
|
|
17
|
+
use async_trait::async_trait;
|
|
18
|
+
use chrono::{DateTime, Utc};
|
|
19
|
+
use serde::{Deserialize, Serialize};
|
|
20
|
+
use std::collections::HashMap;
|
|
21
|
+
use std::sync::RwLock;
|
|
22
|
+
|
|
23
|
+
use crate::kernel::{ExecutionId, MessageId, ParentType, TenantId, ThreadId, UserId};
|
|
24
|
+
|
|
25
|
+
use super::StorageBackend;
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Thread - Conversation container
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
/// A conversation thread containing messages
|
|
32
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
33
|
+
pub struct Thread {
|
|
34
|
+
/// Unique thread identifier
|
|
35
|
+
pub id: ThreadId,
|
|
36
|
+
/// Tenant for multi-tenancy
|
|
37
|
+
pub tenant_id: TenantId,
|
|
38
|
+
/// User who owns this thread
|
|
39
|
+
pub user_id: UserId,
|
|
40
|
+
/// Optional thread title (auto-generated from first message)
|
|
41
|
+
pub title: Option<String>,
|
|
42
|
+
/// When the thread was created
|
|
43
|
+
pub created_at: DateTime<Utc>,
|
|
44
|
+
/// When the thread was last updated
|
|
45
|
+
pub updated_at: DateTime<Utc>,
|
|
46
|
+
/// Soft delete timestamp (None = active)
|
|
47
|
+
pub deleted_at: Option<DateTime<Utc>>,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
impl Thread {
|
|
51
|
+
/// Create a new thread
|
|
52
|
+
pub fn new(tenant_id: TenantId, user_id: UserId) -> Self {
|
|
53
|
+
let now = Utc::now();
|
|
54
|
+
Self {
|
|
55
|
+
id: ThreadId::new(),
|
|
56
|
+
tenant_id,
|
|
57
|
+
user_id,
|
|
58
|
+
title: None,
|
|
59
|
+
created_at: now,
|
|
60
|
+
updated_at: now,
|
|
61
|
+
deleted_at: None,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Create a thread with a specific ID (for testing/restore)
|
|
66
|
+
pub fn with_id(id: ThreadId, tenant_id: TenantId, user_id: UserId) -> Self {
|
|
67
|
+
let now = Utc::now();
|
|
68
|
+
Self {
|
|
69
|
+
id,
|
|
70
|
+
tenant_id,
|
|
71
|
+
user_id,
|
|
72
|
+
title: None,
|
|
73
|
+
created_at: now,
|
|
74
|
+
updated_at: now,
|
|
75
|
+
deleted_at: None,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Check if thread is soft-deleted
|
|
80
|
+
pub fn is_deleted(&self) -> bool {
|
|
81
|
+
self.deleted_at.is_some()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// =============================================================================
|
|
86
|
+
// Message Role
|
|
87
|
+
// =============================================================================
|
|
88
|
+
|
|
89
|
+
/// Message role (AI SDK compatible)
|
|
90
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
91
|
+
#[serde(rename_all = "lowercase")]
|
|
92
|
+
pub enum MessageRole {
|
|
93
|
+
/// User message
|
|
94
|
+
User,
|
|
95
|
+
/// Assistant/AI response
|
|
96
|
+
Assistant,
|
|
97
|
+
/// System message (instructions, context)
|
|
98
|
+
System,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl std::fmt::Display for MessageRole {
|
|
102
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
103
|
+
match self {
|
|
104
|
+
MessageRole::User => write!(f, "user"),
|
|
105
|
+
MessageRole::Assistant => write!(f, "assistant"),
|
|
106
|
+
MessageRole::System => write!(f, "system"),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// Message Parts (AI SDK compatible)
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
/// Message part for rich content (AI SDK compatible)
|
|
116
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
117
|
+
#[serde(tag = "type", rename_all = "kebab-case")]
|
|
118
|
+
pub enum MessagePart {
|
|
119
|
+
/// Plain text content
|
|
120
|
+
Text { text: String },
|
|
121
|
+
|
|
122
|
+
/// Reasoning/thinking (collapsed in UI)
|
|
123
|
+
Reasoning { text: String },
|
|
124
|
+
|
|
125
|
+
/// Tool invocation
|
|
126
|
+
ToolCall {
|
|
127
|
+
tool_call_id: String,
|
|
128
|
+
tool_name: String,
|
|
129
|
+
args: serde_json::Value,
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/// Tool result
|
|
133
|
+
ToolResult {
|
|
134
|
+
tool_call_id: String,
|
|
135
|
+
tool_name: String,
|
|
136
|
+
result: serde_json::Value,
|
|
137
|
+
is_error: bool,
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
/// Source/citation
|
|
141
|
+
Source {
|
|
142
|
+
source_id: String,
|
|
143
|
+
url: Option<String>,
|
|
144
|
+
title: Option<String>,
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
/// File attachment
|
|
148
|
+
File {
|
|
149
|
+
file_id: String,
|
|
150
|
+
filename: String,
|
|
151
|
+
mime_type: String,
|
|
152
|
+
size_bytes: u64,
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/// Image (inline or referenced)
|
|
156
|
+
Image {
|
|
157
|
+
image_id: String,
|
|
158
|
+
url: Option<String>,
|
|
159
|
+
alt_text: Option<String>,
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
/// Code block
|
|
163
|
+
Code {
|
|
164
|
+
language: Option<String>,
|
|
165
|
+
code: String,
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// =============================================================================
|
|
170
|
+
// Token Usage
|
|
171
|
+
// =============================================================================
|
|
172
|
+
|
|
173
|
+
/// Token usage for billing and analytics
|
|
174
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
175
|
+
pub struct TokenUsage {
|
|
176
|
+
/// Input/prompt tokens
|
|
177
|
+
pub prompt_tokens: u32,
|
|
178
|
+
/// Output/completion tokens
|
|
179
|
+
pub completion_tokens: u32,
|
|
180
|
+
/// Total tokens (may include cached)
|
|
181
|
+
pub total_tokens: u32,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
impl TokenUsage {
|
|
185
|
+
/// Create new token usage
|
|
186
|
+
pub fn new(prompt: u32, completion: u32) -> Self {
|
|
187
|
+
Self {
|
|
188
|
+
prompt_tokens: prompt,
|
|
189
|
+
completion_tokens: completion,
|
|
190
|
+
total_tokens: prompt + completion,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// =============================================================================
|
|
196
|
+
// Execution Stats
|
|
197
|
+
// =============================================================================
|
|
198
|
+
|
|
199
|
+
/// Execution statistics for a message
|
|
200
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
201
|
+
pub struct ExecutionStats {
|
|
202
|
+
/// Total LLM invocations
|
|
203
|
+
pub llm_calls: u32,
|
|
204
|
+
/// Total tool invocations
|
|
205
|
+
pub tool_calls: u32,
|
|
206
|
+
/// Nested agent executions
|
|
207
|
+
pub sub_agents: u32,
|
|
208
|
+
/// Total steps executed
|
|
209
|
+
pub steps: u32,
|
|
210
|
+
/// Decisions made (audit)
|
|
211
|
+
pub decisions: u32,
|
|
212
|
+
/// Artifacts produced
|
|
213
|
+
pub artifacts: u32,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// =============================================================================
|
|
217
|
+
// Cost Info
|
|
218
|
+
// =============================================================================
|
|
219
|
+
|
|
220
|
+
/// Cost information for billing
|
|
221
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
222
|
+
pub struct CostInfo {
|
|
223
|
+
/// Input token cost
|
|
224
|
+
pub input_cost: f64,
|
|
225
|
+
/// Output token cost
|
|
226
|
+
pub output_cost: f64,
|
|
227
|
+
/// Total cost
|
|
228
|
+
pub total_cost: f64,
|
|
229
|
+
/// Currency (default: USD)
|
|
230
|
+
pub currency: String,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
impl Default for CostInfo {
|
|
234
|
+
fn default() -> Self {
|
|
235
|
+
Self {
|
|
236
|
+
input_cost: 0.0,
|
|
237
|
+
output_cost: 0.0,
|
|
238
|
+
total_cost: 0.0,
|
|
239
|
+
currency: "USD".to_string(),
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// =============================================================================
|
|
245
|
+
// Finish Reason
|
|
246
|
+
// =============================================================================
|
|
247
|
+
|
|
248
|
+
/// Why the response finished
|
|
249
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
250
|
+
#[serde(rename_all = "snake_case")]
|
|
251
|
+
pub enum FinishReason {
|
|
252
|
+
/// Normal completion
|
|
253
|
+
Stop,
|
|
254
|
+
/// Hit max tokens
|
|
255
|
+
Length,
|
|
256
|
+
/// Needs to call tools
|
|
257
|
+
ToolCalls,
|
|
258
|
+
/// Content filter triggered
|
|
259
|
+
ContentFilter,
|
|
260
|
+
/// Error occurred
|
|
261
|
+
Error,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// =============================================================================
|
|
265
|
+
// Message Metadata
|
|
266
|
+
// =============================================================================
|
|
267
|
+
|
|
268
|
+
/// Message metadata (AI SDK compatible)
|
|
269
|
+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
270
|
+
pub struct MessageMetadata {
|
|
271
|
+
/// When response completed (Unix ms)
|
|
272
|
+
pub completed_at: Option<i64>,
|
|
273
|
+
/// Total duration in ms
|
|
274
|
+
pub duration_ms: Option<u64>,
|
|
275
|
+
|
|
276
|
+
/// Model used (e.g., "gpt-4o", "claude-3-opus")
|
|
277
|
+
pub model: Option<String>,
|
|
278
|
+
/// Provider (e.g., "azure", "anthropic")
|
|
279
|
+
pub provider: Option<String>,
|
|
280
|
+
|
|
281
|
+
/// Token usage (aggregated across all LLM calls)
|
|
282
|
+
pub token_usage: Option<TokenUsage>,
|
|
283
|
+
|
|
284
|
+
/// Execution statistics
|
|
285
|
+
pub stats: Option<ExecutionStats>,
|
|
286
|
+
|
|
287
|
+
/// Why the response finished
|
|
288
|
+
pub finish_reason: Option<FinishReason>,
|
|
289
|
+
|
|
290
|
+
/// Cost information (if available)
|
|
291
|
+
pub cost: Option<CostInfo>,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// =============================================================================
|
|
295
|
+
// Message
|
|
296
|
+
// =============================================================================
|
|
297
|
+
|
|
298
|
+
/// A message in a thread (AI SDK compatible)
|
|
299
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
300
|
+
pub struct Message {
|
|
301
|
+
/// Unique message identifier
|
|
302
|
+
pub id: MessageId,
|
|
303
|
+
/// Thread this message belongs to
|
|
304
|
+
pub thread_id: ThreadId,
|
|
305
|
+
/// Link to execution (for assistant messages)
|
|
306
|
+
pub execution_id: Option<ExecutionId>,
|
|
307
|
+
|
|
308
|
+
// Hierarchy
|
|
309
|
+
/// Parent message (for reply chains)
|
|
310
|
+
pub parent_id: Option<MessageId>,
|
|
311
|
+
/// What triggered this message
|
|
312
|
+
pub parent_type: ParentType,
|
|
313
|
+
|
|
314
|
+
// Content
|
|
315
|
+
/// Message role
|
|
316
|
+
pub role: MessageRole,
|
|
317
|
+
/// Simple text content (for display)
|
|
318
|
+
pub content: String,
|
|
319
|
+
/// Rich content parts (AI SDK format)
|
|
320
|
+
pub parts: Vec<MessagePart>,
|
|
321
|
+
|
|
322
|
+
// Timestamps
|
|
323
|
+
/// When created
|
|
324
|
+
pub created_at: DateTime<Utc>,
|
|
325
|
+
/// When last updated
|
|
326
|
+
pub updated_at: Option<DateTime<Utc>>,
|
|
327
|
+
/// Soft delete timestamp
|
|
328
|
+
pub deleted_at: Option<DateTime<Utc>>,
|
|
329
|
+
|
|
330
|
+
/// Additional metadata
|
|
331
|
+
pub metadata: MessageMetadata,
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
impl Message {
|
|
335
|
+
/// Create a new user message
|
|
336
|
+
pub fn user(thread_id: ThreadId, content: impl Into<String>) -> Self {
|
|
337
|
+
let content = content.into();
|
|
338
|
+
Self {
|
|
339
|
+
id: MessageId::new(),
|
|
340
|
+
thread_id,
|
|
341
|
+
execution_id: None,
|
|
342
|
+
parent_id: None,
|
|
343
|
+
parent_type: ParentType::UserMessage,
|
|
344
|
+
role: MessageRole::User,
|
|
345
|
+
content: content.clone(),
|
|
346
|
+
parts: vec![MessagePart::Text { text: content }],
|
|
347
|
+
created_at: Utc::now(),
|
|
348
|
+
updated_at: None,
|
|
349
|
+
deleted_at: None,
|
|
350
|
+
metadata: MessageMetadata::default(),
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/// Create a new assistant message
|
|
355
|
+
pub fn assistant(
|
|
356
|
+
thread_id: ThreadId,
|
|
357
|
+
execution_id: ExecutionId,
|
|
358
|
+
content: impl Into<String>,
|
|
359
|
+
parent_id: Option<MessageId>,
|
|
360
|
+
) -> Self {
|
|
361
|
+
let content = content.into();
|
|
362
|
+
Self {
|
|
363
|
+
id: MessageId::new(),
|
|
364
|
+
thread_id,
|
|
365
|
+
execution_id: Some(execution_id),
|
|
366
|
+
parent_id,
|
|
367
|
+
parent_type: ParentType::UserMessage,
|
|
368
|
+
role: MessageRole::Assistant,
|
|
369
|
+
content: content.clone(),
|
|
370
|
+
parts: vec![MessagePart::Text { text: content }],
|
|
371
|
+
created_at: Utc::now(),
|
|
372
|
+
updated_at: None,
|
|
373
|
+
deleted_at: None,
|
|
374
|
+
metadata: MessageMetadata::default(),
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/// Create a new system message
|
|
379
|
+
pub fn system(thread_id: ThreadId, content: impl Into<String>) -> Self {
|
|
380
|
+
let content = content.into();
|
|
381
|
+
Self {
|
|
382
|
+
id: MessageId::new(),
|
|
383
|
+
thread_id,
|
|
384
|
+
execution_id: None,
|
|
385
|
+
parent_id: None,
|
|
386
|
+
parent_type: ParentType::System,
|
|
387
|
+
role: MessageRole::System,
|
|
388
|
+
content: content.clone(),
|
|
389
|
+
parts: vec![MessagePart::Text { text: content }],
|
|
390
|
+
created_at: Utc::now(),
|
|
391
|
+
updated_at: None,
|
|
392
|
+
deleted_at: None,
|
|
393
|
+
metadata: MessageMetadata::default(),
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/// Check if message is soft-deleted
|
|
398
|
+
pub fn is_deleted(&self) -> bool {
|
|
399
|
+
self.deleted_at.is_some()
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/// Set the parent message
|
|
403
|
+
pub fn with_parent(mut self, parent_id: MessageId, parent_type: ParentType) -> Self {
|
|
404
|
+
self.parent_id = Some(parent_id);
|
|
405
|
+
self.parent_type = parent_type;
|
|
406
|
+
self
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// Add rich content parts
|
|
410
|
+
pub fn with_parts(mut self, parts: Vec<MessagePart>) -> Self {
|
|
411
|
+
self.parts = parts;
|
|
412
|
+
self
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/// Set metadata
|
|
416
|
+
pub fn with_metadata(mut self, metadata: MessageMetadata) -> Self {
|
|
417
|
+
self.metadata = metadata;
|
|
418
|
+
self
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// =============================================================================
|
|
423
|
+
// MessageStore Trait
|
|
424
|
+
// =============================================================================
|
|
425
|
+
|
|
426
|
+
/// MessageStore trait - thread/message persistence
|
|
427
|
+
///
|
|
428
|
+
/// This is the core persistence trait for user-facing message storage.
|
|
429
|
+
/// Unlike EventStore (immutable audit log), MessageStore supports:
|
|
430
|
+
/// - Soft delete (GDPR compliance)
|
|
431
|
+
/// - Message editing (optional)
|
|
432
|
+
/// - Thread management
|
|
433
|
+
#[async_trait]
|
|
434
|
+
pub trait MessageStore: StorageBackend {
|
|
435
|
+
// =========================================================================
|
|
436
|
+
// Thread Operations
|
|
437
|
+
// =========================================================================
|
|
438
|
+
|
|
439
|
+
/// Create a new thread
|
|
440
|
+
async fn create_thread(&self, thread: Thread) -> anyhow::Result<ThreadId>;
|
|
441
|
+
|
|
442
|
+
/// Get a thread by ID
|
|
443
|
+
async fn get_thread(&self, thread_id: &ThreadId) -> anyhow::Result<Option<Thread>>;
|
|
444
|
+
|
|
445
|
+
/// Update thread (title, etc.)
|
|
446
|
+
async fn update_thread(&self, thread: Thread) -> anyhow::Result<()>;
|
|
447
|
+
|
|
448
|
+
/// Soft-delete a thread (sets deleted_at)
|
|
449
|
+
async fn delete_thread(&self, thread_id: &ThreadId) -> anyhow::Result<()>;
|
|
450
|
+
|
|
451
|
+
/// List threads for a user
|
|
452
|
+
async fn list_threads(
|
|
453
|
+
&self,
|
|
454
|
+
tenant_id: &TenantId,
|
|
455
|
+
user_id: &UserId,
|
|
456
|
+
limit: usize,
|
|
457
|
+
offset: usize,
|
|
458
|
+
) -> anyhow::Result<Vec<Thread>>;
|
|
459
|
+
|
|
460
|
+
// =========================================================================
|
|
461
|
+
// Message Operations
|
|
462
|
+
// =========================================================================
|
|
463
|
+
|
|
464
|
+
/// Create a new message in a thread
|
|
465
|
+
async fn create_message(&self, message: Message) -> anyhow::Result<MessageId>;
|
|
466
|
+
|
|
467
|
+
/// Get a message by ID
|
|
468
|
+
async fn get_message(&self, message_id: &MessageId) -> anyhow::Result<Option<Message>>;
|
|
469
|
+
|
|
470
|
+
/// Update a message (content, metadata)
|
|
471
|
+
async fn update_message(&self, message: Message) -> anyhow::Result<()>;
|
|
472
|
+
|
|
473
|
+
/// Soft-delete a message
|
|
474
|
+
async fn delete_message(&self, message_id: &MessageId) -> anyhow::Result<()>;
|
|
475
|
+
|
|
476
|
+
/// Get all messages in a thread (ordered by created_at)
|
|
477
|
+
async fn list_messages(
|
|
478
|
+
&self,
|
|
479
|
+
thread_id: &ThreadId,
|
|
480
|
+
include_deleted: bool,
|
|
481
|
+
) -> anyhow::Result<Vec<Message>>;
|
|
482
|
+
|
|
483
|
+
/// Get messages by execution ID
|
|
484
|
+
async fn get_messages_by_execution(
|
|
485
|
+
&self,
|
|
486
|
+
execution_id: &ExecutionId,
|
|
487
|
+
) -> anyhow::Result<Vec<Message>>;
|
|
488
|
+
|
|
489
|
+
// =========================================================================
|
|
490
|
+
// Convenience Methods
|
|
491
|
+
// =========================================================================
|
|
492
|
+
|
|
493
|
+
/// Get or create a thread (implicit thread creation)
|
|
494
|
+
async fn get_or_create_thread(
|
|
495
|
+
&self,
|
|
496
|
+
tenant_id: TenantId,
|
|
497
|
+
user_id: UserId,
|
|
498
|
+
) -> anyhow::Result<Thread> {
|
|
499
|
+
// Default: just create a new thread
|
|
500
|
+
let thread = Thread::new(tenant_id, user_id);
|
|
501
|
+
self.create_thread(thread.clone()).await?;
|
|
502
|
+
Ok(thread)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/// Count messages in a thread
|
|
506
|
+
async fn count_messages(&self, thread_id: &ThreadId) -> anyhow::Result<u64> {
|
|
507
|
+
let messages = self.list_messages(thread_id, false).await?;
|
|
508
|
+
Ok(messages.len() as u64)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// =============================================================================
|
|
513
|
+
// InMemoryMessageStore
|
|
514
|
+
// =============================================================================
|
|
515
|
+
|
|
516
|
+
/// In-memory implementation of MessageStore
|
|
517
|
+
///
|
|
518
|
+
/// Thread-safe storage using RwLock. Suitable for single-node deployments
|
|
519
|
+
/// and testing. For persistence across restarts, use SQLite implementation.
|
|
520
|
+
#[derive(Default)]
|
|
521
|
+
pub struct InMemoryMessageStore {
|
|
522
|
+
threads: RwLock<HashMap<String, Thread>>,
|
|
523
|
+
messages: RwLock<HashMap<String, Message>>,
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
impl InMemoryMessageStore {
|
|
527
|
+
/// Create a new empty message store
|
|
528
|
+
pub fn new() -> Self {
|
|
529
|
+
Self {
|
|
530
|
+
threads: RwLock::new(HashMap::new()),
|
|
531
|
+
messages: RwLock::new(HashMap::new()),
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/// Create an Arc-wrapped instance for sharing
|
|
536
|
+
pub fn shared() -> std::sync::Arc<Self> {
|
|
537
|
+
std::sync::Arc::new(Self::new())
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
#[async_trait]
|
|
542
|
+
impl StorageBackend for InMemoryMessageStore {
|
|
543
|
+
fn name(&self) -> &str {
|
|
544
|
+
"in-memory-message-store"
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
fn requires_network(&self) -> bool {
|
|
548
|
+
false
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async fn health_check(&self) -> anyhow::Result<()> {
|
|
552
|
+
Ok(())
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
#[async_trait]
|
|
557
|
+
impl MessageStore for InMemoryMessageStore {
|
|
558
|
+
async fn create_thread(&self, thread: Thread) -> anyhow::Result<ThreadId> {
|
|
559
|
+
let id = thread.id.clone();
|
|
560
|
+
let mut guard = self.threads.write().expect("lock poisoned");
|
|
561
|
+
guard.insert(id.to_string(), thread);
|
|
562
|
+
Ok(id)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async fn get_thread(&self, thread_id: &ThreadId) -> anyhow::Result<Option<Thread>> {
|
|
566
|
+
let guard = self.threads.read().expect("lock poisoned");
|
|
567
|
+
Ok(guard.get(&thread_id.to_string()).cloned())
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async fn update_thread(&self, thread: Thread) -> anyhow::Result<()> {
|
|
571
|
+
let mut guard = self.threads.write().expect("lock poisoned");
|
|
572
|
+
guard.insert(thread.id.to_string(), thread);
|
|
573
|
+
Ok(())
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async fn delete_thread(&self, thread_id: &ThreadId) -> anyhow::Result<()> {
|
|
577
|
+
let mut guard = self.threads.write().expect("lock poisoned");
|
|
578
|
+
if let Some(thread) = guard.get_mut(&thread_id.to_string()) {
|
|
579
|
+
thread.deleted_at = Some(Utc::now());
|
|
580
|
+
}
|
|
581
|
+
Ok(())
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async fn list_threads(
|
|
585
|
+
&self,
|
|
586
|
+
tenant_id: &TenantId,
|
|
587
|
+
user_id: &UserId,
|
|
588
|
+
limit: usize,
|
|
589
|
+
offset: usize,
|
|
590
|
+
) -> anyhow::Result<Vec<Thread>> {
|
|
591
|
+
let guard = self.threads.read().expect("lock poisoned");
|
|
592
|
+
let mut threads: Vec<_> = guard
|
|
593
|
+
.values()
|
|
594
|
+
.filter(|t| {
|
|
595
|
+
t.tenant_id == *tenant_id
|
|
596
|
+
&& t.user_id == *user_id
|
|
597
|
+
&& t.deleted_at.is_none()
|
|
598
|
+
})
|
|
599
|
+
.cloned()
|
|
600
|
+
.collect();
|
|
601
|
+
|
|
602
|
+
// Sort by created_at descending (newest first)
|
|
603
|
+
threads.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
|
604
|
+
|
|
605
|
+
Ok(threads.into_iter().skip(offset).take(limit).collect())
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async fn create_message(&self, message: Message) -> anyhow::Result<MessageId> {
|
|
609
|
+
let id = message.id.clone();
|
|
610
|
+
let thread_id = message.thread_id.clone();
|
|
611
|
+
|
|
612
|
+
// Update thread's updated_at
|
|
613
|
+
{
|
|
614
|
+
let mut thread_guard = self.threads.write().expect("lock poisoned");
|
|
615
|
+
if let Some(thread) = thread_guard.get_mut(&thread_id.to_string()) {
|
|
616
|
+
thread.updated_at = Utc::now();
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
let mut guard = self.messages.write().expect("lock poisoned");
|
|
621
|
+
guard.insert(id.to_string(), message);
|
|
622
|
+
Ok(id)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async fn get_message(&self, message_id: &MessageId) -> anyhow::Result<Option<Message>> {
|
|
626
|
+
let guard = self.messages.read().expect("lock poisoned");
|
|
627
|
+
Ok(guard.get(&message_id.to_string()).cloned())
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async fn update_message(&self, mut message: Message) -> anyhow::Result<()> {
|
|
631
|
+
message.updated_at = Some(Utc::now());
|
|
632
|
+
let mut guard = self.messages.write().expect("lock poisoned");
|
|
633
|
+
guard.insert(message.id.to_string(), message);
|
|
634
|
+
Ok(())
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async fn delete_message(&self, message_id: &MessageId) -> anyhow::Result<()> {
|
|
638
|
+
let mut guard = self.messages.write().expect("lock poisoned");
|
|
639
|
+
if let Some(message) = guard.get_mut(&message_id.to_string()) {
|
|
640
|
+
message.deleted_at = Some(Utc::now());
|
|
641
|
+
}
|
|
642
|
+
Ok(())
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async fn list_messages(
|
|
646
|
+
&self,
|
|
647
|
+
thread_id: &ThreadId,
|
|
648
|
+
include_deleted: bool,
|
|
649
|
+
) -> anyhow::Result<Vec<Message>> {
|
|
650
|
+
let guard = self.messages.read().expect("lock poisoned");
|
|
651
|
+
let mut messages: Vec<_> = guard
|
|
652
|
+
.values()
|
|
653
|
+
.filter(|m| {
|
|
654
|
+
m.thread_id == *thread_id
|
|
655
|
+
&& (include_deleted || m.deleted_at.is_none())
|
|
656
|
+
})
|
|
657
|
+
.cloned()
|
|
658
|
+
.collect();
|
|
659
|
+
|
|
660
|
+
// Sort by created_at ascending
|
|
661
|
+
messages.sort_by(|a, b| a.created_at.cmp(&b.created_at));
|
|
662
|
+
|
|
663
|
+
Ok(messages)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async fn get_messages_by_execution(
|
|
667
|
+
&self,
|
|
668
|
+
execution_id: &ExecutionId,
|
|
669
|
+
) -> anyhow::Result<Vec<Message>> {
|
|
670
|
+
let guard = self.messages.read().expect("lock poisoned");
|
|
671
|
+
let messages: Vec<_> = guard
|
|
672
|
+
.values()
|
|
673
|
+
.filter(|m| m.execution_id.as_ref() == Some(execution_id))
|
|
674
|
+
.cloned()
|
|
675
|
+
.collect();
|
|
676
|
+
Ok(messages)
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
#[cfg(test)]
|
|
681
|
+
mod tests {
|
|
682
|
+
use super::*;
|
|
683
|
+
|
|
684
|
+
fn test_tenant() -> TenantId {
|
|
685
|
+
TenantId::from_string("tenant_test")
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
fn test_user() -> UserId {
|
|
689
|
+
UserId::from_string("user_test")
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
#[tokio::test]
|
|
693
|
+
async fn test_create_thread() {
|
|
694
|
+
let store = InMemoryMessageStore::new();
|
|
695
|
+
let thread = Thread::new(test_tenant(), test_user());
|
|
696
|
+
let id = thread.id.clone();
|
|
697
|
+
|
|
698
|
+
store.create_thread(thread).await.unwrap();
|
|
699
|
+
|
|
700
|
+
let loaded = store.get_thread(&id).await.unwrap();
|
|
701
|
+
assert!(loaded.is_some());
|
|
702
|
+
assert_eq!(loaded.unwrap().id, id);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
#[tokio::test]
|
|
706
|
+
async fn test_soft_delete_thread() {
|
|
707
|
+
let store = InMemoryMessageStore::new();
|
|
708
|
+
let thread = Thread::new(test_tenant(), test_user());
|
|
709
|
+
let id = thread.id.clone();
|
|
710
|
+
|
|
711
|
+
store.create_thread(thread).await.unwrap();
|
|
712
|
+
store.delete_thread(&id).await.unwrap();
|
|
713
|
+
|
|
714
|
+
let loaded = store.get_thread(&id).await.unwrap().unwrap();
|
|
715
|
+
assert!(loaded.is_deleted());
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
#[tokio::test]
|
|
719
|
+
async fn test_list_threads_excludes_deleted() {
|
|
720
|
+
let store = InMemoryMessageStore::new();
|
|
721
|
+
let tenant = test_tenant();
|
|
722
|
+
let user = test_user();
|
|
723
|
+
|
|
724
|
+
// Create two threads
|
|
725
|
+
let thread1 = Thread::new(tenant.clone(), user.clone());
|
|
726
|
+
let thread2 = Thread::new(tenant.clone(), user.clone());
|
|
727
|
+
let id2 = thread2.id.clone();
|
|
728
|
+
|
|
729
|
+
store.create_thread(thread1).await.unwrap();
|
|
730
|
+
store.create_thread(thread2).await.unwrap();
|
|
731
|
+
|
|
732
|
+
// Delete one
|
|
733
|
+
store.delete_thread(&id2).await.unwrap();
|
|
734
|
+
|
|
735
|
+
// List should only return 1
|
|
736
|
+
let threads = store.list_threads(&tenant, &user, 100, 0).await.unwrap();
|
|
737
|
+
assert_eq!(threads.len(), 1);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
#[tokio::test]
|
|
741
|
+
async fn test_create_message() {
|
|
742
|
+
let store = InMemoryMessageStore::new();
|
|
743
|
+
let thread = Thread::new(test_tenant(), test_user());
|
|
744
|
+
let thread_id = thread.id.clone();
|
|
745
|
+
store.create_thread(thread).await.unwrap();
|
|
746
|
+
|
|
747
|
+
let message = Message::user(thread_id.clone(), "Hello!");
|
|
748
|
+
let msg_id = message.id.clone();
|
|
749
|
+
store.create_message(message).await.unwrap();
|
|
750
|
+
|
|
751
|
+
let loaded = store.get_message(&msg_id).await.unwrap();
|
|
752
|
+
assert!(loaded.is_some());
|
|
753
|
+
assert_eq!(loaded.unwrap().content, "Hello!");
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
#[tokio::test]
|
|
757
|
+
async fn test_message_parent_chain() {
|
|
758
|
+
let store = InMemoryMessageStore::new();
|
|
759
|
+
let thread = Thread::new(test_tenant(), test_user());
|
|
760
|
+
let thread_id = thread.id.clone();
|
|
761
|
+
store.create_thread(thread).await.unwrap();
|
|
762
|
+
|
|
763
|
+
// User message
|
|
764
|
+
let user_msg = Message::user(thread_id.clone(), "What's the weather?");
|
|
765
|
+
let user_msg_id = user_msg.id.clone();
|
|
766
|
+
store.create_message(user_msg).await.unwrap();
|
|
767
|
+
|
|
768
|
+
// Assistant response
|
|
769
|
+
let exec_id = ExecutionId::new();
|
|
770
|
+
let assistant_msg = Message::assistant(
|
|
771
|
+
thread_id.clone(),
|
|
772
|
+
exec_id,
|
|
773
|
+
"The weather is sunny.",
|
|
774
|
+
Some(user_msg_id.clone()),
|
|
775
|
+
);
|
|
776
|
+
store.create_message(assistant_msg).await.unwrap();
|
|
777
|
+
|
|
778
|
+
// List messages
|
|
779
|
+
let messages = store.list_messages(&thread_id, false).await.unwrap();
|
|
780
|
+
assert_eq!(messages.len(), 2);
|
|
781
|
+
assert_eq!(messages[0].role, MessageRole::User);
|
|
782
|
+
assert_eq!(messages[1].role, MessageRole::Assistant);
|
|
783
|
+
assert_eq!(messages[1].parent_id, Some(user_msg_id));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
#[tokio::test]
|
|
787
|
+
async fn test_soft_delete_message() {
|
|
788
|
+
let store = InMemoryMessageStore::new();
|
|
789
|
+
let thread = Thread::new(test_tenant(), test_user());
|
|
790
|
+
let thread_id = thread.id.clone();
|
|
791
|
+
store.create_thread(thread).await.unwrap();
|
|
792
|
+
|
|
793
|
+
let message = Message::user(thread_id.clone(), "Delete me");
|
|
794
|
+
let msg_id = message.id.clone();
|
|
795
|
+
store.create_message(message).await.unwrap();
|
|
796
|
+
|
|
797
|
+
store.delete_message(&msg_id).await.unwrap();
|
|
798
|
+
|
|
799
|
+
// Should not appear in normal list
|
|
800
|
+
let messages = store.list_messages(&thread_id, false).await.unwrap();
|
|
801
|
+
assert_eq!(messages.len(), 0);
|
|
802
|
+
|
|
803
|
+
// Should appear with include_deleted
|
|
804
|
+
let all_messages = store.list_messages(&thread_id, true).await.unwrap();
|
|
805
|
+
assert_eq!(all_messages.len(), 1);
|
|
806
|
+
assert!(all_messages[0].is_deleted());
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
#[tokio::test]
|
|
810
|
+
async fn test_get_messages_by_execution() {
|
|
811
|
+
let store = InMemoryMessageStore::new();
|
|
812
|
+
let thread = Thread::new(test_tenant(), test_user());
|
|
813
|
+
let thread_id = thread.id.clone();
|
|
814
|
+
store.create_thread(thread).await.unwrap();
|
|
815
|
+
|
|
816
|
+
let exec_id = ExecutionId::new();
|
|
817
|
+
let msg = Message::assistant(thread_id, exec_id.clone(), "Response", None);
|
|
818
|
+
store.create_message(msg).await.unwrap();
|
|
819
|
+
|
|
820
|
+
let messages = store.get_messages_by_execution(&exec_id).await.unwrap();
|
|
821
|
+
assert_eq!(messages.len(), 1);
|
|
822
|
+
assert_eq!(messages[0].execution_id, Some(exec_id));
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
#[tokio::test]
|
|
826
|
+
async fn test_message_with_parts() {
|
|
827
|
+
let store = InMemoryMessageStore::new();
|
|
828
|
+
let thread = Thread::new(test_tenant(), test_user());
|
|
829
|
+
let thread_id = thread.id.clone();
|
|
830
|
+
store.create_thread(thread).await.unwrap();
|
|
831
|
+
|
|
832
|
+
let exec_id = ExecutionId::new();
|
|
833
|
+
let parts = vec![
|
|
834
|
+
MessagePart::Reasoning {
|
|
835
|
+
text: "Let me think...".to_string(),
|
|
836
|
+
},
|
|
837
|
+
MessagePart::ToolCall {
|
|
838
|
+
tool_call_id: "tc_123".to_string(),
|
|
839
|
+
tool_name: "get_weather".to_string(),
|
|
840
|
+
args: serde_json::json!({"city": "NYC"}),
|
|
841
|
+
},
|
|
842
|
+
MessagePart::Text {
|
|
843
|
+
text: "The weather is sunny.".to_string(),
|
|
844
|
+
},
|
|
845
|
+
];
|
|
846
|
+
|
|
847
|
+
let msg = Message::assistant(thread_id, exec_id, "The weather is sunny.", None)
|
|
848
|
+
.with_parts(parts);
|
|
849
|
+
let msg_id = msg.id.clone();
|
|
850
|
+
store.create_message(msg).await.unwrap();
|
|
851
|
+
|
|
852
|
+
let loaded = store.get_message(&msg_id).await.unwrap().unwrap();
|
|
853
|
+
assert_eq!(loaded.parts.len(), 3);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
#[tokio::test]
|
|
857
|
+
async fn test_message_metadata() {
|
|
858
|
+
let store = InMemoryMessageStore::new();
|
|
859
|
+
let thread = Thread::new(test_tenant(), test_user());
|
|
860
|
+
let thread_id = thread.id.clone();
|
|
861
|
+
store.create_thread(thread).await.unwrap();
|
|
862
|
+
|
|
863
|
+
let exec_id = ExecutionId::new();
|
|
864
|
+
let metadata = MessageMetadata {
|
|
865
|
+
model: Some("gpt-4o".to_string()),
|
|
866
|
+
provider: Some("azure".to_string()),
|
|
867
|
+
duration_ms: Some(1500),
|
|
868
|
+
token_usage: Some(TokenUsage::new(100, 200)),
|
|
869
|
+
stats: Some(ExecutionStats {
|
|
870
|
+
llm_calls: 2,
|
|
871
|
+
tool_calls: 1,
|
|
872
|
+
sub_agents: 0,
|
|
873
|
+
steps: 3,
|
|
874
|
+
decisions: 1,
|
|
875
|
+
artifacts: 0,
|
|
876
|
+
}),
|
|
877
|
+
finish_reason: Some(FinishReason::Stop),
|
|
878
|
+
..Default::default()
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
let msg = Message::assistant(thread_id, exec_id, "Response", None)
|
|
882
|
+
.with_metadata(metadata);
|
|
883
|
+
let msg_id = msg.id.clone();
|
|
884
|
+
store.create_message(msg).await.unwrap();
|
|
885
|
+
|
|
886
|
+
let loaded = store.get_message(&msg_id).await.unwrap().unwrap();
|
|
887
|
+
assert_eq!(loaded.metadata.model, Some("gpt-4o".to_string()));
|
|
888
|
+
assert_eq!(loaded.metadata.stats.as_ref().unwrap().llm_calls, 2);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
#[tokio::test]
|
|
892
|
+
async fn test_thread_updated_on_message() {
|
|
893
|
+
let store = InMemoryMessageStore::new();
|
|
894
|
+
let thread = Thread::new(test_tenant(), test_user());
|
|
895
|
+
let thread_id = thread.id.clone();
|
|
896
|
+
let original_updated = thread.updated_at;
|
|
897
|
+
store.create_thread(thread).await.unwrap();
|
|
898
|
+
|
|
899
|
+
// Small delay to ensure timestamp differs
|
|
900
|
+
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
|
901
|
+
|
|
902
|
+
let message = Message::user(thread_id.clone(), "Hello!");
|
|
903
|
+
store.create_message(message).await.unwrap();
|
|
904
|
+
|
|
905
|
+
let loaded_thread = store.get_thread(&thread_id).await.unwrap().unwrap();
|
|
906
|
+
assert!(loaded_thread.updated_at > original_updated);
|
|
907
|
+
}
|
|
908
|
+
}
|