diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52be7aa..c0a21c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,8 @@ env: RUSTFLAGS: "-D warnings" RUST_BACKTRACE: 1 CARGO_INCREMENTAL: 0 - SCCACHE_GHA_ENABLED: "true" - RUSTC_WRAPPER: "sccache" + # Override .cargo/config.toml target-cpu=native to prevent SIGILL on different runners + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS: "" jobs: # =========================================================================== @@ -36,26 +36,28 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + with: + toolchain: stable - name: Configure sccache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@676c0e67b665684f17941acf5cc3af83bcf10228 # v0.0.6 - name: Cache Rust artifacts - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 with: prefix-key: "v1-build" shared-key: "${{ matrix.os }}" cache-on-failure: true - name: Build - run: cargo build --release --locked + run: cargo build --release - name: Build (all features) - run: cargo build --release --all-features --locked + run: cargo build --release --all-features # =========================================================================== # GATE 2: Lint with Clippy @@ -65,24 +67,25 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable with: + toolchain: stable components: clippy - name: Configure sccache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@676c0e67b665684f17941acf5cc3af83bcf10228 # v0.0.6 - name: Cache Rust artifacts - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 with: prefix-key: "v1-lint" cache-on-failure: true - name: Run Clippy - run: cargo clippy --all-targets --all-features --locked -- -D warnings + run: cargo clippy --all-targets --all-features -- -D warnings # =========================================================================== # GATE 3: Format Check @@ -92,11 +95,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable with: + toolchain: stable components: rustfmt - name: Check formatting @@ -114,26 +118,28 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + with: + toolchain: stable - name: Configure sccache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@676c0e67b665684f17941acf5cc3af83bcf10228 # v0.0.6 - name: Cache Rust artifacts - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 with: prefix-key: "v1-test" shared-key: "${{ matrix.os }}" cache-on-failure: true - name: Run tests - run: cargo test --all-features --locked + run: cargo test --all-features - name: Run doc tests - run: cargo test --doc --locked + run: cargo test --doc # =========================================================================== # Documentation Build @@ -143,24 +149,29 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + # Use nightly for docsrs feature flags (doc_cfg, doc_auto_cfg) + - name: Install Rust toolchain (nightly for docsrs features) + uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master + with: + toolchain: nightly - name: Configure sccache - uses: mozilla-actions/sccache-action@v0.0.6 + uses: mozilla-actions/sccache-action@676c0e67b665684f17941acf5cc3af83bcf10228 # v0.0.6 - name: Cache Rust artifacts - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 with: prefix-key: "v1-docs" cache-on-failure: true + # Use --features compression to match docs.rs metadata configuration + # (avoids local-embeddings which requires ONNX runtime not in docs.rs) - name: Build documentation - run: cargo doc --no-deps --all-features --locked + run: cargo doc --no-deps --features compression env: - RUSTDOCFLAGS: "-D warnings" + RUSTDOCFLAGS: "-D warnings --cfg docsrs" # =========================================================================== # Security Audit @@ -170,10 +181,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + with: + toolchain: stable - name: Install cargo-audit run: cargo install cargo-audit --locked diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce2682a..00b39c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ on: workflow_dispatch: inputs: confirm: - description: 'This workflow is deprecated. Use reasonkit/.github/workflows/release.yml instead.' + description: "This workflow is deprecated. Use reasonkit/.github/workflows/release.yml instead." required: true type: boolean default: false diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index db1ffba..32c000b 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -21,10 +21,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable + with: + toolchain: stable - name: Install cargo-audit run: cargo install cargo-audit --locked @@ -40,10 +42,10 @@ jobs: checks: [advisories, licenses, bans, sources] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Run cargo-deny (${{ matrix.checks }}) - uses: EmbarkStudios/cargo-deny-action@v1 + uses: EmbarkStudios/cargo-deny-action@ef301417264190a1eb9f26fcf171642070085c5b # v1 with: log-level: warn command: check ${{ matrix.checks }} @@ -53,12 +55,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 - name: Run Gitleaks - uses: gitleaks/gitleaks-action@v2 + uses: gitleaks/gitleaks-action@dcedce43c6f43de0b836d1fe38946645c9c638dc # v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -68,10 +70,10 @@ jobs: if: github.event_name == 'pull_request' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Dependency Review - uses: actions/dependency-review-action@v4 + uses: actions/dependency-review-action@46a3c492319c890177366b6ef46d6b4f89743ed4 # v4 with: fail-on-severity: moderate deny-licenses: GPL-3.0, AGPL-3.0 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 1f76fc6..6ecb990 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -2,7 +2,7 @@ name: Stale Issues on: schedule: - - cron: "0 0 * * *" # Daily at midnight + - cron: "0 0 * * *" # Daily at midnight permissions: issues: write @@ -12,7 +12,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9 with: stale-issue-message: | This issue has been automatically marked as stale because it has not had recent activity. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4792d6b..acd137a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,4 +51,4 @@ Open an issue or reach out at [team@reasonkit.sh](mailto:team@reasonkit.sh). --- -*Part of the ReasonKit Ecosystem* +_Part of the ReasonKit Ecosystem_ diff --git a/Cargo.toml b/Cargo.toml index 60aa552..001c8ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "reasonkit-mem" -version = "0.1.0" +version = "0.1.5" edition = "2021" -rust-version = "1.74" +rust-version = "1.75" authors = ["ReasonKit Team "] description = "High-performance vector database & RAG memory layer - hybrid search, embeddings, RAPTOR trees, BM25 fusion, and semantic retrieval for AI systems" license = "Apache-2.0" @@ -47,7 +47,7 @@ targets = [ "aarch64-apple-darwin", "x86_64-pc-windows-msvc", ] -cargo-args = ["--locked"] +# Note: --locked removed because workspace members don't have their own Cargo.lock [badges] maintenance = { status = "actively-developed" } @@ -81,6 +81,25 @@ harness = false name = "storage_bench" harness = false +# ============================================================================ +# BINARIES +# ============================================================================ + +[[bin]] +name = "vdream_mcp" +path = "src/bin/vdream_mcp.rs" +required-features = ["vdreamteam"] + +[[bin]] +name = "vdream_cli" +path = "src/bin/vdream_cli.rs" +required-features = ["vdreamteam"] + +[[bin]] +name = "rkmem_server" +path = "src/bin/rkmem_server.rs" +required-features = ["http-server"] + # ============================================================================ # DEPENDENCIES # ============================================================================ @@ -104,7 +123,7 @@ rmp-serde = "1.3" qdrant-client = "1.10" # Full-text Search (Tantivy BM25) -tantivy = "0.22" +tantivy = "0.25" # Embedded key-value store (pure Rust) sled = "0.34" @@ -115,6 +134,10 @@ tokio = { version = "1", features = ["full"] } # HTTP Client (for Qdrant health checks) reqwest = { version = "0.12", features = ["json"] } +# HTTP Server (for API sidecar mode) +axum = { version = "0.7", optional = true } +tower-http = { version = "0.5", features = ["cors", "trace"], optional = true } + # Error Handling anyhow = "1.0" thiserror = "1.0" @@ -124,6 +147,7 @@ async-trait = "0.1" # Logging tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["fmt"], optional = true } # UUID uuid = { version = "1", features = ["v4", "serde"] } @@ -156,17 +180,20 @@ crossbeam-utils = "0.8" dirs = "5" # ONNX Runtime for local embeddings (BGE-M3) -ort = { version = "2.0.0-rc.10", optional = true } +ort = { version = "2.0.0-rc.11", optional = true } # HuggingFace tokenizers -tokenizers = { version = "0.19", optional = true } +tokenizers = { version = "0.21", optional = true } -# NDArray for tensor operations -ndarray = { version = "0.16", optional = true } +# NDArray for tensor operations (must match ort's ndarray version) +ndarray = { version = "0.17", optional = true } # LZ4 compression (optional, for dual-layer memory) lz4_flex = { version = "0.11", optional = true } +# YAML serialization (optional, for vDreamTeam memory system) +serde_yaml = { version = "0.9", optional = true } + # ============================================================================ # DEV DEPENDENCIES # ============================================================================ @@ -195,5 +222,15 @@ local-embeddings = ["ort", "tokenizers", "ndarray"] # LZ4 compression for dual-layer memory architecture compression = ["lz4_flex"] +# vDreamTeam AI Agent Memory System +# Provides structured memory for AI agent teams with: +# - Constitutional layer (shared identity, constraints) +# - Role-specific memory (per-agent decisions, lessons, PxP logs) +# - Cross-agent coordination logging +vdreamteam = ["serde_yaml"] + +# HTTP API Server for external RAG integration +http-server = ["axum", "tower-http", "tracing-subscriber"] + # Full feature set -full = ["python", "local-embeddings", "compression"] +full = ["python", "local-embeddings", "compression", "vdreamteam", "http-server"] diff --git a/README.md b/README.md index f73ffef..e3bde2e 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,15 @@ **Memory & Retrieval Infrastructure for ReasonKit** -[![CI](https://img.shields.io/github/actions/workflow/status/reasonkit/reasonkit-mem/ci.yml?branch=main&style=flat-square&logo=github&label=CI&color=06b6d4)](https://github.com/reasonkit/reasonkit-mem/actions/workflows/ci.yml) -[![Security](https://img.shields.io/github/actions/workflow/status/reasonkit/reasonkit-mem/security.yml?branch=main&style=flat-square&logo=github&label=Security&color=10b981)](https://github.com/reasonkit/reasonkit-mem/actions/workflows/security.yml) -[![Crates.io](https://img.shields.io/crates/v/reasonkit-mem?style=flat-square&color=%2306b6d4)](https://crates.io/crates/reasonkit-mem) -[![docs.rs](https://img.shields.io/docsrs/reasonkit-mem?style=flat-square&color=%2310b981)](https://docs.rs/reasonkit-mem) -[![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square&color=%23a855f7)](./LICENSE) -[![Rust](https://img.shields.io/badge/rust-1.74+-orange?style=flat-square&logo=rust&color=%23f97316)](https://www.rust-lang.org/) +[![CI](https://img.shields.io/github/actions/workflow/status/reasonkit/reasonkit-mem/ci.yml?branch=main&style=flat-square&logo=github&label=CI&color=06b6d4&logoColor=06b6d4)](https://github.com/reasonkit/reasonkit-mem/actions/workflows/ci.yml) +[![Security](https://img.shields.io/github/actions/workflow/status/reasonkit/reasonkit-mem/security.yml?branch=main&style=flat-square&logo=github&label=Security&color=10b981&logoColor=10b981)](https://github.com/reasonkit/reasonkit-mem/actions/workflows/security.yml) +[![Crates.io](https://img.shields.io/crates/v/reasonkit-mem?style=flat-square&logo=rust&color=10b981&logoColor=f9fafb)](https://crates.io/crates/reasonkit-mem) +[![docs.rs](https://img.shields.io/docsrs/reasonkit-mem?style=flat-square&logo=docs.rs&color=06b6d4&logoColor=f9fafb)](https://docs.rs/reasonkit-mem) +[![Downloads](https://img.shields.io/crates/d/reasonkit-mem?style=flat-square&color=ec4899&logo=rust&logoColor=f9fafb)](https://crates.io/crates/reasonkit-mem) +[![License](https://img.shields.io/badge/license-Apache%202.0-a855f7?style=flat-square&labelColor=030508)](./LICENSE) +[![Rust](https://img.shields.io/badge/rust-1.75+-f97316?style=flat-square&logo=rust&logoColor=f9fafb)](https://www.rust-lang.org/) -*The Long-Term Memory Layer ("Hippocampus") for AI Reasoning* +_The Long-Term Memory Layer ("Hippocampus") for AI Reasoning_ [Documentation](https://docs.rs/reasonkit-mem) | [ReasonKit Core](https://github.com/ReasonKit/reasonkit-core) | [Website](https://reasonkit.sh) @@ -31,6 +32,23 @@ ## Installation +### Universal Installer (Recommended) + +**Installs all 4 ReasonKit projects together:** + +```bash +curl -fsSL https://get.reasonkit.sh | bash -s -- --with-memory +``` + +**Platform & Shell Support:** + +- ✅ All platforms (Linux/macOS/Windows/WSL) +- ✅ All shells (Bash/Zsh/Fish/Nu/PowerShell/Elvish) +- ✅ Auto-detects shell and configures PATH +- ✅ Beautiful progress visualization + +### Cargo (Rust Library) + Add to your `Cargo.toml`: ```toml @@ -150,23 +168,24 @@ For detailed information about embedded mode, see [docs/EMBEDDED_MODE_GUIDE.md]( ## Architecture -![ReasonKit Mem Hybrid Architecture](https://reasonkit.sh/assets/brand/mem/hybrid_architecture.png) -![ReasonKit Mem Hybrid Architecture Technical Diagram](https://reasonkit.sh/assets/brand/mem/hybrid_retrieval_engine.svg) +![ReasonKit Mem Hybrid Architecture](./brand/readme/hybrid_architecture.png) +![ReasonKit Mem Hybrid Architecture Technical Diagram](./brand/readme/hybrid_retrieval_engine.svg) ### The RAPTOR Algorithm (Hierarchical Indexing) ReasonKit Mem implements **RAPTOR** (Recursive Abstractive Processing for Tree-Organized Retrieval) to answer high-level questions across large document sets. -![ReasonKit Mem RAPTOR Tree Structure](https://reasonkit.sh/assets/brand/mem/raptor_tree_structure.svg) -![ReasonKit Mem RAPTOR Tree](https://reasonkit.sh/assets/brand/mem/raptor_tree.png) +![ReasonKit Mem RAPTOR Tree Structure](./brand/readme/raptor_tree_structure.svg) + +![ReasonKit Mem RAPTOR Tree](./brand/readme/raptor_tree.png) ### The Memory Dashboard -![ReasonKit Mem Dashboard](https://reasonkit.sh/assets/brand/mem/memory_dashboard.png) +![ReasonKit Mem Dashboard](./brand/readme/memory_dashboard.png) ### Integration Ecosystem -![ReasonKit Mem Ecosystem](https://reasonkit.sh/assets/brand/mem/mem_ecosystem.png) +![ReasonKit Mem Ecosystem](./brand/readme/mem_ecosystem.png) ## Technology Stack @@ -275,14 +294,14 @@ use reasonkit_mem::retrieval::{ ## Version & Maturity -| Component | Status | Notes | -|-----------|--------|-------| -| **Vector Storage** | ✅ Stable | Qdrant integration production-ready | -| **Hybrid Search** | ✅ Stable | Dense + Sparse fusion working | -| **RAPTOR Trees** | ✅ Stable | Hierarchical retrieval implemented | -| **Embeddings** | ✅ Stable | OpenAI API fully supported | -| **Local Embeddings** | 🔶 Beta | BGE-M3 ONNX (enable with `local-embeddings` feature) | -| **Python Bindings** | 🔶 Beta | Build from source with `--features python` | +| Component | Status | Notes | +| -------------------- | --------- | ---------------------------------------------------- | +| **Vector Storage** | ✅ Stable | Qdrant integration production-ready | +| **Hybrid Search** | ✅ Stable | Dense + Sparse fusion working | +| **RAPTOR Trees** | ✅ Stable | Hierarchical retrieval implemented | +| **Embeddings** | ✅ Stable | OpenAI API fully supported | +| **Local Embeddings** | 🔶 Beta | BGE-M3 ONNX (enable with `local-embeddings` feature) | +| **Python Bindings** | 🔶 Beta | Build from source with `--features python` | **Current Version:** v0.1.2 | [CHANGELOG](CHANGELOG.md) | [Releases](https://github.com/reasonkit/reasonkit-mem/releases) @@ -308,12 +327,12 @@ Apache License 2.0 - see [LICENSE](https://github.com/reasonkit/reasonkit-mem/bl
-![ReasonKit Ecosystem Connection](https://reasonkit.sh/assets/brand/mem/ecosystem_connection.png) +![ReasonKit Ecosystem Connection](./brand/readme/ecosystem_connection.png) **Part of the ReasonKit Ecosystem** [ReasonKit Core](https://github.com/reasonkit/reasonkit-core) | [ReasonKit Web](https://github.com/reasonkit/reasonkit-web) | [Website](https://reasonkit.sh) -*"See How Your AI Thinks"* +_"See How Your AI Thinks"_
diff --git a/SUPPORT.md b/SUPPORT.md index 86e4482..9461ab1 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -33,12 +33,12 @@ For enterprise support, SLAs, or consulting, contact **enterprise@reasonkit.sh** ## Response Times -| Channel | Expected Response | -|---------|-------------------| -| Security issues | < 24 hours | -| Bug reports | < 48 hours | -| Feature requests | < 1 week | -| Discussions | Community-driven | +| Channel | Expected Response | +| ---------------- | ----------------- | +| Security issues | < 24 hours | +| Bug reports | < 48 hours | +| Feature requests | < 1 week | +| Discussions | Community-driven | --- diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..2cadb7d --- /dev/null +++ b/deny.toml @@ -0,0 +1,115 @@ +# cargo-deny configuration for reasonkit-mem +# Security, license, and dependency validation +# Reference: https://embarkstudios.github.io/cargo-deny/ +# Compatible with cargo-deny 0.18.x + +[graph] +targets = [ + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", + "aarch64-apple-darwin", + "x86_64-pc-windows-msvc", +] +all-features = true + +[output] +feature-depth = 1 + +[advisories] +# Path where advisory database is cloned +db-path = "~/.cargo/advisory-dbs" +# Ignore specific advisories if assessed as non-applicable +ignore = [ + # Unmaintained crates - tracking migration in backlog + { id = "RUSTSEC-2024-0384", reason = "instant crate - used by parking_lot, tracking migration to web-time" }, + { id = "RUSTSEC-2025-0119", reason = "number_prefix - low risk, used by indicatif, tracking migration" }, + { id = "RUSTSEC-2024-0436", reason = "paste - used by tokenizers, tracking migration to pastey" }, + { id = "RUSTSEC-2025-0134", reason = "rustls-pemfile - used by tonic, tracking migration to rustls-pki-types" }, +] + +[licenses] +# Apache 2.0 compatible licenses +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-DFS-2016", + "Unicode-3.0", + "Zlib", + "CC0-1.0", + "0BSD", + "BSL-1.0", + "MPL-2.0", + "Unlicense", + "OpenSSL", # Used by ring crate - permissive license +] +confidence-threshold = 0.8 + +# Exceptions for specific crates +exceptions = [ + # ring uses a complex license expression + { allow = ["ISC", "MIT", "OpenSSL"], crate = "ring" }, +] + +[licenses.private] +# Ignore unpublished workspace crates +ignore = true +registries = [] + +# License clarifications for crates with non-standard license files +[[licenses.clarify]] +crate = "ring" +expression = "ISC AND MIT AND OpenSSL" +license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] + +[[licenses.clarify]] +crate = "webpki" +expression = "ISC" +license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] + +[[licenses.clarify]] +crate = "rustls-webpki" +expression = "ISC" +license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] + +[bans] +# Dependency banning and duplicate detection +multiple-versions = "warn" +wildcards = "deny" +highlight = "all" +workspace-default-features = "allow" +external-default-features = "allow" + +# Ban certain crates we want to avoid +deny = [ + # Deprecated crates + { crate = "rustc-serialize", reason = "Use serde instead" }, + { crate = "quickersort", reason = "Use standard library sort" }, + { crate = "failure", reason = "Use thiserror or anyhow" }, + { crate = "error-chain", reason = "Use thiserror or anyhow" }, + { crate = "tempdir", reason = "Use tempfile instead" }, + { crate = "term", reason = "Use termcolor or crossterm" }, + { crate = "gcc", reason = "Use cc instead" }, +] + +# Skip checking these crates for duplicates +skip = [] + +# Skip trees rooted at these crates +skip-tree = [] + +[sources] +# Source code origin validation +unknown-registry = "deny" +unknown-git = "warn" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] + +[sources.allow-org] +# Allow GitHub organizations +github = [] +gitlab = [] +bitbucket = [] diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f1dec76..3f704b2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -835,4 +835,4 @@ REASONKIT_CONNECTION_POOL_SIZE=10 --- -*Document maintained by ReasonKit Team. For questions, see [CONTRIBUTING.md](../CONTRIBUTING.md).* +_Document maintained by ReasonKit Team. For questions, see [CONTRIBUTING.md](../CONTRIBUTING.md)._ diff --git a/docs/DUAL_LAYER_STORAGE_API_DESIGN.md b/docs/DUAL_LAYER_STORAGE_API_DESIGN.md index 39b0811..68f6edd 100644 --- a/docs/DUAL_LAYER_STORAGE_API_DESIGN.md +++ b/docs/DUAL_LAYER_STORAGE_API_DESIGN.md @@ -1942,12 +1942,12 @@ let fast_wal = WalConfig { ## File Locations -| Artifact | Path | -| --------------- | ------------------------------------------- | -| Design Document | `docs/DUAL_LAYER_STORAGE_API_DESIGN.md` | -| Implementation | `src/storage/dual_layer.rs` | -| Tests | `tests/dual_layer_tests.rs` | -| Benchmarks | `benches/dual_layer_bench.rs` | +| Artifact | Path | +| --------------- | --------------------------------------- | +| Design Document | `docs/DUAL_LAYER_STORAGE_API_DESIGN.md` | +| Implementation | `src/storage/dual_layer.rs` | +| Tests | `tests/dual_layer_tests.rs` | +| Benchmarks | `benches/dual_layer_bench.rs` | --- @@ -1959,4 +1959,4 @@ let fast_wal = WalConfig { --- -*This design follows ReasonKit's Rust-first philosophy and is compatible with the existing `StorageBackend` trait while providing enhanced capabilities for dual-layer memory management.* +_This design follows ReasonKit's Rust-first philosophy and is compatible with the existing `StorageBackend` trait while providing enhanced capabilities for dual-layer memory management._ diff --git a/docs/DUAL_LAYER_STORAGE_ARCHITECTURE.md b/docs/DUAL_LAYER_STORAGE_ARCHITECTURE.md index 032f5c1..63af673 100644 --- a/docs/DUAL_LAYER_STORAGE_ARCHITECTURE.md +++ b/docs/DUAL_LAYER_STORAGE_ARCHITECTURE.md @@ -1966,5 +1966,5 @@ fn cosine_distance(a: &[f32], b: &[f32]) -> f32 { --- -*Document generated for ReasonKit-mem v0.2.0* -*Architecture designed for Debian 13+ compatibility* +_Document generated for ReasonKit-mem v0.2.0_ +_Architecture designed for Debian 13+ compatibility_ diff --git a/docs/STORAGE_API.md b/docs/STORAGE_API.md index 2f00911..2c7a970 100644 --- a/docs/STORAGE_API.md +++ b/docs/STORAGE_API.md @@ -1529,5 +1529,5 @@ use reasonkit_mem::storage::{ --- -*This documentation is part of the ReasonKit Memory Infrastructure.* -*Report issues at: * +_This documentation is part of the ReasonKit Memory Infrastructure._ +_Report issues at: _ diff --git a/docs/data_models.md b/docs/data_models.md index 57bf17e..da7b711 100644 --- a/docs/data_models.md +++ b/docs/data_models.md @@ -25,30 +25,30 @@ struct MemoryUnit { Stores sequences of events or interactions. -* **Structure:** Time-ordered list of `MemoryUnit`s. -* **Use Case:** Chat history, activity logs. -* **Indexing:** Chronological + Semantic. +- **Structure:** Time-ordered list of `MemoryUnit`s. +- **Use Case:** Chat history, activity logs. +- **Indexing:** Chronological + Semantic. ### 3. Semantic Memory Stores facts, concepts, and generalized knowledge. -* **Structure:** Graph-based or clustered vector space. -* **Use Case:** "What is the capital of France?", "User prefers dark mode". -* **Indexing:** RAPTOR (Recursive Abstractive Processing for Tree-Organized Retrieval). +- **Structure:** Graph-based or clustered vector space. +- **Use Case:** "What is the capital of France?", "User prefers dark mode". +- **Indexing:** RAPTOR (Recursive Abstractive Processing for Tree-Organized Retrieval). ## RAPTOR Tree Structure For large knowledge bases, we use a RAPTOR tree: -* **Leaf Nodes:** Original chunks of text (`MemoryUnit`). -* **Parent Nodes:** Summaries of child nodes. -* **Root Node:** High-level summary of the entire cluster/document. +- **Leaf Nodes:** Original chunks of text (`MemoryUnit`). +- **Parent Nodes:** Summaries of child nodes. +- **Root Node:** High-level summary of the entire cluster/document. Retrieval traverses this tree to find the right level of abstraction for a query. ## Vector Schema -* **Dimensions:** 1536 (default, compatible with OpenAI text-embedding-3-small) or 768 (local models). -* **Metric:** Cosine Similarity. -* **Engine:** Qdrant / pgvector (pluggable). +- **Dimensions:** 1536 (default, compatible with OpenAI text-embedding-3-small) or 768 (local models). +- **Metric:** Cosine Similarity. +- **Engine:** Qdrant / pgvector (pluggable). diff --git a/docs/persistence.md b/docs/persistence.md index 0533ed9..194e240 100644 --- a/docs/persistence.md +++ b/docs/persistence.md @@ -8,31 +8,31 @@ ReasonKit Memory supports a dual-layer persistence strategy: Hot (Fast) and Cold Designed for sub-millisecond retrieval during active reasoning. -* **Technology:** Qdrant (primary), or pgvector (PostgreSQL). -* **Data:** Embeddings, metadata, recent ephemeral context. -* **Retention:** Configurable (e.g., last 30 days or active working set). +- **Technology:** Qdrant (primary), or pgvector (PostgreSQL). +- **Data:** Embeddings, metadata, recent ephemeral context. +- **Retention:** Configurable (e.g., last 30 days or active working set). ## 2. Cold Storage (Object/Relational) Designed for durability, audit trails, and full reconstruction. -* **Technology:** SQLite (local), PostgreSQL (server), or S3-compatible Blob Storage. -* **Data:** Full raw text, original documents, complete conversation logs, snapshots of the vector state. -* **Format:** Parquet (for analytics) or JSONL (for portability). +- **Technology:** SQLite (local), PostgreSQL (server), or S3-compatible Blob Storage. +- **Data:** Full raw text, original documents, complete conversation logs, snapshots of the vector state. +- **Format:** Parquet (for analytics) or JSONL (for portability). ## Sync Strategy 1. **Write Path:** - * Agent writes to `MemoryInterface`. - * System writes to **Cold Storage** (WAL/Log) immediately for durability. - * System asynchronously computes embeddings and updates **Hot Storage**. + - Agent writes to `MemoryInterface`. + - System writes to **Cold Storage** (WAL/Log) immediately for durability. + - System asynchronously computes embeddings and updates **Hot Storage**. 2. **Read Path:** - * Query hits **Hot Storage** (Vector Index). - * If payload is missing/truncated in Hot, fetch full content from **Cold Storage** using ID. + - Query hits **Hot Storage** (Vector Index). + - If payload is missing/truncated in Hot, fetch full content from **Cold Storage** using ID. ## Backup & Recovery -* **Snapshotting:** Qdrant snapshots are taken daily. -* **PITR:** PostgreSQL Point-in-Time Recovery is enabled for the Cold layer. -* **Export:** `reasonkit-mem export --format jsonl` allows dumping the entire memory state for migration. +- **Snapshotting:** Qdrant snapshots are taken daily. +- **PITR:** PostgreSQL Point-in-Time Recovery is enabled for the Cold layer. +- **Export:** `reasonkit-mem export --format jsonl` allows dumping the entire memory state for migration. diff --git a/src/bin/vdream_cli.rs b/src/bin/vdream_cli.rs new file mode 100644 index 0000000..645297c --- /dev/null +++ b/src/bin/vdream_cli.rs @@ -0,0 +1,394 @@ +//! vDreamTeam CLI +//! +//! Command-line interface for vDreamTeam memory operations. +//! +//! # Usage +//! +//! ```bash +//! # Build +//! cargo build --bin vdream_cli --features vdreamteam +//! +//! # Run commands +//! vdream_cli stats # Show memory statistics +//! vdream_cli query constitutional identity # Query constitutional layer +//! vdream_cli query role vcto decisions # Query role decisions +//! vdream_cli check CONS-001 # Check constraint +//! vdream_cli log-pxp vcto "Decision context" # Log PxP event +//! ``` + +use std::env; +use std::path::PathBuf; + +#[cfg(feature = "vdreamteam")] +use reasonkit_mem::vdreamteam::{Consultation, PxPEntry, VDreamMemory}; + +#[cfg(feature = "vdreamteam")] +#[tokio::main] +async fn main() -> Result<(), Box> { + let args: Vec = env::args().collect(); + + if args.len() < 2 { + print_usage(); + return Ok(()); + } + + let agents_path = env::var("VDREAM_AGENTS_PATH") + .map(PathBuf::from) + .unwrap_or_else(|_| { + if let Some(home) = dirs::home_dir() { + let rk_agents = home.join("RK-PROJECT").join(".agents"); + if rk_agents.exists() { + return rk_agents; + } + } + PathBuf::from(".agents") + }); + + let command = args[1].as_str(); + + match command { + "stats" => cmd_stats(&agents_path).await?, + "query" => { + if args.len() < 4 { + eprintln!( + "Usage: vdream_cli query [query_type]" + ); + return Ok(()); + } + cmd_query( + &agents_path, + &args[2], + &args[3], + args.get(4).map(|s| s.as_str()), + ) + .await?; + } + "check" => { + if args.len() < 3 { + eprintln!("Usage: vdream_cli check [proposed_action]"); + return Ok(()); + } + cmd_check(&agents_path, &args[2], args.get(3).map(|s| s.as_str())).await?; + } + "log-pxp" => { + if args.len() < 4 { + eprintln!("Usage: vdream_cli log-pxp [model]"); + return Ok(()); + } + cmd_log_pxp( + &agents_path, + &args[2], + &args[3], + args.get(4).map(|s| s.as_str()), + ) + .await?; + } + "roles" => cmd_roles(&agents_path).await?, + "constraints" => cmd_constraints(&agents_path).await?, + "help" | "-h" | "--help" => print_usage(), + _ => { + eprintln!("Unknown command: {}", command); + print_usage(); + } + } + + Ok(()) +} + +#[cfg(feature = "vdreamteam")] +fn print_usage() { + println!( + r#"vDreamTeam CLI - AI Agent Memory System + +USAGE: + vdream_cli [OPTIONS] + +COMMANDS: + stats Show memory statistics + query constitutional
Query constitutional layer + Sections: identity, constraints, boundaries, quality_gates, consultation, all + query role [query_type] Query role-specific memory + Query types: decisions, lessons, consultations, skills, all + check [action] Check if action violates constraint + log-pxp [model] Log a PxP consultation event + roles List available roles + constraints List all constraints + help Show this help + +ENVIRONMENT: + VDREAM_AGENTS_PATH Path to .agents directory (default: ~/RK-PROJECT/.agents) + +EXAMPLES: + vdream_cli stats + vdream_cli query constitutional identity + vdream_cli query role vcto decisions + vdream_cli check CONS-001 "Using Node.js for MCP server" + vdream_cli log-pxp vcto "Architecture decision" deepseek-v3.2 + vdream_cli roles +"# + ); +} + +#[cfg(feature = "vdreamteam")] +async fn cmd_stats(agents_path: &PathBuf) -> Result<(), Box> { + let memory = VDreamMemory::load(agents_path).await?; + + println!("vDreamTeam Memory Statistics"); + println!("============================"); + println!("Agents Path: {}", agents_path.display()); + println!(); + + // Constitutional layer + match memory.constitutional().await { + Ok(constitutional) => { + println!("Constitutional Layer: LOADED"); + println!(" Identity: {}", constitutional.identity.organization.name); + println!( + " Constraints: {} defined", + constitutional.constraints.len() + ); + println!( + " Boundaries: {} OSS, {} proprietary, {} never-OSS", + constitutional.boundaries.oss_projects.len(), + constitutional.boundaries.proprietary_projects.len(), + constitutional.boundaries.never_oss.len() + ); + println!( + " Quality Gates: {} defined", + constitutional.quality_gates.len() + ); + } + Err(e) => { + println!("Constitutional Layer: ERROR - {}", e); + } + } + println!(); + + // Role stats + let role_ids = memory.role_ids(); + println!("Roles: {} found", role_ids.len()); + for role_id in &role_ids { + if let Ok(stats) = memory.pxp_stats(role_id).await { + println!( + " {} - {} consultations, models: {:?}", + role_id, + stats.total_consultations, + stats.model_usage.keys().collect::>() + ); + } + } + + Ok(()) +} + +#[cfg(feature = "vdreamteam")] +async fn cmd_query( + agents_path: &PathBuf, + target: &str, + section_or_role: &str, + query_type: Option<&str>, +) -> Result<(), Box> { + let memory = VDreamMemory::load(agents_path).await?; + + match target { + "constitutional" => { + let constitutional = memory.constitutional().await?; + match section_or_role { + "identity" => { + println!( + "{}", + serde_json::to_string_pretty(&constitutional.identity)? + ); + } + "constraints" => { + println!( + "{}", + serde_json::to_string_pretty(&constitutional.constraints)? + ); + } + "boundaries" => { + println!( + "{}", + serde_json::to_string_pretty(&constitutional.boundaries)? + ); + } + "quality_gates" => { + println!( + "{}", + serde_json::to_string_pretty(&constitutional.quality_gates)? + ); + } + "consultation" => { + println!( + "{}", + serde_json::to_string_pretty(&constitutional.consultation)? + ); + } + "all" => { + println!("{}", serde_json::to_string_pretty(&constitutional)?); + } + _ => { + eprintln!("Unknown constitutional section: {}", section_or_role); + eprintln!( + "Available: identity, constraints, boundaries, quality_gates, consultation, all" + ); + } + } + } + "role" => { + let role_id = section_or_role; + let qtype = query_type.unwrap_or("all"); + + match memory.role(role_id).await? { + Some(role_memory) => match qtype { + "decisions" => { + println!("{}", serde_json::to_string_pretty(&role_memory.decisions)?); + } + "lessons" => { + println!("{}", serde_json::to_string_pretty(&role_memory.lessons)?); + } + "consultations" => { + println!("{}", serde_json::to_string_pretty(&role_memory.pxp_log)?); + } + "skills" => { + println!("{}", serde_json::to_string_pretty(&role_memory.skills)?); + } + "all" => { + println!("{}", serde_json::to_string_pretty(&role_memory)?); + } + _ => { + eprintln!("Unknown query type: {}", qtype); + eprintln!("Available: decisions, lessons, consultations, skills, all"); + } + }, + None => { + eprintln!("Role not found: {}", role_id); + eprintln!("Available roles: {:?}", memory.role_ids()); + } + } + } + _ => { + eprintln!("Unknown target: {}", target); + eprintln!("Available: constitutional, role"); + } + } + + Ok(()) +} + +#[cfg(feature = "vdreamteam")] +async fn cmd_check( + agents_path: &PathBuf, + constraint_id: &str, + proposed_action: Option<&str>, +) -> Result<(), Box> { + let memory = VDreamMemory::load(agents_path).await?; + + match memory.check_constraint(constraint_id).await? { + Some(constraint) => { + println!("Constraint: {}", constraint.id); + println!("Description: {}", constraint.description); + println!("Enforcement: {}", constraint.enforcement); + if !constraint.consequence.is_empty() { + println!("Consequence: {}", constraint.consequence); + } + + if let Some(action) = proposed_action { + println!(); + println!("Proposed Action: {}", action); + println!("---"); + println!( + "NOTE: This constraint has {} enforcement.", + constraint.enforcement + ); + println!("Evaluate the proposed action against the constraint description."); + } + } + None => { + eprintln!("Constraint not found: {}", constraint_id); + let constitutional = memory.constitutional().await?; + let available: Vec<_> = constitutional.constraints.keys().collect(); + eprintln!("Available constraints: {:?}", available); + } + } + + Ok(()) +} + +#[cfg(feature = "vdreamteam")] +async fn cmd_log_pxp( + agents_path: &PathBuf, + role_id: &str, + decision_context: &str, + model: Option<&str>, +) -> Result<(), Box> { + let mut memory = VDreamMemory::load(agents_path).await?; + + let model_name = model.unwrap_or("claude-opus-4.5"); + + let entry = PxPEntry::new(role_id, decision_context).add_consultation(Consultation { + model: model_name.to_string(), + cli_command: format!("vdream_cli log-pxp {} ...", role_id), + prompt_summary: format!("CLI PxP log: {}", decision_context), + response_summary: "Logged via CLI".to_string(), + confidence: 0.8, + }); + + memory.log_pxp(entry).await?; + + println!("PxP entry logged successfully!"); + println!(" Role: {}", role_id); + println!(" Context: {}", decision_context); + println!(" Model: {}", model_name); + + Ok(()) +} + +#[cfg(feature = "vdreamteam")] +async fn cmd_roles(agents_path: &PathBuf) -> Result<(), Box> { + let memory = VDreamMemory::load(agents_path).await?; + + println!("Available Roles:"); + println!("================"); + for role_id in memory.role_ids() { + if let Ok(Some(role)) = memory.role(role_id).await { + let decision_count = role.decisions.entries.len(); + let lesson_count = role.lessons.entries.len(); + let pxp_count = role.pxp_log.entries.len(); + println!( + " {} - {} decisions, {} lessons, {} consultations", + role_id, decision_count, lesson_count, pxp_count + ); + } else { + println!(" {}", role_id); + } + } + + Ok(()) +} + +#[cfg(feature = "vdreamteam")] +async fn cmd_constraints(agents_path: &PathBuf) -> Result<(), Box> { + let memory = VDreamMemory::load(agents_path).await?; + let constitutional = memory.constitutional().await?; + + println!("Constraints:"); + println!("============"); + for (id, constraint) in &constitutional.constraints { + println!(" {} [{}]", id, constraint.enforcement); + println!(" {}", constraint.description); + if !constraint.consequence.is_empty() { + println!(" Consequence: {}", constraint.consequence); + } + println!(); + } + + Ok(()) +} + +#[cfg(not(feature = "vdreamteam"))] +fn main() { + eprintln!("Error: vdreamteam feature is not enabled."); + eprintln!("Run with: cargo run --bin vdream_cli --features vdreamteam"); + std::process::exit(1); +} diff --git a/src/bin/vdream_mcp.rs b/src/bin/vdream_mcp.rs new file mode 100644 index 0000000..c11f4f1 --- /dev/null +++ b/src/bin/vdream_mcp.rs @@ -0,0 +1,71 @@ +//! vDreamTeam MCP Server Binary +//! +//! Runs the vDreamTeam memory MCP server over stdio. +//! +//! # Usage +//! +//! ```bash +//! # Run directly +//! cargo run --bin vdream_mcp --features vdreamteam +//! +//! # Or install and run +//! cargo install --path . --features vdreamteam +//! vdream_mcp +//! ``` +//! +//! # MCP Configuration (claude_desktop_config.json) +//! +//! ```json +//! { +//! "mcpServers": { +//! "vdreamteam": { +//! "command": "vdream_mcp", +//! "args": [], +//! "env": { +//! "VDREAM_AGENTS_PATH": "/path/to/.agents" +//! } +//! } +//! } +//! } +//! ``` + +use std::env; +use std::path::PathBuf; + +#[cfg(feature = "vdreamteam")] +use reasonkit_mem::vdreamteam::VDreamMCPServer; + +#[cfg(feature = "vdreamteam")] +#[tokio::main] +async fn main() -> Result<(), Box> { + // Get agents path from environment or use default + let agents_path = env::var("VDREAM_AGENTS_PATH") + .map(PathBuf::from) + .unwrap_or_else(|_| { + // Default: ~/RK-PROJECT/.agents or ./.agents + if let Some(home) = dirs::home_dir() { + let rk_agents = home.join("RK-PROJECT").join(".agents"); + if rk_agents.exists() { + return rk_agents; + } + } + PathBuf::from(".agents") + }); + + eprintln!( + "vDreamTeam MCP Server starting with agents path: {}", + agents_path.display() + ); + + let mut server = VDreamMCPServer::new(agents_path); + server.run_stdio().await?; + + Ok(()) +} + +#[cfg(not(feature = "vdreamteam"))] +fn main() { + eprintln!("Error: vdreamteam feature is not enabled."); + eprintln!("Run with: cargo run --bin vdream_mcp --features vdreamteam"); + std::process::exit(1); +} diff --git a/src/indexing/mod.rs b/src/indexing/mod.rs index 85e550e..3c4a8cf 100644 --- a/src/indexing/mod.rs +++ b/src/indexing/mod.rs @@ -208,19 +208,19 @@ impl BM25Index { let doc_id = retrieved_doc .get_first(self.fields.doc_id) - .and_then(|v: &tantivy::schema::OwnedValue| v.as_str()) + .and_then(|v| v.as_str()) .map(|s: &str| s.to_string()) .unwrap_or_default(); let chunk_id = retrieved_doc .get_first(self.fields.chunk_id) - .and_then(|v: &tantivy::schema::OwnedValue| v.as_str()) + .and_then(|v| v.as_str()) .map(|s: &str| s.to_string()) .unwrap_or_default(); let text = retrieved_doc .get_first(self.fields.text) - .and_then(|v: &tantivy::schema::OwnedValue| v.as_str()) + .and_then(|v| v.as_str()) .map(|s: &str| s.to_string()) .unwrap_or_default(); @@ -313,19 +313,19 @@ impl BM25Index { let doc_id_str = retrieved_doc .get_first(self.fields.doc_id) - .and_then(|v: &tantivy::schema::OwnedValue| v.as_str()) + .and_then(|v| v.as_str()) .map(|s: &str| s.to_string()) .unwrap_or_default(); let chunk_id_str = retrieved_doc .get_first(self.fields.chunk_id) - .and_then(|v: &tantivy::schema::OwnedValue| v.as_str()) + .and_then(|v| v.as_str()) .map(|s: &str| s.to_string()) .unwrap_or_default(); let text = retrieved_doc .get_first(self.fields.text) - .and_then(|v: &tantivy::schema::OwnedValue| v.as_str()) + .and_then(|v| v.as_str()) .map(|s: &str| s.to_string()) .unwrap_or_default(); diff --git a/src/lib.rs b/src/lib.rs index dd32bd5..6fedde8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -524,6 +524,37 @@ pub mod rag; /// ``` pub mod service; +/// vDreamTeam AI Agent Memory System. +/// +/// This module provides structured memory for AI agent teams, enabling: +/// - Constitutional layer (shared identity, constraints, governance) +/// - Role-specific memory (per-agent decisions, lessons, PxP logs) +/// - Cross-agent coordination logging +/// +/// Requires the `vdreamteam` feature flag. +/// +/// # Example +/// +/// ```rust,ignore +/// use reasonkit_mem::vdreamteam::{VDreamMemory, PxPEntry, Consultation}; +/// +/// let mut memory = VDreamMemory::load(".agents").await?; +/// +/// let entry = PxPEntry::new("vcto", "Architecture decision") +/// .add_consultation(Consultation { +/// model: "deepseek-v3.2".to_string(), +/// cli_command: "ollama run deepseek-v3.2:cloud".to_string(), +/// prompt_summary: "Validate design".to_string(), +/// response_summary: "Design approved".to_string(), +/// confidence: 0.90, +/// }); +/// +/// memory.log_pxp(entry).await?; +/// ``` +#[cfg(feature = "vdreamteam")] +#[cfg_attr(docsrs, doc(cfg(feature = "vdreamteam")))] +pub mod vdreamteam; + /// Prelude for convenient imports. /// /// This module re-exports the most commonly used types for convenience. diff --git a/src/vdreamteam/mcp_server.rs b/src/vdreamteam/mcp_server.rs new file mode 100644 index 0000000..760079e --- /dev/null +++ b/src/vdreamteam/mcp_server.rs @@ -0,0 +1,783 @@ +//! vDreamTeam MCP Server +//! +//! Model Context Protocol server for vDreamTeam memory queries. +//! Provides tools for AI agents to query and interact with the +//! constitutional and role-specific memory layers. +//! +//! # Tools +//! +//! - `vdream_query_constitutional`: Query constitutional layer (identity, constraints) +//! - `vdream_query_role`: Query role-specific memory (decisions, lessons, PxP logs) +//! - `vdream_log_pxp`: Log a PxP consultation event +//! - `vdream_log_decision`: Record an agent decision +//! - `vdream_get_stats`: Get memory statistics +//! - `vdream_check_constraint`: Check if an action violates constraints +//! +//! # Transport +//! +//! Uses JSON-RPC 2.0 over stdio (standard MCP transport). + +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::io::{self, BufRead, Write}; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::RwLock; + +use super::{Consultation, Decision, MemResult, PxPEntry, VDreamMemory}; + +/// MCP Server version +pub const MCP_VERSION: &str = "2024-11-05"; + +/// Server info for MCP protocol +#[derive(Debug, Clone, Serialize)] +pub struct ServerInfo { + pub name: String, + pub version: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, +} + +/// Server capabilities +#[derive(Debug, Clone, Serialize)] +pub struct ServerCapabilities { + pub tools: ToolsCapability, + #[serde(skip_serializing_if = "Option::is_none")] + pub resources: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ToolsCapability { + pub list_changed: bool, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ResourcesCapability { + pub subscribe: bool, + pub list_changed: bool, +} + +/// Tool definition for MCP +#[derive(Debug, Clone, Serialize)] +pub struct ToolDefinition { + pub name: String, + pub description: String, + #[serde(rename = "inputSchema")] + pub input_schema: Value, +} + +/// Tool execution result +#[derive(Debug, Clone, Serialize)] +pub struct ToolResult { + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_error: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ContentBlock { + #[serde(rename = "type")] + pub content_type: String, + pub text: String, +} + +impl ToolResult { + pub fn success(text: impl Into) -> Self { + Self { + content: Some(vec![ContentBlock { + content_type: "text".to_string(), + text: text.into(), + }]), + is_error: None, + } + } + + pub fn error(text: impl Into) -> Self { + Self { + content: Some(vec![ContentBlock { + content_type: "text".to_string(), + text: text.into(), + }]), + is_error: Some(true), + } + } +} + +/// JSON-RPC Request +#[derive(Debug, Deserialize)] +pub struct JsonRpcRequest { + pub jsonrpc: String, + pub id: Option, + pub method: String, + #[serde(default)] + pub params: Value, +} + +/// JSON-RPC Response +#[derive(Debug, Serialize)] +pub struct JsonRpcResponse { + pub jsonrpc: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, Serialize)] +pub struct JsonRpcError { + pub code: i32, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +/// vDreamTeam MCP Server +pub struct VDreamMCPServer { + memory: Arc>>, + agents_path: PathBuf, + initialized: bool, +} + +impl VDreamMCPServer { + /// Create new MCP server instance + pub fn new(agents_path: impl Into) -> Self { + Self { + memory: Arc::new(RwLock::new(None)), + agents_path: agents_path.into(), + initialized: false, + } + } + + /// Initialize the server (load memory) + pub async fn initialize(&mut self) -> MemResult<()> { + let memory = VDreamMemory::load(&self.agents_path).await?; + *self.memory.write().await = Some(memory); + self.initialized = true; + Ok(()) + } + + /// Get server info + pub fn get_server_info(&self) -> ServerInfo { + ServerInfo { + name: "vDreamTeam Memory Server".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + description: Some( + "MCP server for vDreamTeam AI agent memory - constitutional and role-specific layers" + .to_string(), + ), + } + } + + /// Get server capabilities + pub fn get_capabilities(&self) -> ServerCapabilities { + ServerCapabilities { + tools: ToolsCapability { + list_changed: false, + }, + resources: Some(ResourcesCapability { + subscribe: false, + list_changed: false, + }), + } + } + + /// List available tools + pub fn list_tools(&self) -> Vec { + vec![ + ToolDefinition { + name: "vdream_query_constitutional".to_string(), + description: "Query the constitutional layer (identity, constraints, boundaries, quality gates)".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "section": { + "type": "string", + "enum": ["identity", "constraints", "boundaries", "quality_gates", "consultation", "all"], + "description": "Which constitutional section to query" + } + }, + "required": ["section"] + }), + }, + ToolDefinition { + name: "vdream_query_role".to_string(), + description: "Query role-specific memory (decisions, lessons, consultations, skills)".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "role_id": { + "type": "string", + "description": "Role ID (e.g., 'vceo', 'vcto', 'lead_core')" + }, + "query_type": { + "type": "string", + "enum": ["decisions", "lessons", "consultations", "skills", "all"], + "description": "What type of memory to query" + }, + "limit": { + "type": "integer", + "description": "Maximum entries to return (default: 10)" + } + }, + "required": ["role_id", "query_type"] + }), + }, + ToolDefinition { + name: "vdream_log_pxp".to_string(), + description: "Log a PxP (Prompt x Prompt) consultation event".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "role_id": { + "type": "string", + "description": "Role ID that performed the consultation" + }, + "decision_context": { + "type": "string", + "description": "Context for the decision being made" + }, + "model": { + "type": "string", + "description": "Model consulted (e.g., 'deepseek-v3.2', 'claude-opus-4.5')" + }, + "prompt_summary": { + "type": "string", + "description": "Brief summary of the prompt" + }, + "response_summary": { + "type": "string", + "description": "Brief summary of the response" + }, + "confidence": { + "type": "number", + "description": "Confidence level 0.0-1.0" + } + }, + "required": ["role_id", "decision_context", "model", "prompt_summary", "response_summary", "confidence"] + }), + }, + ToolDefinition { + name: "vdream_log_decision".to_string(), + description: "Record an agent decision".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "role_id": { + "type": "string", + "description": "Role ID making the decision" + }, + "title": { + "type": "string", + "description": "Decision title" + }, + "context": { + "type": "string", + "description": "Decision context" + }, + "alternatives": { + "type": "array", + "items": { "type": "string" }, + "description": "Alternatives considered" + }, + "rationale": { + "type": "string", + "description": "Reasoning for the decision" + }, + "outcome": { + "type": "string", + "description": "Outcome or result" + } + }, + "required": ["role_id", "title", "context", "rationale"] + }), + }, + ToolDefinition { + name: "vdream_get_stats".to_string(), + description: "Get vDreamTeam memory statistics".to_string(), + input_schema: json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, + ToolDefinition { + name: "vdream_check_constraint".to_string(), + description: "Check if an action would violate a constraint".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "constraint_id": { + "type": "string", + "description": "Constraint ID to check (e.g., 'CONS-001')" + }, + "proposed_action": { + "type": "string", + "description": "Description of the proposed action" + } + }, + "required": ["constraint_id", "proposed_action"] + }), + }, + ] + } + + /// Handle a tool call + pub async fn call_tool(&self, name: &str, arguments: Value) -> ToolResult { + match name { + "vdream_query_constitutional" => self.handle_query_constitutional(arguments).await, + "vdream_query_role" => self.handle_query_role(arguments).await, + "vdream_log_pxp" => self.handle_log_pxp(arguments).await, + "vdream_log_decision" => self.handle_log_decision(arguments).await, + "vdream_get_stats" => self.handle_get_stats().await, + "vdream_check_constraint" => self.handle_check_constraint(arguments).await, + _ => ToolResult::error(format!("Unknown tool: {}", name)), + } + } + + async fn handle_query_constitutional(&self, args: Value) -> ToolResult { + let section = args + .get("section") + .and_then(|v| v.as_str()) + .unwrap_or("all"); + let memory_guard = self.memory.read().await; + let memory = match memory_guard.as_ref() { + Some(m) => m, + None => return ToolResult::error("Memory not initialized"), + }; + + match memory.constitutional().await { + Ok(constitutional) => match section { + "identity" => ToolResult::success( + serde_json::to_string_pretty(&constitutional.identity).unwrap_or_default(), + ), + "constraints" => ToolResult::success( + serde_json::to_string_pretty(&constitutional.constraints).unwrap_or_default(), + ), + "boundaries" => ToolResult::success( + serde_json::to_string_pretty(&constitutional.boundaries).unwrap_or_default(), + ), + "quality_gates" => ToolResult::success( + serde_json::to_string_pretty(&constitutional.quality_gates).unwrap_or_default(), + ), + "consultation" => ToolResult::success( + serde_json::to_string_pretty(&constitutional.consultation).unwrap_or_default(), + ), + "all" => ToolResult::success( + serde_json::to_string_pretty(&constitutional).unwrap_or_default(), + ), + _ => ToolResult::error(format!("Unknown section: {}", section)), + }, + Err(e) => ToolResult::error(format!("Failed to read constitutional: {}", e)), + } + } + + async fn handle_query_role(&self, args: Value) -> ToolResult { + let role_id = match args.get("role_id").and_then(|v| v.as_str()) { + Some(r) => r, + None => return ToolResult::error("role_id is required"), + }; + + let query_type = args + .get("query_type") + .and_then(|v| v.as_str()) + .unwrap_or("all"); + let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize; + + let memory_guard = self.memory.read().await; + let memory = match memory_guard.as_ref() { + Some(m) => m, + None => return ToolResult::error("Memory not initialized"), + }; + + match memory.role(role_id).await { + Ok(Some(role_memory)) => match query_type { + "decisions" => { + let decisions: Vec<_> = + role_memory.decisions.entries.iter().take(limit).collect(); + ToolResult::success( + serde_json::to_string_pretty(&decisions).unwrap_or_default(), + ) + } + "lessons" => { + let lessons: Vec<_> = role_memory.lessons.entries.iter().take(limit).collect(); + ToolResult::success(serde_json::to_string_pretty(&lessons).unwrap_or_default()) + } + "consultations" => { + let pxp: Vec<_> = role_memory.pxp_log.entries.iter().take(limit).collect(); + ToolResult::success(serde_json::to_string_pretty(&pxp).unwrap_or_default()) + } + "skills" => ToolResult::success( + serde_json::to_string_pretty(&role_memory.skills).unwrap_or_default(), + ), + "all" => ToolResult::success( + serde_json::to_string_pretty(&role_memory).unwrap_or_default(), + ), + _ => ToolResult::error(format!("Unknown query_type: {}", query_type)), + }, + Ok(None) => { + let role_ids = memory.role_ids(); + ToolResult::error(format!( + "Role not found: {}. Available: {:?}", + role_id, role_ids + )) + } + Err(e) => ToolResult::error(format!("Failed to query role: {}", e)), + } + } + + async fn handle_log_pxp(&self, args: Value) -> ToolResult { + let role_id = match args.get("role_id").and_then(|v| v.as_str()) { + Some(r) => r, + None => return ToolResult::error("role_id is required"), + }; + + let decision_context = match args.get("decision_context").and_then(|v| v.as_str()) { + Some(c) => c, + None => return ToolResult::error("decision_context is required"), + }; + + let model = args + .get("model") + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); + let prompt_summary = args + .get("prompt_summary") + .and_then(|v| v.as_str()) + .unwrap_or(""); + let response_summary = args + .get("response_summary") + .and_then(|v| v.as_str()) + .unwrap_or(""); + let confidence = args + .get("confidence") + .and_then(|v| v.as_f64()) + .unwrap_or(0.5); + + let entry = PxPEntry::new(role_id, decision_context).add_consultation(Consultation { + model: model.to_string(), + cli_command: "claude -p (via MCP)".to_string(), + prompt_summary: prompt_summary.to_string(), + response_summary: response_summary.to_string(), + confidence, + }); + + let mut memory_guard = self.memory.write().await; + match memory_guard.as_mut() { + Some(memory) => match memory.log_pxp(entry).await { + Ok(()) => ToolResult::success("PxP entry logged successfully"), + Err(e) => ToolResult::error(format!("Failed to log PxP: {}", e)), + }, + None => ToolResult::error("Memory not initialized"), + } + } + + async fn handle_log_decision(&self, args: Value) -> ToolResult { + let role_id = match args.get("role_id").and_then(|v| v.as_str()) { + Some(r) => r.to_string(), + None => return ToolResult::error("role_id is required"), + }; + + let title = match args.get("title").and_then(|v| v.as_str()) { + Some(t) => t.to_string(), + None => return ToolResult::error("title is required"), + }; + + let context = args + .get("context") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + // Parse alternatives (for future use in Decision builder) + let _alternatives: Vec = args + .get("alternatives") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect() + }) + .unwrap_or_default(); + + let rationale = args + .get("rationale") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + + // Parse outcome (for future use in Decision builder) + let _outcome = args + .get("outcome") + .and_then(|v| v.as_str()) + .map(String::from); + let confidence = args.get("confidence").and_then(|v| v.as_f64()); + + let decision = Decision::new(&role_id, &title) + .with_context(&context) + .with_decision(&rationale) + .with_rationale(&rationale); + + let decision = if let Some(conf) = confidence { + decision.with_confidence(conf) + } else { + decision + }; + + let decision = decision.finalize(); + + let mut memory_guard = self.memory.write().await; + match memory_guard.as_mut() { + Some(memory) => match memory.record_decision(decision).await { + Ok(()) => ToolResult::success("Decision recorded successfully"), + Err(e) => ToolResult::error(format!("Failed to record decision: {}", e)), + }, + None => ToolResult::error("Memory not initialized"), + } + } + + async fn handle_get_stats(&self) -> ToolResult { + let memory_guard = self.memory.read().await; + let memory = match memory_guard.as_ref() { + Some(m) => m, + None => return ToolResult::error("Memory not initialized"), + }; + + let role_ids = memory.role_ids(); + let const_loaded = memory.constitutional().await.is_ok(); + + // Collect stats for each role + let mut role_stats = Vec::new(); + for role_id in &role_ids { + if let Ok(stats) = memory.pxp_stats(role_id).await { + role_stats.push(json!({ + "role_id": role_id, + "total_consultations": stats.total_consultations, + "model_usage": stats.model_usage, + })); + } + } + + let stats = json!({ + "constitutional_loaded": const_loaded, + "roles_count": role_ids.len(), + "roles": role_ids, + "role_stats": role_stats, + "agents_path": self.agents_path.display().to_string(), + }); + + ToolResult::success(serde_json::to_string_pretty(&stats).unwrap_or_default()) + } + + async fn handle_check_constraint(&self, args: Value) -> ToolResult { + let constraint_id = match args.get("constraint_id").and_then(|v| v.as_str()) { + Some(c) => c, + None => return ToolResult::error("constraint_id is required"), + }; + + let proposed_action = args + .get("proposed_action") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + let memory_guard = self.memory.read().await; + let memory = match memory_guard.as_ref() { + Some(m) => m, + None => return ToolResult::error("Memory not initialized"), + }; + + match memory.check_constraint(constraint_id).await { + Ok(Some(constraint)) => { + let result = json!({ + "constraint": constraint, + "proposed_action": proposed_action, + "analysis": format!( + "Constraint '{}' with enforcement '{}' should be evaluated against the proposed action.", + constraint.id, constraint.enforcement + ), + }); + ToolResult::success(serde_json::to_string_pretty(&result).unwrap_or_default()) + } + Ok(None) => { + // Get available constraints + match memory.constitutional().await { + Ok(constitutional) => { + let available: Vec<_> = constitutional.constraints.keys().collect(); + ToolResult::error(format!( + "Constraint not found: {}. Available: {:?}", + constraint_id, available + )) + } + Err(_) => ToolResult::error(format!("Constraint not found: {}", constraint_id)), + } + } + Err(e) => ToolResult::error(format!("Failed to check constraint: {}", e)), + } + } + + /// Handle a JSON-RPC request + pub async fn handle_request(&mut self, request: JsonRpcRequest) -> JsonRpcResponse { + match request.method.as_str() { + "initialize" => { + if let Err(e) = self.initialize().await { + return JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id, + result: None, + error: Some(JsonRpcError { + code: -32603, + message: format!("Failed to initialize: {}", e), + data: None, + }), + }; + } + + JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id, + result: Some(json!({ + "protocolVersion": MCP_VERSION, + "capabilities": self.get_capabilities(), + "serverInfo": self.get_server_info(), + })), + error: None, + } + } + + "tools/list" => JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id, + result: Some(json!({ + "tools": self.list_tools() + })), + error: None, + }, + + "tools/call" => { + let name = request + .params + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or(""); + let arguments = request + .params + .get("arguments") + .cloned() + .unwrap_or(json!({})); + + let result = self.call_tool(name, arguments).await; + + JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id, + result: Some(serde_json::to_value(result).unwrap_or(json!({}))), + error: None, + } + } + + "notifications/initialized" | "initialized" => { + // Client notification, no response needed + JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: None, + result: None, + error: None, + } + } + + _ => JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: request.id, + result: None, + error: Some(JsonRpcError { + code: -32601, + message: format!("Method not found: {}", request.method), + data: None, + }), + }, + } + } + + /// Run the MCP server over stdio + pub async fn run_stdio(&mut self) -> io::Result<()> { + let stdin = io::stdin(); + let mut stdout = io::stdout(); + + for line in stdin.lock().lines() { + let line = line?; + if line.is_empty() { + continue; + } + + let request: JsonRpcRequest = match serde_json::from_str(&line) { + Ok(r) => r, + Err(e) => { + let error_response = JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: None, + result: None, + error: Some(JsonRpcError { + code: -32700, + message: format!("Parse error: {}", e), + data: None, + }), + }; + writeln!(stdout, "{}", serde_json::to_string(&error_response)?)?; + stdout.flush()?; + continue; + } + }; + + let response = self.handle_request(request).await; + + // Don't send response for notifications (no id) + if response.id.is_some() || response.error.is_some() { + writeln!(stdout, "{}", serde_json::to_string(&response)?)?; + stdout.flush()?; + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_server_info() { + let server = VDreamMCPServer::new("/tmp/agents"); + let info = server.get_server_info(); + assert_eq!(info.name, "vDreamTeam Memory Server"); + } + + #[tokio::test] + async fn test_list_tools() { + let server = VDreamMCPServer::new("/tmp/agents"); + let tools = server.list_tools(); + assert!(tools.len() >= 5); + assert!(tools + .iter() + .any(|t| t.name == "vdream_query_constitutional")); + assert!(tools.iter().any(|t| t.name == "vdream_get_stats")); + } + + #[tokio::test] + async fn test_handle_tools_list() { + let mut server = VDreamMCPServer::new("/tmp/agents"); + let request = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + id: Some(json!(2)), + method: "tools/list".to_string(), + params: json!({}), + }; + + let response = server.handle_request(request).await; + assert!(response.result.is_some()); + let result = response.result.unwrap(); + assert!(result.get("tools").is_some()); + } +} diff --git a/src/vdreamteam/mod.rs b/src/vdreamteam/mod.rs new file mode 100644 index 0000000..edbc698 --- /dev/null +++ b/src/vdreamteam/mod.rs @@ -0,0 +1,1983 @@ +//! vDreamTeam AI Agent Memory System +//! +//! This module provides structured memory for AI agent teams with: +//! - **Constitutional Layer**: Shared identity, constraints, and governance +//! - **Role-Specific Memory**: Per-agent decisions, lessons, PxP logs +//! - **Cross-Agent Coordination**: Shared logging for multi-agent workflows +//! +//! # Architecture +//! +//! ```text +//! vDreamTeam Memory System +//! ======================== +//! +//! +-----------------------------------+ +//! | Layer 0: CONSTITUTIONAL | +//! | (Shared, Immutable Core) | +//! | - Core identity & constraints | +//! | - Hard rules (CONS-001 to 016) | +//! | - Mission, values, boundaries | +//! +-----------------------------------+ +//! | +//! +--------------------+--------------------+ +//! | | | +//! +-------+ +-------+ +-------+ +//! | vCEO | | vCTO | | vCMO | +//! | Layer | | Layer | | Layer | +//! +-------+ +-------+ +-------+ +//! ``` +//! +//! # Example +//! +//! ```rust,ignore +//! use reasonkit_mem::vdreamteam::{VDreamMemory, RoleId, PxPEntry, Consultation}; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! // Load vDreamTeam memory from .agents directory +//! let mut memory = VDreamMemory::load(".agents").await?; +//! +//! // Log a PxP consultation +//! let entry = PxPEntry::new("vcto", "Architecture decision") +//! .add_consultation(Consultation { +//! model: "deepseek-v3.2".to_string(), +//! cli_command: "ollama run deepseek-v3.2:cloud".to_string(), +//! prompt_summary: "Validate 2-layer architecture".to_string(), +//! response_summary: "Design validated".to_string(), +//! confidence: 0.90, +//! }); +//! +//! memory.log_pxp(entry).await?; +//! +//! Ok(()) +//! } +//! ``` +//! +//! # Features +//! +//! Requires the `vdreamteam` feature flag in Cargo.toml. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; + +use crate::{MemError, MemResult}; + +// ============================================================================ +// Sub-modules +// ============================================================================ + +/// MCP Server for vDreamTeam memory queries +pub mod mcp_server; + +// Re-export MCP server types +pub use mcp_server::VDreamMCPServer; + +// ============================================================================ +// Core Types +// ============================================================================ + +/// Unique identifier for a role (e.g., "vcto", "vceo", "vcmo") +pub type RoleId = String; + +/// Main vDreamTeam memory system +/// +/// Provides unified access to constitutional and role-specific memory layers. +#[derive(Debug)] +pub struct VDreamMemory { + /// Base path for .agents directory + base_path: PathBuf, + /// Constitutional layer (shared across all roles) + constitutional: Arc>, + /// Role-specific memory layers + roles: HashMap>>, + /// Cross-agent coordination log path + cross_agent_log_path: PathBuf, +} + +impl VDreamMemory { + /// Load vDreamTeam memory from a base directory + /// + /// # Arguments + /// + /// * `base_path` - Path to the .agents directory + /// + /// # Example + /// + /// ```rust,ignore + /// let memory = VDreamMemory::load(".agents").await?; + /// ``` + pub async fn load(base_path: impl AsRef) -> MemResult { + let base_path = base_path.as_ref().to_path_buf(); + let constitutional_path = base_path.join("constitutional"); + let roles_path = base_path.join("roles"); + let cross_agent_log_path = base_path.join("logs").join("cross_agent.ndjson"); + + // Load constitutional layer + let constitutional = ConstitutionalMemory::load(&constitutional_path).await?; + + // Discover and load role-specific memory + let mut roles = HashMap::new(); + if roles_path.exists() { + let mut entries = tokio::fs::read_dir(&roles_path) + .await + .map_err(|e| MemError::storage(format!("Failed to read roles directory: {}", e)))?; + + while let Some(entry) = entries + .next_entry() + .await + .map_err(|e| MemError::storage(format!("Failed to read role entry: {}", e)))? + { + let path = entry.path(); + if path.is_dir() { + if let Some(role_id) = path.file_name().and_then(|n| n.to_str()) { + let role_memory = RoleMemory::load(role_id, &path).await?; + roles.insert(role_id.to_string(), Arc::new(RwLock::new(role_memory))); + } + } + } + } + + Ok(Self { + base_path, + constitutional: Arc::new(RwLock::new(constitutional)), + roles, + cross_agent_log_path, + }) + } + + /// Create a new empty vDreamTeam memory + pub fn new(base_path: impl AsRef) -> Self { + let base_path = base_path.as_ref().to_path_buf(); + Self { + cross_agent_log_path: base_path.join("logs").join("cross_agent.ndjson"), + base_path, + constitutional: Arc::new(RwLock::new(ConstitutionalMemory::default())), + roles: HashMap::new(), + } + } + + /// Get the constitutional memory (read-only) + pub async fn constitutional(&self) -> MemResult { + let guard = self.constitutional.read().await; + Ok(guard.clone()) + } + + /// Get role-specific memory (read-only) + pub async fn role(&self, role_id: &str) -> MemResult> { + match self.roles.get(role_id) { + Some(role) => { + let guard = role.read().await; + Ok(Some(guard.clone())) + } + None => Ok(None), + } + } + + /// Log a PxP consultation entry + /// + /// Appends to the role's consults.yaml file. + pub async fn log_pxp(&mut self, entry: PxPEntry) -> MemResult<()> { + let role_id = entry.role.clone(); + + // Ensure role memory exists + if !self.roles.contains_key(&role_id) { + return Err(MemError::storage(format!( + "Role '{}' not found in memory system", + role_id + ))); + } + + // Update role memory + let role = self.roles.get(&role_id).unwrap(); + { + let mut guard = role.write().await; + guard.pxp_log.entries.push(entry.clone()); + } + + // Persist to disk + self.persist_role_pxp(&role_id).await?; + + // Also append to cross-agent log + self.append_cross_agent_event(CrossAgentEvent::PxPSession { + timestamp: entry.timestamp, + role: role_id, + models: entry + .consultations + .iter() + .map(|c| c.model.clone()) + .collect(), + topic: entry.decision_context.clone(), + result: entry.final_decision.clone().unwrap_or_default(), + }) + .await?; + + Ok(()) + } + + /// Record a decision made by a role + pub async fn record_decision(&mut self, decision: Decision) -> MemResult<()> { + let role_id = decision.role.clone(); + + if !self.roles.contains_key(&role_id) { + return Err(MemError::storage(format!( + "Role '{}' not found in memory system", + role_id + ))); + } + + let role = self.roles.get(&role_id).unwrap(); + { + let mut guard = role.write().await; + guard.decisions.entries.push(decision.clone()); + } + + self.persist_role_decisions(&role_id).await?; + + // Log to cross-agent + self.append_cross_agent_event(CrossAgentEvent::Decision { + timestamp: decision.timestamp, + role: role_id, + decision_id: decision.id.clone(), + title: decision.title.clone(), + confidence: decision.confidence, + }) + .await?; + + Ok(()) + } + + /// Record a lesson learned + pub async fn record_lesson(&mut self, lesson: Lesson) -> MemResult<()> { + let role_id = lesson.role.clone(); + + if !self.roles.contains_key(&role_id) { + return Err(MemError::storage(format!( + "Role '{}' not found in memory system", + role_id + ))); + } + + let role = self.roles.get(&role_id).unwrap(); + { + let mut guard = role.write().await; + guard.lessons.entries.push(lesson.clone()); + } + + self.persist_role_lessons(&role_id).await?; + + Ok(()) + } + + /// Get all roles in the memory system + pub fn role_ids(&self) -> Vec<&String> { + self.roles.keys().collect() + } + + /// Check a constraint by ID + pub async fn check_constraint(&self, constraint_id: &str) -> MemResult> { + let guard = self.constitutional.read().await; + Ok(guard.constraints.get(constraint_id).cloned()) + } + + /// Get PxP statistics for a role + pub async fn pxp_stats(&self, role_id: &str) -> MemResult { + let role = self + .roles + .get(role_id) + .ok_or_else(|| MemError::storage(format!("Role '{}' not found", role_id)))?; + + let guard = role.read().await; + let entries = &guard.pxp_log.entries; + + let total_consultations: usize = entries.iter().map(|e| e.consultations.len()).sum(); + + let avg_confidence = if !entries.is_empty() { + entries + .iter() + .flat_map(|e| e.consultations.iter().map(|c| c.confidence)) + .sum::() + / total_consultations.max(1) as f64 + } else { + 0.0 + }; + + let mut model_usage: HashMap = HashMap::new(); + for entry in entries { + for consultation in &entry.consultations { + *model_usage.entry(consultation.model.clone()).or_default() += 1; + } + } + + Ok(PxPStats { + total_sessions: entries.len(), + total_consultations, + avg_confidence, + model_usage, + }) + } + + // ======================================================================== + // Private Methods + // ======================================================================== + + async fn persist_role_pxp(&self, role_id: &str) -> MemResult<()> { + let role = self + .roles + .get(role_id) + .ok_or_else(|| MemError::storage(format!("Role '{}' not found", role_id)))?; + + let guard = role.read().await; + let path = self + .base_path + .join("roles") + .join(role_id) + .join("memory") + .join("consults.yaml"); + + let yaml = serde_yaml::to_string(&guard.pxp_log) + .map_err(|e| MemError::storage(format!("Failed to serialize PxP log: {}", e)))?; + + tokio::fs::create_dir_all(path.parent().unwrap()) + .await + .map_err(|e| MemError::storage(format!("Failed to create directory: {}", e)))?; + + tokio::fs::write(&path, yaml) + .await + .map_err(|e| MemError::storage(format!("Failed to write PxP log: {}", e)))?; + + Ok(()) + } + + async fn persist_role_decisions(&self, role_id: &str) -> MemResult<()> { + let role = self + .roles + .get(role_id) + .ok_or_else(|| MemError::storage(format!("Role '{}' not found", role_id)))?; + + let guard = role.read().await; + let path = self + .base_path + .join("roles") + .join(role_id) + .join("memory") + .join("decisions.yaml"); + + let yaml = serde_yaml::to_string(&guard.decisions) + .map_err(|e| MemError::storage(format!("Failed to serialize decisions: {}", e)))?; + + tokio::fs::create_dir_all(path.parent().unwrap()) + .await + .map_err(|e| MemError::storage(format!("Failed to create directory: {}", e)))?; + + tokio::fs::write(&path, yaml) + .await + .map_err(|e| MemError::storage(format!("Failed to write decisions: {}", e)))?; + + Ok(()) + } + + async fn persist_role_lessons(&self, role_id: &str) -> MemResult<()> { + let role = self + .roles + .get(role_id) + .ok_or_else(|| MemError::storage(format!("Role '{}' not found", role_id)))?; + + let guard = role.read().await; + let path = self + .base_path + .join("roles") + .join(role_id) + .join("memory") + .join("lessons.yaml"); + + let yaml = serde_yaml::to_string(&guard.lessons) + .map_err(|e| MemError::storage(format!("Failed to serialize lessons: {}", e)))?; + + tokio::fs::create_dir_all(path.parent().unwrap()) + .await + .map_err(|e| MemError::storage(format!("Failed to create directory: {}", e)))?; + + tokio::fs::write(&path, yaml) + .await + .map_err(|e| MemError::storage(format!("Failed to write lessons: {}", e)))?; + + Ok(()) + } + + async fn append_cross_agent_event(&self, event: CrossAgentEvent) -> MemResult<()> { + let json = serde_json::to_string(&event) + .map_err(|e| MemError::storage(format!("Failed to serialize event: {}", e)))?; + + tokio::fs::create_dir_all(self.cross_agent_log_path.parent().unwrap()) + .await + .map_err(|e| MemError::storage(format!("Failed to create logs directory: {}", e)))?; + + use tokio::io::AsyncWriteExt; + let mut file = tokio::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&self.cross_agent_log_path) + .await + .map_err(|e| MemError::storage(format!("Failed to open cross-agent log: {}", e)))?; + + file.write_all(format!("{}\n", json).as_bytes()) + .await + .map_err(|e| MemError::storage(format!("Failed to write event: {}", e)))?; + + Ok(()) + } +} + +// ============================================================================ +// Constitutional Memory (Layer 0) +// ============================================================================ + +/// Constitutional memory - shared immutable core for all agents +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ConstitutionalMemory { + /// Core identity (mission, tagline, philosophy) + pub identity: Identity, + /// Hard constraints (CONS-001 to CONS-016) + pub constraints: HashMap, + /// Quality gates + pub quality_gates: Vec, + /// Boundaries (OSS vs Proprietary) + pub boundaries: Boundaries, + /// PxP consultation requirements + pub consultation: ConsultationConfig, +} + +impl ConstitutionalMemory { + /// Load constitutional memory from a directory + pub async fn load(path: impl AsRef) -> MemResult { + let path = path.as_ref(); + + let mut memory = Self::default(); + + // Load identity.yaml + let identity_path = path.join("identity.yaml"); + if identity_path.exists() { + let content = tokio::fs::read_to_string(&identity_path) + .await + .map_err(|e| MemError::storage(format!("Failed to read identity.yaml: {}", e)))?; + memory.identity = serde_yaml::from_str(&content) + .map_err(|e| MemError::storage(format!("Failed to parse identity.yaml: {}", e)))?; + } + + // Load constraints.yaml + let constraints_path = path.join("constraints.yaml"); + if constraints_path.exists() { + let content = tokio::fs::read_to_string(&constraints_path) + .await + .map_err(|e| { + MemError::storage(format!("Failed to read constraints.yaml: {}", e)) + })?; + let wrapper: ConstraintsWrapper = serde_yaml::from_str(&content).map_err(|e| { + MemError::storage(format!("Failed to parse constraints.yaml: {}", e)) + })?; + // Populate constraint IDs from HashMap keys + memory.constraints = wrapper + .constraints + .into_iter() + .map(|(key, mut constraint)| { + if constraint.id.is_empty() { + constraint.id = key.clone(); + } + (key, constraint) + }) + .collect(); + } + + // Load quality_gates.yaml + let gates_path = path.join("quality_gates.yaml"); + if gates_path.exists() { + let content = tokio::fs::read_to_string(&gates_path).await.map_err(|e| { + MemError::storage(format!("Failed to read quality_gates.yaml: {}", e)) + })?; + let wrapper: QualityGatesWrapper = serde_yaml::from_str(&content).map_err(|e| { + MemError::storage(format!("Failed to parse quality_gates.yaml: {}", e)) + })?; + memory.quality_gates = wrapper.gates; + } + + // Load boundaries.yaml + let boundaries_path = path.join("boundaries.yaml"); + if boundaries_path.exists() { + let content = tokio::fs::read_to_string(&boundaries_path) + .await + .map_err(|e| MemError::storage(format!("Failed to read boundaries.yaml: {}", e)))?; + memory.boundaries = serde_yaml::from_str(&content).map_err(|e| { + MemError::storage(format!("Failed to parse boundaries.yaml: {}", e)) + })?; + } + + // Load consultation.yaml + let consultation_path = path.join("consultation.yaml"); + if consultation_path.exists() { + let content = tokio::fs::read_to_string(&consultation_path) + .await + .map_err(|e| { + MemError::storage(format!("Failed to read consultation.yaml: {}", e)) + })?; + memory.consultation = serde_yaml::from_str(&content).map_err(|e| { + MemError::storage(format!("Failed to parse consultation.yaml: {}", e)) + })?; + } + + Ok(memory) + } +} + +/// Core identity of the vDreamTeam +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Identity { + pub version: String, + pub last_updated: String, + pub mission: String, + pub tagline: String, + pub philosophy: String, + pub principles: Vec, + pub organization: OrganizationInfo, +} + +/// A guiding principle +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Principle { + pub code: String, + pub description: String, + pub enforcement: String, +} + +/// Organization information +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct OrganizationInfo { + pub name: String, + pub website: String, + pub target_arr: String, +} + +/// A hard constraint +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Constraint { + /// Constraint ID (populated from HashMap key if not in YAML) + #[serde(default)] + pub id: String, + pub name: String, + #[serde(default)] + pub description: String, + pub enforcement: String, + #[serde(default)] + pub consequence: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +struct ConstraintsWrapper { + constraints: HashMap, +} + +/// A quality gate +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QualityGate { + #[serde(default)] + pub id: String, + pub name: String, + #[serde(default)] + pub command: String, + #[serde(default)] + pub threshold: String, + #[serde(default)] + pub required: bool, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +struct QualityGatesWrapper { + gates: Vec, +} + +/// OSS vs Proprietary boundaries +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Boundaries { + #[serde(default)] + pub oss_projects: Vec, + #[serde(default)] + pub proprietary_projects: Vec, + #[serde(default)] + pub never_oss: Vec, + /// Additional fields from YAML (classifications, rules, etc.) + #[serde(flatten)] + pub extra: HashMap, +} + +/// PxP consultation configuration +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ConsultationConfig { + #[serde(default)] + pub version: String, + #[serde(default)] + pub philosophy: ConsultationPhilosophy, + #[serde(default)] + pub cli_tools: HashMap, + #[serde(default)] + pub tiers: HashMap, + /// Additional fields from YAML + #[serde(flatten)] + pub extra: HashMap, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ConsultationPhilosophy { + #[serde(default)] + pub axiom: String, + #[serde(default)] + pub requirement: String, + #[serde(default)] + pub minimum_per_session: u32, + #[serde(default)] + pub maximum_per_session: u32, + #[serde(default)] + pub quality_over_speed: bool, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CliTool { + #[serde(default)] + pub command: String, + #[serde(default)] + pub model: Option, + #[serde(default)] + pub tier: u8, + #[serde(default)] + pub specialty: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ConsultationTier { + #[serde(default)] + pub name: String, + #[serde(default)] + pub consultations: String, + #[serde(default)] + pub time: String, + #[serde(default)] + pub use_when: Vec, + #[serde(default)] + pub models: Vec, +} + +// ============================================================================ +// Role-Specific Memory (Layer 2) +// ============================================================================ + +/// Role-specific memory layer +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct RoleMemory { + /// Role identifier + pub role_id: RoleId, + /// Role identity and configuration + pub identity: RoleIdentity, + /// Decision log + pub decisions: DecisionLog, + /// Lessons learned + pub lessons: LessonsLog, + /// PxP consultation log + pub pxp_log: PxPLog, + /// Skills registry + pub skills: SkillsRegistry, +} + +impl RoleMemory { + /// Load role memory from a directory + pub async fn load(role_id: &str, path: impl AsRef) -> MemResult { + let path = path.as_ref(); + let memory_path = path.join("memory"); + + let mut role = Self { + role_id: role_id.to_string(), + ..Default::default() + }; + + // Load identity.yaml + let identity_path = path.join("identity.yaml"); + if identity_path.exists() { + let content = tokio::fs::read_to_string(&identity_path) + .await + .map_err(|e| MemError::storage(format!("Failed to read role identity: {}", e)))?; + role.identity = serde_yaml::from_str(&content) + .map_err(|e| MemError::storage(format!("Failed to parse role identity: {}", e)))?; + } + + // Load decisions.yaml + let decisions_path = memory_path.join("decisions.yaml"); + if decisions_path.exists() { + let content = tokio::fs::read_to_string(&decisions_path) + .await + .map_err(|e| MemError::storage(format!("Failed to read decisions: {}", e)))?; + role.decisions = serde_yaml::from_str(&content) + .map_err(|e| MemError::storage(format!("Failed to parse decisions: {}", e)))?; + } + + // Load lessons.yaml + let lessons_path = memory_path.join("lessons.yaml"); + if lessons_path.exists() { + let content = tokio::fs::read_to_string(&lessons_path) + .await + .map_err(|e| MemError::storage(format!("Failed to read lessons: {}", e)))?; + role.lessons = serde_yaml::from_str(&content) + .map_err(|e| MemError::storage(format!("Failed to parse lessons: {}", e)))?; + } + + // Load consults.yaml (PxP log) + let pxp_path = memory_path.join("consults.yaml"); + if pxp_path.exists() { + let content = tokio::fs::read_to_string(&pxp_path) + .await + .map_err(|e| MemError::storage(format!("Failed to read PxP log: {}", e)))?; + role.pxp_log = serde_yaml::from_str(&content) + .map_err(|e| MemError::storage(format!("Failed to parse PxP log: {}", e)))?; + } + + // Load skills.yaml + let skills_path = memory_path.join("skills.yaml"); + if skills_path.exists() { + let content = tokio::fs::read_to_string(&skills_path) + .await + .map_err(|e| MemError::storage(format!("Failed to read skills: {}", e)))?; + role.skills = serde_yaml::from_str(&content) + .map_err(|e| MemError::storage(format!("Failed to parse skills: {}", e)))?; + } + + Ok(role) + } +} + +/// Role identity and configuration +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct RoleIdentity { + pub version: String, + pub role_id: String, + pub full_name: String, + pub tier: u8, + pub category: String, + pub model: ModelConfig, + pub cli_tools: CliToolsConfig, + pub responsibilities: RoleResponsibilities, + pub pxp_requirements: HashMap, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ModelConfig { + pub primary: String, + pub fallback: Vec, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CliToolsConfig { + pub primary: String, + pub alternatives: Vec, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct RoleResponsibilities { + pub core: Vec, + pub governance: Vec, + pub coordination: Vec, +} + +// ============================================================================ +// Decision Tracking +// ============================================================================ + +/// Log of decisions made by a role +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct DecisionLog { + #[serde(default)] + pub version: String, + #[serde(default)] + pub role_id: String, + #[serde(default)] + pub entries: Vec, +} + +/// A decision record +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Decision { + pub id: String, + pub timestamp: DateTime, + pub role: RoleId, + pub title: String, + pub context: String, + pub decision: String, + pub rationale: String, + pub alternatives_considered: Vec, + pub pxp_consultations: Vec, + pub confidence: f64, + pub status: DecisionStatus, +} + +impl Decision { + pub fn new(role: impl Into, title: impl Into) -> Self { + Self { + id: format!( + "DEC-{}-{}", + Utc::now().format("%Y%m%d%H%M%S"), + uuid::Uuid::new_v4().to_string()[..8].to_uppercase() + ), + timestamp: Utc::now(), + role: role.into(), + title: title.into(), + context: String::new(), + decision: String::new(), + rationale: String::new(), + alternatives_considered: Vec::new(), + pxp_consultations: Vec::new(), + confidence: 0.0, + status: DecisionStatus::Pending, + } + } + + pub fn with_context(mut self, context: impl Into) -> Self { + self.context = context.into(); + self + } + + pub fn with_decision(mut self, decision: impl Into) -> Self { + self.decision = decision.into(); + self + } + + pub fn with_rationale(mut self, rationale: impl Into) -> Self { + self.rationale = rationale.into(); + self + } + + pub fn with_confidence(mut self, confidence: f64) -> Self { + self.confidence = confidence; + self + } + + pub fn finalize(mut self) -> Self { + self.status = DecisionStatus::Finalized; + self + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum DecisionStatus { + Pending, + Finalized, + Superseded, + Reverted, +} + +// ============================================================================ +// Lessons Learned +// ============================================================================ + +/// Log of lessons learned by a role +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct LessonsLog { + #[serde(default)] + pub version: String, + #[serde(default)] + pub role_id: String, + #[serde(default)] + pub entries: Vec, +} + +/// A lesson learned record +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Lesson { + pub id: String, + pub timestamp: DateTime, + pub role: RoleId, + pub title: String, + pub category: LessonCategory, + pub severity: LessonSeverity, + pub context: String, + pub mistake: String, + pub lesson: String, + pub corrective_actions: Vec, + pub prevention: String, +} + +impl Lesson { + pub fn new(role: impl Into, title: impl Into) -> Self { + Self { + id: format!( + "LES-{}-{}", + Utc::now().format("%Y%m%d%H%M%S"), + uuid::Uuid::new_v4().to_string()[..8].to_uppercase() + ), + timestamp: Utc::now(), + role: role.into(), + title: title.into(), + category: LessonCategory::Process, + severity: LessonSeverity::Medium, + context: String::new(), + mistake: String::new(), + lesson: String::new(), + corrective_actions: Vec::new(), + prevention: String::new(), + } + } + + pub fn with_category(mut self, category: LessonCategory) -> Self { + self.category = category; + self + } + + pub fn with_severity(mut self, severity: LessonSeverity) -> Self { + self.severity = severity; + self + } + + pub fn with_context(mut self, context: impl Into) -> Self { + self.context = context.into(); + self + } + + pub fn with_mistake(mut self, mistake: impl Into) -> Self { + self.mistake = mistake.into(); + self + } + + pub fn with_lesson(mut self, lesson: impl Into) -> Self { + self.lesson = lesson.into(); + self + } + + pub fn with_prevention(mut self, prevention: impl Into) -> Self { + self.prevention = prevention.into(); + self + } + + pub fn add_corrective_action(mut self, action: impl Into) -> Self { + self.corrective_actions.push(action.into()); + self + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum LessonCategory { + Governance, + Architecture, + Process, + Tooling, + Communication, + Security, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum LessonSeverity { + Low, + Medium, + High, + Critical, +} + +// ============================================================================ +// PxP Consultation Logging +// ============================================================================ + +/// Log of PxP (Prompt-in-Prompt) consultations +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct PxPLog { + #[serde(default)] + pub version: String, + #[serde(default)] + pub entries: Vec, +} + +/// A PxP consultation session entry +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PxPEntry { + #[serde(default)] + pub id: String, + #[serde(default = "Utc::now")] + pub timestamp: DateTime, + #[serde(default)] + pub session_id: Option, + /// Role ID (populated from parent log if not in YAML) + #[serde(default)] + pub role: RoleId, + #[serde(default)] + pub decision_context: String, + #[serde(default)] + pub consultations: Vec, + #[serde(default)] + pub triangulation_result: Option, + #[serde(default)] + pub final_decision: Option, +} + +impl PxPEntry { + pub fn new(role: impl Into, decision_context: impl Into) -> Self { + Self { + id: format!("PXP-{}", Utc::now().format("%Y%m%d%H%M%S%f")), + timestamp: Utc::now(), + session_id: None, + role: role.into(), + decision_context: decision_context.into(), + consultations: Vec::new(), + triangulation_result: None, + final_decision: None, + } + } + + pub fn with_session(mut self, session_id: impl Into) -> Self { + self.session_id = Some(session_id.into()); + self + } + + pub fn add_consultation(mut self, consultation: Consultation) -> Self { + self.consultations.push(consultation); + self + } + + pub fn with_triangulation(mut self, result: impl Into) -> Self { + self.triangulation_result = Some(result.into()); + self + } + + pub fn with_final_decision(mut self, decision: impl Into) -> Self { + self.final_decision = Some(decision.into()); + self + } +} + +/// A single consultation with an AI model +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Consultation { + #[serde(default)] + pub model: String, + #[serde(default)] + pub cli_command: String, + #[serde(default)] + pub prompt_summary: String, + #[serde(default)] + pub response_summary: String, + #[serde(default)] + pub confidence: f64, +} + +// ============================================================================ +// Skills Registry +// ============================================================================ + +/// Registry of skills available to a role +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct SkillsRegistry { + #[serde(default)] + pub version: String, + #[serde(default)] + pub role_id: String, + #[serde(default)] + pub skills: Vec, + /// Additional fields from YAML (cli_tools, etc.) + #[serde(flatten)] + pub extra: HashMap, +} + +/// A skill available to a role +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Skill { + #[serde(default)] + pub id: String, + #[serde(default)] + pub name: String, + #[serde(default)] + pub description: String, + #[serde(default)] + pub skill_type: SkillType, + #[serde(default)] + pub invocation: String, + #[serde(default)] + pub proficiency: u8, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum SkillType { + #[default] + Cli, + Mcp, + Internal, + External, +} + +// ============================================================================ +// Cross-Agent Coordination +// ============================================================================ + +/// Events in the cross-agent coordination log +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "event", rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CrossAgentEvent { + /// System initialization + SystemInit { + timestamp: DateTime, + message: String, + version: String, + }, + /// PxP consultation session + PxPSession { + timestamp: DateTime, + role: RoleId, + models: Vec, + topic: String, + result: String, + }, + /// Decision made + Decision { + timestamp: DateTime, + role: RoleId, + decision_id: String, + title: String, + confidence: f64, + }, + /// Role handoff + Handoff { + timestamp: DateTime, + from_role: RoleId, + to_role: RoleId, + context: String, + }, + /// Escalation + Escalation { + timestamp: DateTime, + from_role: RoleId, + to_role: RoleId, + reason: String, + }, +} + +// ============================================================================ +// Statistics +// ============================================================================ + +/// PxP consultation statistics for a role +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct PxPStats { + pub total_sessions: usize, + pub total_consultations: usize, + pub avg_confidence: f64, + pub model_usage: HashMap, +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pxp_entry_creation() { + let entry = PxPEntry::new("vcto", "Test decision") + .with_session("test-session-123") + .add_consultation(Consultation { + model: "deepseek-v3.2".to_string(), + cli_command: "ollama run deepseek-v3.2:cloud".to_string(), + prompt_summary: "Test prompt".to_string(), + response_summary: "Test response".to_string(), + confidence: 0.85, + }) + .with_triangulation("3 models agree") + .with_final_decision("Proceed with plan A"); + + assert_eq!(entry.role, "vcto"); + assert_eq!(entry.consultations.len(), 1); + assert!(entry.final_decision.is_some()); + } + + #[test] + fn test_decision_creation() { + let decision = Decision::new("vcto", "Architecture change") + .with_context("Need to optimize storage layer") + .with_decision("Use dual-layer storage") + .with_rationale("Better performance for hot data") + .with_confidence(0.92) + .finalize(); + + assert_eq!(decision.role, "vcto"); + assert_eq!(decision.status, DecisionStatus::Finalized); + assert!(decision.id.starts_with("DEC-")); + } + + #[test] + fn test_lesson_creation() { + let lesson = Lesson::new("vcto", "Quality over Speed") + .with_category(LessonCategory::Process) + .with_severity(LessonSeverity::High) + .with_context("User feedback") + .with_mistake("Rushing to conclusions") + .with_lesson("Take more time, use ThinkTools") + .with_prevention("Apply ThinkTools before concluding") + .add_corrective_action("Updated consultation.yaml"); + + assert_eq!(lesson.role, "vcto"); + assert_eq!(lesson.category, LessonCategory::Process); + assert_eq!(lesson.severity, LessonSeverity::High); + assert_eq!(lesson.corrective_actions.len(), 1); + } + + #[test] + fn test_cross_agent_event_serialization() { + let event = CrossAgentEvent::Decision { + timestamp: Utc::now(), + role: "vcto".to_string(), + decision_id: "DEC-001".to_string(), + title: "Test decision".to_string(), + confidence: 0.9, + }; + + let json = serde_json::to_string(&event).unwrap(); + assert!(json.contains("DECISION")); + assert!(json.contains("vcto")); + } + + #[test] + fn test_vdream_memory_new() { + let memory = VDreamMemory::new(".agents"); + assert!(memory.role_ids().is_empty()); + } +} + +// ============================================================================ +// Integration Tests +// ============================================================================ + +#[cfg(test)] +mod integration_tests { + use super::*; + use std::fs; + use tempfile::TempDir; + + /// Helper: Create a minimal valid .agents directory structure for testing + async fn setup_test_agents_dir(base: &Path) -> MemResult<()> { + // Constitutional layer + let constitutional = base.join("constitutional"); + tokio::fs::create_dir_all(&constitutional) + .await + .map_err(|e| { + MemError::storage(format!("Failed to create constitutional dir: {}", e)) + })?; + + // identity.yaml + let identity = r#" +version: "1.0.0" +last_updated: "2026-01-03" +mission: "Test mission" +tagline: "Test tagline" +philosophy: "Test philosophy" +principles: [] +organization: + name: "Test Org" + website: "https://test.org" + target_arr: "$100K" +"#; + tokio::fs::write(constitutional.join("identity.yaml"), identity) + .await + .map_err(|e| MemError::storage(format!("Failed to write identity.yaml: {}", e)))?; + + // constraints.yaml + let constraints = r#" +constraints: + CONS-001: + id: "CONS-001" + name: "Test Constraint" + description: "A test constraint" + enforcement: "HARD" + consequence: "Reject" +"#; + tokio::fs::write(constitutional.join("constraints.yaml"), constraints) + .await + .map_err(|e| MemError::storage(format!("Failed to write constraints.yaml: {}", e)))?; + + // quality_gates.yaml + let gates = r#" +gates: + - id: "GATE-001" + name: "Build" + command: "cargo build" + threshold: "Exit 0" + required: true +"#; + tokio::fs::write(constitutional.join("quality_gates.yaml"), gates) + .await + .map_err(|e| MemError::storage(format!("Failed to write quality_gates.yaml: {}", e)))?; + + // boundaries.yaml + let boundaries = r#" +oss_projects: + - reasonkit-core +proprietary_projects: + - reasonkit-pro +never_oss: + - rk-research +"#; + tokio::fs::write(constitutional.join("boundaries.yaml"), boundaries) + .await + .map_err(|e| MemError::storage(format!("Failed to write boundaries.yaml: {}", e)))?; + + // consultation.yaml + let consultation = r#" +version: "4.1.0" +philosophy: + axiom: "Multiple AI perspectives improve outcomes" + requirement: "Always consult other models" + minimum_per_session: 2 + maximum_per_session: 15 + quality_over_speed: true +cli_tools: {} +tiers: {} +"#; + tokio::fs::write(constitutional.join("consultation.yaml"), consultation) + .await + .map_err(|e| MemError::storage(format!("Failed to write consultation.yaml: {}", e)))?; + + // Role: vcto + let vcto_path = base.join("roles").join("vcto"); + let vcto_memory = vcto_path.join("memory"); + tokio::fs::create_dir_all(&vcto_memory) + .await + .map_err(|e| MemError::storage(format!("Failed to create vcto/memory dir: {}", e)))?; + + // vcto/identity.yaml + let vcto_identity = r#" +version: "1.0.0" +role_id: "vcto" +full_name: "Virtual Chief Technology Officer" +tier: 1 +category: "executive" +model: + primary: "claude-opus-4-5" + fallback: [] +cli_tools: + primary: "claude -p" + alternatives: [] +responsibilities: + core: + - "Architecture decisions" + governance: + - "Technical standards" + coordination: + - "Team coordination" +pxp_requirements: {} +"#; + tokio::fs::write(vcto_path.join("identity.yaml"), vcto_identity) + .await + .map_err(|e| MemError::storage(format!("Failed to write vcto/identity.yaml: {}", e)))?; + + // vcto/memory/decisions.yaml + let decisions = r#" +version: "1.0.0" +role_id: "vcto" +entries: [] +"#; + tokio::fs::write(vcto_memory.join("decisions.yaml"), decisions) + .await + .map_err(|e| MemError::storage(format!("Failed to write decisions.yaml: {}", e)))?; + + // vcto/memory/lessons.yaml + let lessons = r#" +version: "1.0.0" +role_id: "vcto" +entries: [] +"#; + tokio::fs::write(vcto_memory.join("lessons.yaml"), lessons) + .await + .map_err(|e| MemError::storage(format!("Failed to write lessons.yaml: {}", e)))?; + + // vcto/memory/consults.yaml + let consults = r#" +version: "1.0.0" +entries: [] +"#; + tokio::fs::write(vcto_memory.join("consults.yaml"), consults) + .await + .map_err(|e| MemError::storage(format!("Failed to write consults.yaml: {}", e)))?; + + // vcto/memory/skills.yaml + let skills = r#" +version: "1.0.0" +role_id: "vcto" +skills: [] +"#; + tokio::fs::write(vcto_memory.join("skills.yaml"), skills) + .await + .map_err(|e| MemError::storage(format!("Failed to write skills.yaml: {}", e)))?; + + // Logs directory + tokio::fs::create_dir_all(base.join("logs")) + .await + .map_err(|e| MemError::storage(format!("Failed to create logs dir: {}", e)))?; + + Ok(()) + } + + // ======================================================================== + // Integration Test: Load Valid Directory + // ======================================================================== + + #[tokio::test] + async fn test_load_valid_directory() { + // Setup temp directory with valid structure + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let base_path = temp_dir.path(); + + setup_test_agents_dir(base_path) + .await + .expect("Failed to setup test agents dir"); + + // Load memory + let memory = VDreamMemory::load(base_path) + .await + .expect("Failed to load VDreamMemory"); + + // Verify constitutional layer loaded + let constitutional = memory + .constitutional() + .await + .expect("Failed to get constitutional"); + assert_eq!(constitutional.identity.mission, "Test mission"); + assert!(constitutional.constraints.contains_key("CONS-001")); + assert_eq!(constitutional.quality_gates.len(), 1); + assert_eq!(constitutional.boundaries.oss_projects.len(), 1); + assert_eq!(constitutional.consultation.version, "4.1.0"); + + // Verify role loaded + assert_eq!(memory.role_ids().len(), 1); + assert!(memory.role_ids().contains(&&"vcto".to_string())); + + let role = memory.role("vcto").await.expect("Failed to get role"); + assert!(role.is_some()); + let role = role.unwrap(); + assert_eq!(role.role_id, "vcto"); + assert_eq!(role.identity.full_name, "Virtual Chief Technology Officer"); + } + + // ======================================================================== + // Integration Test: Log PxP Persistence + // ======================================================================== + + #[tokio::test] + async fn test_log_pxp_persistence() { + // Setup temp directory + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let base_path = temp_dir.path(); + + setup_test_agents_dir(base_path) + .await + .expect("Failed to setup test agents dir"); + + // Load memory + let mut memory = VDreamMemory::load(base_path) + .await + .expect("Failed to load VDreamMemory"); + + // Create and log a PxP entry + let entry = PxPEntry::new("vcto", "Test architecture decision") + .with_session("test-session-001") + .add_consultation(Consultation { + model: "deepseek-v3.2".to_string(), + cli_command: "ollama run deepseek-v3.2:cloud".to_string(), + prompt_summary: "Validate test approach".to_string(), + response_summary: "Approach validated".to_string(), + confidence: 0.88, + }) + .add_consultation(Consultation { + model: "claude-opus-4.5".to_string(), + cli_command: "claude -p".to_string(), + prompt_summary: "Secondary validation".to_string(), + response_summary: "Confirmed alignment".to_string(), + confidence: 0.92, + }) + .with_triangulation("2/2 models agree") + .with_final_decision("Proceed with implementation"); + + memory.log_pxp(entry).await.expect("Failed to log PxP"); + + // Verify file was persisted + let consults_path = base_path + .join("roles") + .join("vcto") + .join("memory") + .join("consults.yaml"); + assert!( + consults_path.exists(), + "consults.yaml should exist after logging" + ); + + let content = fs::read_to_string(&consults_path).expect("Failed to read consults.yaml"); + assert!( + content.contains("deepseek-v3.2"), + "Should contain model name" + ); + assert!( + content.contains("Test architecture decision"), + "Should contain decision context" + ); + assert!( + content.contains("2/2 models agree"), + "Should contain triangulation result" + ); + + // Verify cross-agent log + let cross_agent_path = base_path.join("logs").join("cross_agent.ndjson"); + assert!(cross_agent_path.exists(), "cross_agent.ndjson should exist"); + + let cross_content = + fs::read_to_string(&cross_agent_path).expect("Failed to read cross_agent"); + // The event type is serialized via serde with tag="event" and SCREAMING_SNAKE_CASE + // Check for known content that must be in the log + assert!( + cross_content.contains("vcto") && cross_content.contains("event"), + "Cross-agent log should contain role and event data. Got: {}", + cross_content + ); + + // Reload and verify in-memory state + let reloaded = VDreamMemory::load(base_path) + .await + .expect("Failed to reload VDreamMemory"); + + let role = reloaded + .role("vcto") + .await + .expect("Failed to get role") + .unwrap(); + assert_eq!(role.pxp_log.entries.len(), 1); + assert_eq!(role.pxp_log.entries[0].consultations.len(), 2); + assert_eq!( + role.pxp_log.entries[0].final_decision, + Some("Proceed with implementation".to_string()) + ); + } + + // ======================================================================== + // Integration Test: Record Decision Storage + // ======================================================================== + + #[tokio::test] + async fn test_record_decision_storage() { + // Setup temp directory + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let base_path = temp_dir.path(); + + setup_test_agents_dir(base_path) + .await + .expect("Failed to setup test agents dir"); + + // Load memory + let mut memory = VDreamMemory::load(base_path) + .await + .expect("Failed to load VDreamMemory"); + + // Create and record a decision + let decision = Decision::new("vcto", "Dual-Layer Storage Architecture") + .with_context("Need optimized storage for hot and cold data") + .with_decision("Implement 2-layer storage with hot/cold separation") + .with_rationale("Provides optimal performance for frequently accessed data while maintaining cost efficiency for archival") + .with_confidence(0.91) + .finalize(); + + memory + .record_decision(decision) + .await + .expect("Failed to record decision"); + + // Verify file was persisted + let decisions_path = base_path + .join("roles") + .join("vcto") + .join("memory") + .join("decisions.yaml"); + assert!( + decisions_path.exists(), + "decisions.yaml should exist after recording" + ); + + let content = fs::read_to_string(&decisions_path).expect("Failed to read decisions.yaml"); + assert!( + content.contains("Dual-Layer Storage Architecture"), + "Should contain title" + ); + assert!( + content.contains("2-layer storage"), + "Should contain decision" + ); + assert!(content.contains("0.91"), "Should contain confidence"); + assert!(content.contains("finalized"), "Should be finalized"); + + // Verify cross-agent log + let cross_agent_path = base_path.join("logs").join("cross_agent.ndjson"); + let cross_content = + fs::read_to_string(&cross_agent_path).expect("Failed to read cross_agent"); + assert!( + cross_content.contains("DECISION"), + "Should contain Decision event" + ); + + // Reload and verify persistence + let reloaded = VDreamMemory::load(base_path) + .await + .expect("Failed to reload VDreamMemory"); + + let role = reloaded + .role("vcto") + .await + .expect("Failed to get role") + .unwrap(); + assert_eq!(role.decisions.entries.len(), 1); + assert_eq!( + role.decisions.entries[0].title, + "Dual-Layer Storage Architecture" + ); + assert_eq!(role.decisions.entries[0].status, DecisionStatus::Finalized); + } + + // ======================================================================== + // Integration Test: Record Lesson Storage + // ======================================================================== + + #[tokio::test] + async fn test_record_lesson_storage() { + // Setup temp directory + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let base_path = temp_dir.path(); + + setup_test_agents_dir(base_path) + .await + .expect("Failed to setup test agents dir"); + + // Load memory + let mut memory = VDreamMemory::load(base_path) + .await + .expect("Failed to load VDreamMemory"); + + // Create and record a lesson + let lesson = Lesson::new("vcto", "Quality Over Speed") + .with_category(LessonCategory::Process) + .with_severity(LessonSeverity::High) + .with_context("User feedback indicated rushed conclusions") + .with_mistake("Skipping ThinkTools application in favor of quick responses") + .with_lesson("Always apply relevant ThinkTools before finalizing decisions") + .with_prevention("Enforce minimum PxP consultation count per session") + .add_corrective_action("Updated consultation.yaml with stricter minimums") + .add_corrective_action("Added quality gate for ThinkTools usage"); + + memory + .record_lesson(lesson) + .await + .expect("Failed to record lesson"); + + // Verify file was persisted + let lessons_path = base_path + .join("roles") + .join("vcto") + .join("memory") + .join("lessons.yaml"); + assert!( + lessons_path.exists(), + "lessons.yaml should exist after recording" + ); + + let content = fs::read_to_string(&lessons_path).expect("Failed to read lessons.yaml"); + assert!( + content.contains("Quality Over Speed"), + "Should contain title" + ); + assert!( + content.contains("ThinkTools"), + "Should contain lesson content" + ); + assert!(content.contains("PROCESS"), "Should contain category"); + assert!(content.contains("HIGH"), "Should contain severity"); + assert!( + content.contains("consultation.yaml"), + "Should contain corrective action" + ); + + // Reload and verify persistence + let reloaded = VDreamMemory::load(base_path) + .await + .expect("Failed to reload VDreamMemory"); + + let role = reloaded + .role("vcto") + .await + .expect("Failed to get role") + .unwrap(); + assert_eq!(role.lessons.entries.len(), 1); + assert_eq!(role.lessons.entries[0].title, "Quality Over Speed"); + assert_eq!(role.lessons.entries[0].category, LessonCategory::Process); + assert_eq!(role.lessons.entries[0].severity, LessonSeverity::High); + assert_eq!(role.lessons.entries[0].corrective_actions.len(), 2); + } + + // ======================================================================== + // Integration Test: Load Nonexistent Directory (Graceful Fallback) + // ======================================================================== + + #[tokio::test] + async fn test_load_nonexistent_directory() { + // VDreamMemory::load gracefully handles missing directories by + // creating default empty structures. This is by design to allow + // bootstrapping new projects. + // + // To truly test failure, we need a path that causes a hard error + // (e.g., permission denied or invalid path). For now, we verify + // that loading a nonexistent path returns an empty/default memory. + let result = VDreamMemory::load("/nonexistent/path/that/should/not/exist").await; + + // The implementation may either: + // 1. Fail with an error (preferred for strict validation) + // 2. Return empty memory (graceful fallback) + match result { + Ok(memory) => { + // If it succeeds, it should be empty (no roles loaded) + assert!( + memory.role_ids().is_empty(), + "Should have no roles for nonexistent path" + ); + } + Err(err) => { + // If it fails, error should be meaningful + let err_string = err.to_string(); + assert!( + err_string.contains("Failed to read") + || err_string.contains("No such file") + || err_string.contains("not found") + || err_string.contains("directory"), + "Error should indicate path/directory issue: {}", + err_string + ); + } + } + } + + // ======================================================================== + // Integration Test: Check Constraint + // ======================================================================== + + #[tokio::test] + async fn test_check_constraint() { + // Setup temp directory + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let base_path = temp_dir.path(); + + setup_test_agents_dir(base_path) + .await + .expect("Failed to setup test agents dir"); + + // Load memory + let memory = VDreamMemory::load(base_path) + .await + .expect("Failed to load VDreamMemory"); + + // Check existing constraint + let constraint = memory + .check_constraint("CONS-001") + .await + .expect("Failed to check constraint"); + assert!(constraint.is_some()); + let constraint = constraint.unwrap(); + assert_eq!(constraint.name, "Test Constraint"); + assert_eq!(constraint.enforcement, "HARD"); + + // Check non-existing constraint + let none = memory + .check_constraint("CONS-999") + .await + .expect("Failed to check non-existing constraint"); + assert!(none.is_none()); + } + + // ======================================================================== + // Integration Test: PxP Statistics + // ======================================================================== + + #[tokio::test] + async fn test_pxp_stats() { + // Setup temp directory + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let base_path = temp_dir.path(); + + setup_test_agents_dir(base_path) + .await + .expect("Failed to setup test agents dir"); + + // Load memory + let mut memory = VDreamMemory::load(base_path) + .await + .expect("Failed to load VDreamMemory"); + + // Log multiple PxP entries + for i in 1..=3 { + let entry = PxPEntry::new("vcto", format!("Decision {}", i)) + .add_consultation(Consultation { + model: "deepseek-v3.2".to_string(), + cli_command: "ollama run deepseek-v3.2:cloud".to_string(), + prompt_summary: "Prompt".to_string(), + response_summary: "Response".to_string(), + confidence: 0.8 + (i as f64 * 0.05), + }) + .add_consultation(Consultation { + model: "claude-opus-4.5".to_string(), + cli_command: "claude -p".to_string(), + prompt_summary: "Prompt 2".to_string(), + response_summary: "Response 2".to_string(), + confidence: 0.85 + (i as f64 * 0.03), + }); + + memory.log_pxp(entry).await.expect("Failed to log PxP"); + } + + // Get stats + let stats = memory.pxp_stats("vcto").await.expect("Failed to get stats"); + + assert_eq!(stats.total_sessions, 3, "Should have 3 sessions"); + assert_eq!( + stats.total_consultations, 6, + "Should have 6 total consultations" + ); + assert!( + stats.avg_confidence > 0.8, + "Average confidence should be > 0.8" + ); + assert_eq!(stats.model_usage.get("deepseek-v3.2"), Some(&3)); + assert_eq!(stats.model_usage.get("claude-opus-4.5"), Some(&3)); + } + + // ======================================================================== + // Integration Test: Role Not Found Error + // ======================================================================== + + #[tokio::test] + async fn test_role_not_found_error() { + // Setup temp directory + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let base_path = temp_dir.path(); + + setup_test_agents_dir(base_path) + .await + .expect("Failed to setup test agents dir"); + + // Load memory + let mut memory = VDreamMemory::load(base_path) + .await + .expect("Failed to load VDreamMemory"); + + // Try to log PxP for non-existent role + let entry = PxPEntry::new("nonexistent_role", "Test"); + let result = memory.log_pxp(entry).await; + + assert!(result.is_err(), "Should fail for non-existent role"); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("not found"), + "Error should indicate role not found: {}", + err + ); + } + + // ======================================================================== + // Integration Test: Multiple Roles + // ======================================================================== + + #[tokio::test] + async fn test_multiple_roles() { + // Setup temp directory with multiple roles + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let base_path = temp_dir.path(); + + // Setup base structure + setup_test_agents_dir(base_path) + .await + .expect("Failed to setup test agents dir"); + + // Add a second role (vceo) + let vceo_path = base_path.join("roles").join("vceo"); + let vceo_memory = vceo_path.join("memory"); + tokio::fs::create_dir_all(&vceo_memory) + .await + .expect("Failed to create vceo dir"); + + let vceo_identity = r#" +version: "1.0.0" +role_id: "vceo" +full_name: "Virtual Chief Executive Officer" +tier: 1 +category: "executive" +model: + primary: "claude-opus-4-5" + fallback: [] +cli_tools: + primary: "claude -p" + alternatives: [] +responsibilities: + core: + - "Strategic decisions" + governance: + - "Overall governance" + coordination: + - "Executive coordination" +pxp_requirements: {} +"#; + tokio::fs::write(vceo_path.join("identity.yaml"), vceo_identity) + .await + .expect("Failed to write vceo identity"); + + for file in &[ + "decisions.yaml", + "lessons.yaml", + "consults.yaml", + "skills.yaml", + ] { + let content = match *file { + "consults.yaml" => "version: \"1.0.0\"\nentries: []\n", + "skills.yaml" => "version: \"1.0.0\"\nrole_id: \"vceo\"\nskills: []\n", + _ => "version: \"1.0.0\"\nrole_id: \"vceo\"\nentries: []\n", + }; + tokio::fs::write(vceo_memory.join(file), content) + .await + .expect("Failed to write vceo memory file"); + } + + // Load memory + let memory = VDreamMemory::load(base_path) + .await + .expect("Failed to load VDreamMemory"); + + // Verify both roles loaded + let role_ids = memory.role_ids(); + assert_eq!(role_ids.len(), 2, "Should have 2 roles"); + assert!(role_ids.iter().any(|r| *r == "vcto")); + assert!(role_ids.iter().any(|r| *r == "vceo")); + + // Verify each role has correct identity + let vcto = memory + .role("vcto") + .await + .expect("Failed to get vcto") + .unwrap(); + assert_eq!(vcto.identity.full_name, "Virtual Chief Technology Officer"); + + let vceo = memory + .role("vceo") + .await + .expect("Failed to get vceo") + .unwrap(); + assert_eq!(vceo.identity.full_name, "Virtual Chief Executive Officer"); + } +} diff --git a/tests/vdreamteam_integration_tests.rs b/tests/vdreamteam_integration_tests.rs new file mode 100644 index 0000000..b7bad5e --- /dev/null +++ b/tests/vdreamteam_integration_tests.rs @@ -0,0 +1,695 @@ +//! Integration tests for vDreamTeam AI Agent Memory System +//! +//! These tests validate the complete VDreamMemory functionality including: +//! - Loading from valid .agents directory structure +//! - PxP consultation logging and persistence +//! - Decision recording and storage +//! - Lesson learning capture +//! - Cross-agent event coordination +//! - Error handling for nonexistent directories +//! +//! # Test Requirements +//! +//! Requires the `vdreamteam` feature flag: +//! ```bash +//! cargo test --features vdreamteam vdreamteam_integration_tests +//! ``` + +#![cfg(feature = "vdreamteam")] + +use std::path::PathBuf; +use tempfile::TempDir; +use tokio::fs; + +use reasonkit_mem::vdreamteam::{ + Consultation, Decision, Lesson, LessonCategory, LessonSeverity, PxPEntry, VDreamMemory, +}; + +// ============================================================================ +// TEST HELPERS +// ============================================================================ + +/// Create a minimal valid .agents directory structure for testing +async fn create_test_agents_directory(temp_dir: &TempDir) -> PathBuf { + let base_path = temp_dir.path().join(".agents"); + + // Create constitutional layer + let constitutional_path = base_path.join("constitutional"); + fs::create_dir_all(&constitutional_path).await.unwrap(); + + // Create identity.yaml + let identity_yaml = r#" +version: "1.0.0" +last_updated: "2026-01-03" +mission: "Make AI reasoning structured, auditable, and reliable." +tagline: "Turn Prompts into Protocols" +philosophy: "Designed, Not Dreamed" +principles: + - code: "PRIN-001" + description: "Structure beats intelligence" + enforcement: "HARD" +organization: + name: "ReasonKit" + website: "https://reasonkit.sh" + target_arr: "$719K" +"#; + fs::write(constitutional_path.join("identity.yaml"), identity_yaml) + .await + .unwrap(); + + // Create constraints.yaml + let constraints_yaml = r#" +constraints: + CONS-001: + id: "CONS-001" + name: "No Node.js MCP Servers" + description: "All MCP servers must be Rust-first" + enforcement: "HARD" + consequence: "Reject at code review" + CONS-002: + id: "CONS-002" + name: "Rust for Performance" + description: "Performance-critical paths must be Rust" + enforcement: "HARD" + consequence: "No exceptions" +"#; + fs::write( + constitutional_path.join("constraints.yaml"), + constraints_yaml, + ) + .await + .unwrap(); + + // Create quality_gates.yaml + let quality_gates_yaml = r#" +gates: + - id: "GATE-001" + name: "Build" + command: "cargo build --release" + threshold: "Exit 0" + required: true + - id: "GATE-002" + name: "Clippy" + command: "cargo clippy -- -D warnings" + threshold: "0 errors" + required: true +"#; + fs::write( + constitutional_path.join("quality_gates.yaml"), + quality_gates_yaml, + ) + .await + .unwrap(); + + // Create boundaries.yaml + let boundaries_yaml = r#" +oss_projects: + - "reasonkit-core" + - "reasonkit-mem" + - "reasonkit-web" +proprietary_projects: + - "reasonkit-pro" +never_oss: + - "rk-research" + - "rk-startup" +"#; + fs::write(constitutional_path.join("boundaries.yaml"), boundaries_yaml) + .await + .unwrap(); + + // Create consultation.yaml + let consultation_yaml = r#" +version: "4.1.0" +philosophy: + axiom: "Multiple AI perspectives improve outcomes" + requirement: "ALWAYS consult other models" + minimum_per_session: 2 + maximum_per_session: 15 + quality_over_speed: true +cli_tools: + claude: + command: "claude -p" + model: "claude-opus-4-5-20251101" + tier: 1 + specialty: "Architecture" + deepseek: + command: "ollama run deepseek-v3.2:cloud" + model: "deepseek-v3.2" + tier: 2 + specialty: "Reasoning" +tiers: + quick: + name: "Quick Consultation" + consultations: "2-3" + time: "< 5 min" + use_when: + - "Simple decisions" + - "Confidence > 80%" + models: + - "deepseek-v3.2" + - "devstral-2" + thorough: + name: "Thorough Consultation" + consultations: "5-10" + time: "15-30 min" + use_when: + - "Architecture decisions" + - "Security-critical" + models: + - "claude-opus-4.5" + - "deepseek-v3.2" + - "mistral-large-3" +"#; + fs::write( + constitutional_path.join("consultation.yaml"), + consultation_yaml, + ) + .await + .unwrap(); + + // Create roles directory with vcto role + let vcto_path = base_path.join("roles").join("vcto"); + let vcto_memory_path = vcto_path.join("memory"); + fs::create_dir_all(&vcto_memory_path).await.unwrap(); + + // Create vcto identity.yaml + let vcto_identity_yaml = r#" +version: "1.0.0" +role_id: "vcto" +full_name: "Virtual Chief Technology Officer" +tier: 1 +category: "C-SUITE" +model: + primary: "claude-opus-4-5-20251101" + fallback: + - "gpt-5.2-pro" + - "gemini-3-pro" +cli_tools: + primary: "claude -p" + alternatives: + - "gemini -p" + - "codex" +responsibilities: + core: + - "Technical architecture decisions" + - "Rust-first enforcement" + - "Quality gate oversight" + governance: + - "MCP server architecture" + - "Performance standards" + coordination: + - "Lead engineer coordination" + - "Cross-crate integration" +pxp_requirements: + architecture: 5 + breaking_change: 5 + security: 4 + routine: 2 +"#; + fs::write(vcto_path.join("identity.yaml"), vcto_identity_yaml) + .await + .unwrap(); + + // Create empty memory files + let decisions_yaml = r#" +version: "1.0.0" +role_id: "vcto" +entries: [] +"#; + fs::write(vcto_memory_path.join("decisions.yaml"), decisions_yaml) + .await + .unwrap(); + + let lessons_yaml = r#" +version: "1.0.0" +role_id: "vcto" +entries: [] +"#; + fs::write(vcto_memory_path.join("lessons.yaml"), lessons_yaml) + .await + .unwrap(); + + let consults_yaml = r#" +version: "1.0.0" +role_id: "vcto" +entries: [] +"#; + fs::write(vcto_memory_path.join("consults.yaml"), consults_yaml) + .await + .unwrap(); + + let skills_yaml = r#" +version: "1.0.0" +role_id: "vcto" +skills: [] +"#; + fs::write(vcto_memory_path.join("skills.yaml"), skills_yaml) + .await + .unwrap(); + + // Create logs directory + fs::create_dir_all(base_path.join("logs")).await.unwrap(); + + base_path +} + +// ============================================================================ +// TEST: Load Valid Directory +// ============================================================================ + +/// Test loading VDreamMemory from a valid .agents directory structure +#[tokio::test] +async fn test_load_valid_directory() { + let temp_dir = TempDir::new().unwrap(); + let agents_path = create_test_agents_directory(&temp_dir).await; + + // Load the memory system + let memory = VDreamMemory::load(&agents_path).await; + assert!(memory.is_ok(), "Should load from valid directory"); + + let memory = memory.unwrap(); + + // Verify constitutional layer loaded + let constitutional = memory.constitutional().await.unwrap(); + assert_eq!( + constitutional.identity.mission, + "Make AI reasoning structured, auditable, and reliable." + ); + assert_eq!( + constitutional.identity.tagline, + "Turn Prompts into Protocols" + ); + assert_eq!(constitutional.identity.philosophy, "Designed, Not Dreamed"); + + // Verify constraints loaded + assert!(constitutional.constraints.contains_key("CONS-001")); + assert!(constitutional.constraints.contains_key("CONS-002")); + let cons001 = constitutional.constraints.get("CONS-001").unwrap(); + assert_eq!(cons001.name, "No Node.js MCP Servers"); + + // Verify quality gates loaded + assert_eq!(constitutional.quality_gates.len(), 2); + assert!(constitutional + .quality_gates + .iter() + .any(|g| g.id == "GATE-001")); + + // Verify boundaries loaded + assert!(constitutional + .boundaries + .oss_projects + .contains(&"reasonkit-core".to_string())); + assert!(constitutional + .boundaries + .never_oss + .contains(&"rk-research".to_string())); + + // Verify consultation config loaded + assert_eq!(constitutional.consultation.version, "4.1.0"); + assert_eq!( + constitutional.consultation.philosophy.minimum_per_session, + 2 + ); + assert_eq!( + constitutional.consultation.philosophy.maximum_per_session, + 15 + ); + + // Verify role loaded + let role_ids = memory.role_ids(); + assert!(role_ids.iter().any(|r| r.as_str() == "vcto")); + + let vcto = memory.role("vcto").await.unwrap(); + assert!(vcto.is_some()); + let vcto = vcto.unwrap(); + assert_eq!(vcto.identity.full_name, "Virtual Chief Technology Officer"); + assert_eq!(vcto.identity.tier, 1); + assert_eq!(vcto.identity.category, "C-SUITE"); +} + +// ============================================================================ +// TEST: PxP Logging and Persistence +// ============================================================================ + +/// Test logging PxP consultation entries with persistence +#[tokio::test] +async fn test_log_pxp_persistence() { + let temp_dir = TempDir::new().unwrap(); + let agents_path = create_test_agents_directory(&temp_dir).await; + + // Load memory and log a PxP entry + let mut memory = VDreamMemory::load(&agents_path).await.unwrap(); + + let entry = PxPEntry::new("vcto", "Architecture decision for dual-layer memory") + .add_consultation(Consultation { + model: "deepseek-v3.2".to_string(), + cli_command: "ollama run deepseek-v3.2:cloud".to_string(), + prompt_summary: "Validate dual-layer memory architecture".to_string(), + response_summary: "Architecture validated with hot/cold tier design".to_string(), + confidence: 0.92, + }) + .add_consultation(Consultation { + model: "mistral-large-3".to_string(), + cli_command: "ollama run mistral-large-3:675b-cloud".to_string(), + prompt_summary: "Review WAL implementation strategy".to_string(), + response_summary: "WAL approach recommended for durability".to_string(), + confidence: 0.88, + }) + .with_final_decision("Use dual-layer memory with WAL for crash recovery".to_string()); + + let result = memory.log_pxp(entry).await; + assert!(result.is_ok(), "Should log PxP entry successfully"); + + // Verify the entry was persisted + let consults_path = agents_path + .join("roles") + .join("vcto") + .join("memory") + .join("consults.yaml"); + let content = fs::read_to_string(&consults_path).await.unwrap(); + assert!(content.contains("Architecture decision for dual-layer memory")); + assert!(content.contains("deepseek-v3.2")); + assert!(content.contains("mistral-large-3")); + + // Verify cross-agent log was updated + let cross_agent_log = agents_path.join("logs").join("cross_agent.ndjson"); + let log_content = fs::read_to_string(&cross_agent_log) + .await + .unwrap_or_else(|e| { + panic!( + "Failed to read cross_agent.ndjson: {} at path {:?}", + e, cross_agent_log + ); + }); + // Note: serde rename_all="SCREAMING_SNAKE_CASE" converts PxPSession to PX_P_SESSION + assert!( + log_content.contains("PX_P_SESSION"), + "Log should contain PX_P_SESSION event. Got: {}", + log_content + ); + assert!(log_content.contains("vcto")); + + // Verify PxP stats + let stats = memory.pxp_stats("vcto").await.unwrap(); + assert_eq!(stats.total_sessions, 1); + assert_eq!(stats.total_consultations, 2); + assert!(stats.avg_confidence > 0.85); + assert!(stats.model_usage.contains_key("deepseek-v3.2")); + assert!(stats.model_usage.contains_key("mistral-large-3")); +} + +// ============================================================================ +// TEST: Decision Recording and Storage +// ============================================================================ + +/// Test recording decisions with full metadata +#[tokio::test] +async fn test_record_decision_storage() { + let temp_dir = TempDir::new().unwrap(); + let agents_path = create_test_agents_directory(&temp_dir).await; + + let mut memory = VDreamMemory::load(&agents_path).await.unwrap(); + + let decision = Decision::new("vcto", "Adopt 2-layer memory architecture") + .with_context("Need to implement long-term memory for AI agents with different access patterns") + .with_decision("Use hot/cold tier architecture with WAL for durability") + .with_rationale("Hot tier for frequently accessed data, cold tier for historical, WAL for crash recovery") + .with_confidence(0.92) + .finalize(); + + let result = memory.record_decision(decision).await; + assert!(result.is_ok(), "Should record decision successfully"); + + // Verify persistence + let decisions_path = agents_path + .join("roles") + .join("vcto") + .join("memory") + .join("decisions.yaml"); + let content = fs::read_to_string(&decisions_path).await.unwrap(); + assert!(content.contains("Adopt 2-layer memory architecture")); + assert!(content.contains("hot/cold tier architecture")); + assert!(content.contains("finalized")); + + // Verify cross-agent log contains decision event + let cross_agent_log = agents_path.join("logs").join("cross_agent.ndjson"); + let log_content = fs::read_to_string(&cross_agent_log).await.unwrap(); + assert!( + log_content.contains("DECISION"), + "Log should contain DECISION event" + ); + assert!(log_content.contains("Adopt 2-layer memory architecture")); +} + +// ============================================================================ +// TEST: Lesson Recording and Storage +// ============================================================================ + +/// Test recording lessons learned with full metadata +#[tokio::test] +async fn test_record_lesson_storage() { + let temp_dir = TempDir::new().unwrap(); + let agents_path = create_test_agents_directory(&temp_dir).await; + + let mut memory = VDreamMemory::load(&agents_path).await.unwrap(); + + let lesson = Lesson::new("vcto", "Always validate PxP model responses") + .with_category(LessonCategory::Process) + .with_severity(LessonSeverity::High) + .with_context("During architecture review, a model returned outdated syntax") + .with_mistake("Trusted model response without verification") + .with_lesson("Always triangulate with 3+ sources for critical decisions") + .with_prevention("Add mandatory verification step to PxP protocol") + .add_corrective_action("Update consultation.yaml with verification requirements") + .add_corrective_action("Add ProofGuard validation to decision workflow"); + + let result = memory.record_lesson(lesson).await; + assert!(result.is_ok(), "Should record lesson successfully"); + + // Verify persistence + let lessons_path = agents_path + .join("roles") + .join("vcto") + .join("memory") + .join("lessons.yaml"); + let content = fs::read_to_string(&lessons_path).await.unwrap(); + assert!(content.contains("Always validate PxP model responses")); + assert!(content.contains("triangulate with 3+ sources")); + assert!(content.contains("ProofGuard validation")); +} + +// ============================================================================ +// TEST: Nonexistent Directory Error Handling +// ============================================================================ + +/// Test loading from nonexistent directory returns appropriate error +#[tokio::test] +async fn test_load_nonexistent_directory() { + let nonexistent_path = PathBuf::from("/nonexistent/path/to/.agents"); + let result = VDreamMemory::load(&nonexistent_path).await; + + // Should succeed but with empty/default values since files don't exist + // The load function creates defaults for missing files + assert!( + result.is_ok(), + "Should handle nonexistent directory gracefully" + ); + + let memory = result.unwrap(); + let role_ids = memory.role_ids(); + assert!( + role_ids.is_empty(), + "Should have no roles for nonexistent directory" + ); +} + +// ============================================================================ +// TEST: Constraint Checking +// ============================================================================ + +/// Test constraint checking functionality +#[tokio::test] +async fn test_check_constraint() { + let temp_dir = TempDir::new().unwrap(); + let agents_path = create_test_agents_directory(&temp_dir).await; + + let memory = VDreamMemory::load(&agents_path).await.unwrap(); + + // Check existing constraint + let cons001 = memory.check_constraint("CONS-001").await.unwrap(); + assert!(cons001.is_some()); + let constraint = cons001.unwrap(); + assert_eq!(constraint.id, "CONS-001"); + assert_eq!(constraint.name, "No Node.js MCP Servers"); + assert_eq!(constraint.enforcement, "HARD"); + + // Check non-existing constraint + let nonexistent = memory.check_constraint("CONS-999").await.unwrap(); + assert!(nonexistent.is_none()); +} + +// ============================================================================ +// TEST: Multiple PxP Sessions +// ============================================================================ + +/// Test logging multiple PxP sessions and verifying stats +#[tokio::test] +async fn test_multiple_pxp_sessions() { + let temp_dir = TempDir::new().unwrap(); + let agents_path = create_test_agents_directory(&temp_dir).await; + + let mut memory = VDreamMemory::load(&agents_path).await.unwrap(); + + // Log 5 PxP sessions with varying consultations + for i in 1..=5 { + let mut entry = PxPEntry::new("vcto", format!("Decision #{}", i)); + + // Add 1-3 consultations per session + for j in 1..=(i % 3 + 1) { + entry = entry.add_consultation(Consultation { + model: format!("model-{}", j), + cli_command: format!("ollama run model-{}", j), + prompt_summary: format!("Prompt {} for decision {}", j, i), + response_summary: format!("Response validated"), + confidence: 0.80 + (j as f64 * 0.05), + }); + } + + memory.log_pxp(entry).await.unwrap(); + } + + // Verify stats + let stats = memory.pxp_stats("vcto").await.unwrap(); + assert_eq!(stats.total_sessions, 5); + assert!(stats.total_consultations >= 5); // At least 1 per session + assert!(stats.avg_confidence > 0.80); +} + +// ============================================================================ +// TEST: Role Not Found Error +// ============================================================================ + +/// Test error handling when logging to nonexistent role +#[tokio::test] +async fn test_log_to_nonexistent_role() { + let temp_dir = TempDir::new().unwrap(); + let agents_path = create_test_agents_directory(&temp_dir).await; + + let mut memory = VDreamMemory::load(&agents_path).await.unwrap(); + + let entry = PxPEntry::new("nonexistent_role", "Test decision"); + let result = memory.log_pxp(entry).await; + + assert!(result.is_err(), "Should fail for nonexistent role"); + let err = result.unwrap_err(); + assert!(err.to_string().contains("not found")); +} + +// ============================================================================ +// TEST: New Empty Memory +// ============================================================================ + +/// Test creating new empty VDreamMemory +#[tokio::test] +async fn test_new_empty_memory() { + let temp_dir = TempDir::new().unwrap(); + let base_path = temp_dir.path().join(".agents"); + + let memory = VDreamMemory::new(&base_path); + + // Should have default constitutional memory + let constitutional = memory.constitutional().await.unwrap(); + assert!(constitutional.identity.mission.is_empty()); + + // Should have no roles + let role_ids = memory.role_ids(); + assert!(role_ids.is_empty()); +} + +// ============================================================================ +// TEST: Cross-Agent Event Logging +// ============================================================================ + +/// Test that all operations log to cross-agent log +#[tokio::test] +async fn test_cross_agent_event_logging() { + let temp_dir = TempDir::new().unwrap(); + let agents_path = create_test_agents_directory(&temp_dir).await; + + let mut memory = VDreamMemory::load(&agents_path).await.unwrap(); + + // Log PxP + let pxp_entry = PxPEntry::new("vcto", "PxP Test").add_consultation(Consultation { + model: "test-model".to_string(), + cli_command: "test".to_string(), + prompt_summary: "test".to_string(), + response_summary: "test".to_string(), + confidence: 0.9, + }); + memory.log_pxp(pxp_entry).await.unwrap(); + + // Record decision + let decision = Decision::new("vcto", "Decision Test") + .with_decision("Test decision") + .with_confidence(0.85) + .finalize(); + memory.record_decision(decision).await.unwrap(); + + // Check cross-agent log contains both events + let cross_agent_log = agents_path.join("logs").join("cross_agent.ndjson"); + let log_content = fs::read_to_string(&cross_agent_log).await.unwrap(); + + let lines: Vec<&str> = log_content.lines().collect(); + assert!(lines.len() >= 2, "Should have at least 2 events logged"); + + // Verify event types (serde rename_all="SCREAMING_SNAKE_CASE" - PxPSession becomes PX_P_SESSION) + assert!( + log_content.contains("PX_P_SESSION"), + "Log should contain PX_P_SESSION event" + ); + assert!( + log_content.contains("DECISION"), + "Log should contain DECISION event" + ); +} + +// ============================================================================ +// TEST: Reload Persisted Data +// ============================================================================ + +/// Test that persisted data can be reloaded +#[tokio::test] +async fn test_reload_persisted_data() { + let temp_dir = TempDir::new().unwrap(); + let agents_path = create_test_agents_directory(&temp_dir).await; + + // First session: log data + { + let mut memory = VDreamMemory::load(&agents_path).await.unwrap(); + + let entry = PxPEntry::new("vcto", "Persistent Decision").add_consultation(Consultation { + model: "persistent-model".to_string(), + cli_command: "test".to_string(), + prompt_summary: "Should persist".to_string(), + response_summary: "Confirmed".to_string(), + confidence: 0.95, + }); + memory.log_pxp(entry).await.unwrap(); + } + + // Second session: reload and verify + { + let memory = VDreamMemory::load(&agents_path).await.unwrap(); + let vcto = memory.role("vcto").await.unwrap().unwrap(); + + assert_eq!(vcto.pxp_log.entries.len(), 1); + assert_eq!( + vcto.pxp_log.entries[0].decision_context, + "Persistent Decision" + ); + assert_eq!( + vcto.pxp_log.entries[0].consultations[0].model, + "persistent-model" + ); + } +}