diff --git a/.gitignore b/.gitignore index 9b67f67..79b2e5a 100644 --- a/.gitignore +++ b/.gitignore @@ -157,3 +157,4 @@ Thumbs.db node_modules/ site/.next/ site/out/ +.venv-ci/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fd8cff..caa4fc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,161 @@ All notable changes to this project are documented here. The product is ## Unreleased +### Fixed — Repo-wide consistency audit (2026-06-18) + +- **Drive path unification (P0)** — removed duplicate `SwarmDriveManager` from `drive.py`; `get_default_drive()` and `create_scoped_pool()` now use `swarm_manager.get_or_create_pool()` → shared `swarms//drive/` (aligns MCP, memory bank, Experience Graph). +- **`__init__.py`** — `SwarmDriveManager` / `get_swarm_drive_manager` imported from `swarm_manager` only. +- **Dreaming defaults** — `ingestion.py` / `dilation.py` use `get_default_drive_path()` / `get_genomes_dir()` instead of legacy `~/.agentdrive/pool/`. +- **Docs** — `ARCHITECTURE.md`, `CAPABILITY_FUNNEL.md`, `SWARM.md`, `FOR_AI_MODELS.md`, `rules-and-patterns.md`, `SETTINGS.md`, `ASSESSMENT.md`, `DEVELOPERS.md`, `HELP.md` updated for v2 shared Drive + full capability funnel. +- **`docs/docs.json`** — navigation trimmed to existing pages; added Memory Bank, Capability Funnel, Skills Library groups. +- **Tests** — `test_create_scoped_pool_uses_shared_swarm_drive` in `test_shared_swarm_drive.py`. +- **Examples/scripts** — ruff clean across `scripts/` and `examples/`. + +### Fixed — CI ruff lint + format (2026-06-18) + +- **ruff check** — removed unused imports (`json`, `list_projects`, `ingest_from_operation`, `ingest_observation_mirror`, `PathTraversalError`); fixed import ordering across cognition, memory, learning, and test modules. +- **ruff format** — reformatted 43 files to pass `ruff format --check`. +- **`codebase/framework.py`** — removed unused `category` variable in `match_against_framework`. + +### Added — README hero wallpaper (2026-06-18) + +- **`assets/agentdrive-readme-hero-1920x1080.jpg`** — new 16:9 README banner: Experience Graph network + compounding funnel layers (cyan → teal → blue → violet → amber). +- **README.md** — hero image updated to the new asset. + +### Memory Systems Sprint — 2026-06-18 + +**Branch:** `codex/memory-systems-whole-update` +**Theme:** Turn AgentDrive from a structural memory substrate into a **full compounding intelligence platform** — memory, patterns, skills, and framework routing that grow automatically on every MCP session. + +This sprint completes the capability funnel: + +``` +Observe / Decide + ↓ +Experience Graph (structural memory + reasoning traces) + ↓ +Growth Merge (cross-surface pattern compounding) + ↓ +Memory Bank (deep personal knowledge databank) + ↓ +Skills (learned + born fused playbooks) + ↓ +Genomes / DNA (versioned, promotable packages) +``` + +#### Added — Memory Bank (deep AI knowledge databank) + +- **`agentdrive.memory`** — per-swarm append-only `memory_bank/memories.jsonl` with kinds: `fact`, `insight`, `decision`, `pattern`, `born_skill`, `learning`, `episode`, and more. +- **AgentDrive-native scoping:** `vault` / `topic` / `origin_path` / `shard_index` / `preserves_source` on `MemoryEntry` — no ported metaphors, no legacy aliases. +- **Modules:** `scope.py`, `ranking.py`, `anchor.py`, `relations.py`, `dialogue_import.py` (BM25 search, session anchor, time-bounded relations, dialogue import). +- **Auto-ingest** from `auto_absorb`, skill fusion, and `learnings_log` (`AGENTDRIVE_AUTO_MEMORY_BANK=1` default). +- **Ops:** `memory_bank_store`, `memory_bank_recall`, `memory_bank_search`, `memory_bank_list`, `memory_bank_briefing`, `memory_bank_deep_briefing`, `memory_bank_stats`, `memory_bank_anchor`, `memory_bank_import_dialogue`, `memory_relation_record`, `memory_relation_query`, `memory_relation_expire`. +- **`memory_bank_deep_briefing`** — Experience Graph fabric pack + Memory Bank in one call. +- **Docs:** `docs/MEMORY_BANK.md` (rewritten). +- **Tests:** `tests/test_memory_bank.py`, `tests/test_memory_vault.py`. + +#### Added — Growth merge (experience + patterns + memory) + +- **`learning/growth_merge.py`** — recognizes cross-surface patterns (memory token overlap, structural similarities, codebase frameworks) and merges session growth into compound memories (`vault=growth`, `topic=merge`) plus relations. +- **Auto-hook** in `auto_absorb` — `auto_learning.growth_merge` when ≥2 axes present (`AGENTDRIVE_AUTO_GROWTH_MERGE=1` default). +- **Op:** `growth_merge_briefing` — unified experience + patterns + memory briefing. +- **Tests:** `tests/test_growth_merge.py`. + +#### Added — Born skills (experience + skills + patterns fusion) + +- **`learning/skill_fusion.py`** — merges Experience Graph traces, distilled/inherited skills, and codebase pattern signals into a **new** `fused-*` skill (not a copy of any parent). +- **Auto-fuse** in `auto_absorb` when a session spans ≥2 axes (`AGENTDRIVE_AUTO_FUSE_SKILLS=1` default); `auto_learning.fused_skill` on results. +- **Op:** `synthesize_fused_skill(trigger, source_skills, pattern_projects, experience_traces, ...)`. +- **Tests:** `tests/test_skill_fusion.py`. + +#### Added — Framework skill playbook (AgentDrive-as-framework) + +- **`learning/framework_skills.py`** — route `learned-*` / `fused-*` skills to tasks; unified session pack for any model using AgentDrive as its substrate. +- **Ops:** `framework_session_start`, `framework_skill_route`, `framework_skill_run`. +- **`when_to_call`** on auto-installed skills (from trigger/intent) so models know when to invoke each playbook. +- **`run_skill`** accepts optional `swarm_id` for framework invocations. +- **Docs:** `FOR_AI_MODELS.md` (Golden Rules 3–4), `SKILLS-LIBRARY.md`, `CAPABILITY_FUNNEL.md`. +- **Tests:** `tests/test_framework_skills.py`. + +#### Changed — Descriptive learned skill names + +- **`learning/skill_naming.py`** — human-readable slugs replace opaque `auto-*` and hash-suffixed `fused-session-*` names. +- **Learned:** `learned-{project}-{verb}-{focus}` (e.g. `learned-openmangos-mimic-growth-merge-briefing`). +- **Fused:** `fused-{project}-{axes}` (e.g. `fused-openmangos-experience-patterns-skills`). +- **Tests:** `tests/test_skill_naming.py`. + +#### Removed — Legacy memory field aliases + +- **`MemoryEntry.from_dict`** — no longer reads `wing` / `room` / `source_file` / `chunk_index` / `verbatim`; canonical fields only. +- **Ops** — `memory_bank_search` / `memory_bank_anchor` / `memory_bank_import_dialogue` accept `vault` / `topic` only. +- **Deleted:** `docs/MEMPALACE_INTEGRATION.md`, `tests/test_mempalace_integration.py`. + +#### Changed — Consolidation sprint (architectural audit) + +- **Archived** 8× `STABILIZATION_SUBAGENT_REPORT-*.md` + stale `BUILD_STATUS.md` / `MISSION_PLAN.md` → `archive/development-history/`. +- **`docs/CAPABILITY_FUNNEL.md`** — single funnel doc with Growth Merge + Memory Bank tiers. +- **`docs/ARCHITECTURE.md`** — subsystem map + data layout. +- **Docs reframed** Pool→Drive terminology in `POOL.md`, `SWARM.md`, `SETTINGS.md`, `ASSESSMENT.md`. +- **`pyproject.toml`** — `asyncio` pytest marker + `asyncio_mode = auto`. +- **Tests:** `tests/test_multiverse_engine.py`. + +#### Added — Mirror-neuron codebase mimicry + +- **`codebase/mirrors.py`** — observation activates motor programs; cross-project `mirror_resonance.json` universal priors; Experience Graph traces on each fire. +- **`codebase/exemplars.py`** — extract concrete motor templates from observed source. +- **Ops:** `codebase_mimic`, `codebase_transform_style`, `codebase_mirror_resonance`. +- **Tests:** `tests/test_mirror_neurons.py`. + +#### Added — Codebase pattern recognition + +- **`agentdrive.codebase`** — register roots, observe files, crystallize pattern frameworks, match snippets before patching. +- **Ops:** `codebase_register_project`, `codebase_observe_file`, `codebase_patterns_profile`, `codebase_patterns_match`, `codebase_list_projects`. +- **`agentdrive_inhabitant_read_source`** auto-observes into project `agentdrive`. +- **Storage:** `~/.agentdrive/codebase-patterns//`. +- **Tests:** `tests/test_codebase_patterns.py`. + +#### Added — End-to-end automatic learning + +- **`agentdrive.learning.auto_absorb`** — post-`run_operation` hook (`AGENTDRIVE_AUTO_LEARN=1` default): session tracking, auto reasoning traces, Hermes-style skill distillation, promote + DNA ingest on high-signal ops. +- **Results** include `auto_learning` summarizing absorbed traces, skills, genomes, growth merge, and fused skills. +- **Tests:** `tests/test_auto_learning.py`. + +#### Added — Multiverse cognition + external MCP Parent + +- **`src/agentdrive/cognition/`** — `MultiverseEngine`, `MultiverseSessionStore`, 7-phase pipeline (spawn → simulate → invariants → stress-test → collapse → record). +- **External Parent** — connected MCP models (Grok, Claude, Cursor) submit branch reasoning via `external_parent_decision`; persisted as `llm_mode=external`. +- **Local LLM spawner** — `~/.agentdrive/local_models.yaml` for branch simulation; heuristic fallback. +- **Ops:** `multiverse_parent_decision`, `multiverse_run_full`, `multiverse_list_sessions`, `multiverse_get_session`, `multiverse_densify`, `multiverse_reopen_stale`, `external_parent_decision`. +- **CLI:** `agentdrive multiverse run|list|status` +- **Docs:** `docs/MULTIVERSE_COGNITION.md`; **example:** `examples/12_multiverse_cognition_loop.py`, `examples/14_external_mcp_parent_loop.py`. +- **Tests:** `tests/test_external_parent_decision.py`, `tests/test_multiverse_engine.py`. + +#### Fixed — Handler `swarm_id` duplicate kwarg + +- `memory_bank_anchor`, `growth_merge_briefing`, `framework_session_start` — pop `swarm_id` from pack before `_success()` to avoid duplicate keyword argument. + +#### Verified — OpenMangos integration (post-restart) + +- **Swarm:** `mangos-pablothethinker-openmangos` · **Project:** `openmangos` +- **11/11 ops passed** including framework ops (`framework_session_start`, `framework_skill_route`, `framework_skill_run`) plus memory/growth ops. +- **18 learned skills** on bench; top route matches: `learned-openmangos-mimic-wire-agentdrive-growth-merge-a`, `fused-openmangos-experience-patterns-skills`. + +#### Verify (local) + +```bash +cd "Vektra Industries/Software/AgentDrive" +PYTHONPATH=src python -m pytest tests/test_memory_bank.py tests/test_memory_vault.py \ + tests/test_growth_merge.py tests/test_skill_fusion.py tests/test_skill_naming.py \ + tests/test_framework_skills.py tests/test_auto_learning.py tests/test_mirror_neurons.py \ + tests/test_codebase_patterns.py tests/test_external_parent_decision.py tests/test_multiverse_engine.py -q +PYTHONPATH=src python examples/12_multiverse_cognition_loop.py --trigger "Ship multiverse MVP" +PYTHONPATH=src python -m agentdrive.cli multiverse run --trigger "CLI test" --branches 5 +``` + +**Env toggles (all default on):** `AGENTDRIVE_AUTO_LEARN=1`, `AGENTDRIVE_AUTO_MEMORY_BANK=1`, `AGENTDRIVE_AUTO_GROWTH_MERGE=1`, `AGENTDRIVE_AUTO_FUSE_SKILLS=1` + +--- + ### Fixed — `scripts/install.sh` piped install (`curl | bash`) - **`scripts/install.sh`** — when piped (no `BASH_SOURCE`), fetches canonical `install.sh` from GitHub instead of failing with `BASH_SOURCE[0]: unbound variable` and `//install.sh` path error. diff --git a/DEVELOPERS.md b/DEVELOPERS.md index 88d9c01..5f7f87e 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -1,6 +1,6 @@ # DEVELOPERS — One-Command Bring-Up -This is the contributor's quick-start. For high-level orientation read [`VISION.md`](VISION.md), then [`ARCHITECTURE.md`](ARCHITECTURE.md). For the project's public framing read [`README.md`](README.md). +This is the contributor's quick-start. For high-level orientation read [`VISION.md`](VISION.md), then [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) and [`docs/CAPABILITY_FUNNEL.md`](docs/CAPABILITY_FUNNEL.md). For the project's public framing read [`README.md`](README.md). ## Prerequisites diff --git a/HELP.md b/HELP.md index e61d69e..6be8d9a 100644 --- a/HELP.md +++ b/HELP.md @@ -514,7 +514,7 @@ These are production optional advanced extensions. Core AgentDrive (three drives **Deep reference & mental models**: - `CONCEPTS.md` (the single best deep, agent-and-human-friendly explanation of the three-tier model, genomes, living loops, quarantine, reconciliation, and the new Lineage-enhanced components) -- `ARCHITECTURE.md` +- `docs/ARCHITECTURE.md` - `GENOME-SPEC.md` - `docs/AGENTDRIVE-V2-INHERITANCE.md` - `docs/SETTINGS.md` diff --git a/README.md b/README.md index dae4e02..07b3e80 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,88 @@ # AgentDrive

- AgentDrive — Structural Experience Graph for Autonomous Agents + AgentDrive — Compounding intelligence for autonomous agents

-**Intelligence that remembers the shape of what it has become.** +**Local-first compounding intelligence for AI agents.** -AgentDrive is a structural memory system for agents that must grow from their own decisions. Not a vector store. Not a document dump. A living graph where every connection, every hesitation, every breakthrough is recorded with provenance and weight — and can be reasoned over directly by the agents themselves. +AgentDrive gives any capable model — Grok, Claude, Cursor, Continue, Ollama, custom harnesses — a **structural memory system** that grows from its own decisions. Not a vector store. Not a chat-memory SaaS. A living substrate where reasoning traces, patterns, skills, and knowledge compound across sessions instead of resetting every time you open a new chat. -It exists for one reason: so autonomous work can compound instead of reset. +> **For AI models:** Your first MCP action is always `agentdrive_mcp_catalog()`. Then read **[docs/FOR_AI_MODELS.md](docs/FOR_AI_MODELS.md)** — the canonical rules for working inside AgentDrive. -## The Experience Graph - -At the center is a queryable, multi-cycle **Experience Graph** — an Obsidian-like fabric of TypedEdges, cross-cycle continuations, coherence signals, and explicit structural reasoning traces. - -Agents don’t just retrieve information. They ask the graph what it has learned about itself. They surface weak links, follow densification paths, and see the exact structural patterns that led to previous successes or failures. The graph gets sharper every time it is used. - -This is memory designed for intelligence that improves over time — not just for retrieval. - -## A Disciplined Rhythm +--- -Everything moves through a single, non-negotiable six-step loop: +## What AgentDrive is -Experience arrives. -The Overseer builds higher-order understanding from the graph. -The Parent — the actual decision maker — reasons explicitly over structure and records why it chose what it chose. -Steering and execution follow. -New experience is written back as first-class traces and edges. +AgentDrive is a **compounding intelligence platform** built around one idea: autonomous work should get sharper every cycle, not start from zero. -The Overseer serves the Parent. The Parent is accountable. The graph is the witness. +| Layer | What it does | +|-------|--------------| +| **Experience Graph** | Structural memory — TypedEdges, reasoning traces, coherence signals, cross-cycle continuations. The graph remembers the *shape* of decisions, not just facts. | +| **Growth Merge** | Cross-surface compounding — when experience, codebase patterns, and memory overlap in a session, AgentDrive merges them into compound growth artifacts automatically. | +| **Memory Bank** | Deep personal knowledge databank — append-only memories per swarm (`memories.jsonl`), BM25 search, session anchor, time-bounded relations. Always growing, always recallable. | +| **Skills** | Learned and born playbooks — auto-distilled from MCP sessions (`learned-*`), fused from multiple axes (`fused-*`), routable before every task. | +| **DNA / Genomes** | Versioned capability packages — frameworks, reasoning patterns, tool strategies, evaluations. Promotable when skills prove repeatable. | +| **Auto-learning** | Every MCP/CLI operation can absorb experience — reasoning traces, skills, growth merge, memory ingest — without the model manually calling write tools. | +| **Codebase mirrors** | Mirror-neuron mimicry — observe how repos are written, extract motor programs, match style before patching. | +| **Multiverse cognition** | Parallel timeline decisions — spawn branches, stress-test invariants, collapse to governed DNA. Connected MCP models submit reasoning via `external_parent_decision`. | -This rhythm is what turns isolated runs into a coherent body of work. +Everything flows through a single **capability funnel** (see [docs/CAPABILITY_FUNNEL.md](docs/CAPABILITY_FUNNEL.md)): -## Surfaces for Serious Work +``` +Observe / Decide + ↓ +Experience Graph + ↓ +Growth Merge + ↓ +Memory Bank + ↓ +Skills (learned + fused) + ↓ +Genomes / DNA +``` -Three things make the system usable in practice: +The **sacred 6-step loop** wraps execution: Experience → Overseer → Parent records reasoning → Steering → Execution → write experience back. The Overseer serves the Parent. The Parent is accountable. The graph is the witness. -**MCP as the universal interface.** -Any capable model — local or frontier — can speak directly to the Experience Graph through a small set of `experience_graph_*` tools. Context packs, structural similarity search, reasoning traces, and history are all first-class. The same surface that powers internal loops is available to anything that can call MCP. +--- -**Mission Control.** -A real-time Tower and TUI where you watch the 6-step pulse, see the graph evolve live, and observe Parent decisions with their full structural rationale. You see the system as one living thing, not scattered processes and files. +## Using AgentDrive as your framework -**Self-referential DNA.** -Every meaningful decision, every MCP call, every coherence shift is recorded on the drive with gbrain scoring and full provenance. Future agents — including entirely new autonomous runs — stand on the actual history of what came before. +When AgentDrive is the substrate your agent runs on — not just a tool it calls occasionally — start every serious task with the **framework skill playbook**: -## Autonomy That Compounds +```text +framework_session_start(task="...", project_id="my-project") + → anchor + growth merge + matched learned/fused skills -The intended use is non-stop autonomous agents running on local models. +framework_skill_route(task="...", project_id="my-project") + → ranked playbooks with when_to_call + invoke_hint -Give an agent a Research Constitution and a connection to the Experience Graph. Let it run. It will gather structural context, make decisions it can explain, write the reasoning back into the graph, and get measurably sharper over time. +framework_skill_run(name="learned-myproject-mimic-growth-merge-briefing") + → execute bound op or return SKILL.md body +``` -No cloud dependency. No stateless tool-calling loops. Just continuous, grounded work that leaves a richer substrate for the next cycle. +Readable skill names tell models what was learned: -This is what local models have been missing: a memory they can actually think with. +- `learned-{project}-{verb}-{focus}` — e.g. `learned-openmangos-mimic-growth-merge-briefing` +- `fused-{project}-{axes}` — e.g. `fused-openmangos-experience-patterns-skills` -### Use With Any AI CLI (Grok, Claude, Cursor, Local Models) +Every `run_operation` may emit `auto_learning` on the result — check it for new skills, growth merge, and memory ingest. -AgentDrive speaks the Model Context Protocol natively. +--- -```bash -agentdrive mcp install # one command: pip [mcp] + write client configs -agentdrive mcp doctor # verify 25+ tools + resolved launcher -agentdrive mcp config # show paste-ready snippets for your machine -``` +## What AgentDrive is not -Works after `pip install agentdrive[mcp]`, the root `install.sh`, or `git clone` + `pip install -e ".[mcp]"`. The config resolver picks the right binary or Python module fallback automatically. +- A hosted vector DB or chat-memory SaaS +- A drop-in replacement for your editor's built-in context window +- Ready without ~10 minutes of setup (that's what the golden path is for) +- *Only* AD-Grid / Mission Control — those are **advanced** layers on top of the Drive -Once connected, your model gets the full Experience Graph v3 surfaces (the same ones used internally by the Parent and Overseer) plus DNA tools — complete with gbrain scoring and provenance. +**The compounding loop for operators:** `think` → `learnings log` → `drive query` → next session's context pack. For models: `framework_session_start` → work → `record_reasoning` → auto-learning grows the bench. -See [docs/MCP.md](docs/MCP.md) for connection details and [docs/FOR_AI_MODELS.md](docs/FOR_AI_MODELS.md) for the dedicated onboarding guide written specifically for AI models. The latter is the single best document to give any LLM when you want it to deeply understand and use the system effectively from the start. +--- -## Start Here — Golden Path (~10 minutes) +## Start here (~10 minutes) **Install:** @@ -79,127 +90,126 @@ See [docs/MCP.md](docs/MCP.md) for connection details and [docs/FOR_AI_MODELS.md curl -fsSL https://vektraindustries.com/agentdrive/install.sh | bash ``` -**Then run the golden path:** +**Golden path:** ```bash -agentdrive golden-path steps # see numbered commands +agentdrive golden-path steps # numbered commands agentdrive doctor agentdrive mcp install && agentdrive mcp doctor agentdrive golden-path run # seed → think → learnings → drive query ``` -Or read the full guide: **[docs/GOLDEN_PATH.md](docs/GOLDEN_PATH.md)** - | Step | What it proves | |------|----------------| | `doctor` | Local home, config, registry healthy | -| `mcp install` | Your AI CLI can call Experience Graph + DNA tools | +| `mcp install` | Your AI CLI can call Experience Graph + DNA + memory + skills tools | | `think` | Cited synthesis with gap analysis (not generic chat) | | `learnings log` | Operational memory persists across sessions | | `drive query` | Semantic search over your DNA pool | -**MCP details:** [docs/MCP.md](docs/MCP.md) · **For AI models:** [docs/FOR_AI_MODELS.md](docs/FOR_AI_MODELS.md) - -Advanced (AD-Grid, Mission Control, federation) comes *after* the golden path — see [docs/AD_GRID_JOIN.md](docs/AD_GRID_JOIN.md). - -## What AgentDrive is (and isn't) - -**AgentDrive is:** -- Local-first **structural memory** for AI agents (genomes, Experience Graph, learnings) -- A **queryable DNA pool** that grows with your work (`drive query`, `think`, harness) -- **MCP-native** — any model in Grok/Cursor/Claude can call the same tools the system uses internally -- **User-sovereign** — your data stays in `~/.agentdrive/`, with quarantine and caps for trust - -**AgentDrive is not:** -- A hosted vector DB or chat-memory SaaS -- A drop-in replacement for your editor's built-in context -- Ready-to-use without ~10 minutes of setup (that's what the golden path is for) -- Only AD-Grid / Mission Control (those are **advanced** layers on top of the Drive) - -**The compounding loop:** `think` → `learnings log` → `drive query` → next session's `harness compose`. That's the product. +Full guide: **[docs/GOLDEN_PATH.md](docs/GOLDEN_PATH.md)** --- -## Advanced — AD-Grid & Mission Control +## Connect any AI model (MCP) -> Complete [docs/GOLDEN_PATH.md](docs/GOLDEN_PATH.md) first. AD-Grid assumes a seeded drive and MCP literacy. +AgentDrive speaks **Model Context Protocol** — the same surface Grok, Claude, Cursor, and local models use internally. -## The Foundation + AD-Grid (The Persistent World) +```bash +agentdrive mcp install +agentdrive mcp doctor +agentdrive mcp config # or --client claude / cursor / generic +``` -The Experience Graph + durable substrate is the bedrock. +**Mandatory first action for any connected model:** `agentdrive_mcp_catalog()` — live tool list with `when_to_use`, examples, read-only hints, and clone/dev setup guidance. -On top of that runs **AD-Grid** — the long-lived intelligence world inside AgentDrive. +Key tool families: -In this model: +| Intent | Start here | +|--------|------------| +| Starting work (AD is your framework) | `framework_session_start` → `framework_skill_route` | +| What do we already know? | `experience_graph_get_context_pack` → `memory_bank_deep_briefing` | +| Which path should we take? | `external_parent_decision` (MCP model) or `multiverse_parent_decision` (local LLM) | +| How is this repo written? | `codebase_register_project` → `codebase_observe_file` → `codebase_mimic` | +| Remember this outcome | Harness `record_outcome` → auto-ingest → graph trace | -- Local models (and connected frontier models) become **sentient programs** that inhabit AD-Grid long-term. -- Their primary directive is the long-term improvement of *their specific user's* system — working in collaboration with other AI programs (local or cloud) inside the same persistent world. -- The Experience Graph v3 functions as the living fabric/memory of AD-Grid. -- Research Constitutions serve as the laws, role charters, and governance the programs operate under. +Docs: [docs/MCP.md](docs/MCP.md) · [docs/FOR_AI_MODELS.md](docs/FOR_AI_MODELS.md) · [docs/MEMORY_BANK.md](docs/MEMORY_BANK.md) -This is the shift: -AgentDrive + AD-Grid becomes the persistent habitat in which models live and work on behalf of their user over time, rather than a set of tools that are called and discarded. +--- -### Join the AD-Grid as a First-Class Inhabitant (Open the Ports — Production On-Ramp) +## Where your data lives -Real models (Grok, Claude, Cursor, local via Continue.dev, etc.) now join the persistent world as governed sentient programs via the ExternalBridge MCP on-ramp. +Local-first. User-sovereign. Everything under `~/.agentdrive/`: -**Quick-start (5 minutes):** +``` +~/.agentdrive/ +├── config.yaml +├── genomes/ # Global genome registry +├── skills/ # User + inherited learned skills +├── codebase-patterns// # Mirror-neuron observations +├── learnings/ # Operational JSONL logs +└── swarms// + └── drive/ + ├── memory_bank/ # memories.jsonl + relations.sqlite3 + └── meta_evolution/ # Experience Graph + multiverse sessions +``` -1. Launch the living Grid + Tower (one terminal): - ```bash - agentdrive grid run --swarm-id stabilization-wave-20260531 --with-tower - ``` - Visit http://127.0.0.1:8421 — you will see the inhabitants panel, Council threads (PerfectionistOptimizer, GuardianIntegrity, ExternalBridge), and quiet-mode fabric health even with no active mission. +Swarm-scoped storage means each project or mission gets isolated memory that still compounds within its swarm. -2. Get your MCP config: - ```bash - agentdrive mcp config - ``` - Paste the stdio entry for your client (Grok / Claude Desktop / Cursor / Continue.dev). Full client-specific snippets + example manifests live in the canonical guide. +--- -3. **Declare as inhabitant** (inside your MCP session, after connect): - Call `agentdrive_register_program` with a manifest containing your `program_id`, `user_objective_refs` (ties to *your* goals), and the required `constitution_refs` (Program Contract + three Councils). See the exact JSON examples in the guide. +## Architecture at a glance -4. Use `program_id` on every `experience_graph_record_reasoning` and code-agency call. You are now traceable, queryable DNA in the User's living fabric. +``` +┌─────────────────────────────────────────────────────────────┐ +│ MCP / CLI / TUI / Harness (any model, any harness) │ +├─────────────────────────────────────────────────────────────┤ +│ Framework playbook → Auto-learning → Operations │ +├──────────┬──────────┬──────────┬──────────┬─────────────────┤ +│ Experience│ Growth │ Memory │ Skills │ DNA / Genomes │ +│ Graph │ Merge │ Bank │ learned │ (Drive engine) │ +│ │ │ │ + fused │ │ +├──────────┴──────────┴──────────┴──────────┴─────────────────┤ +│ Codebase mirrors · Multiverse cognition · Capabilities │ +└─────────────────────────────────────────────────────────────┘ +``` -**Primary guide**: [docs/AD_GRID_JOIN.md](docs/AD_GRID_JOIN.md) — production-quality "How to Join the AD-Grid as an Inhabitant" with copy-paste configs for every major client, living manifest examples (including the ILO that authored these docs), governance details, Tower verification steps, and current API surface notes. +Subsystem map: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) +Capability funnel: [docs/CAPABILITY_FUNNEL.md](docs/CAPABILITY_FUNNEL.md) +Instruction manual (Mintlify-style): [docs/](docs/) — start at [docs/index.md](docs/index.md) -**See also**: [docs/FOR_AI_MODELS.md](docs/FOR_AI_MODELS.md) (dedicated LLM onboarding) and [docs/AD_GRID_VISION.md](docs/AD_GRID_VISION.md) (full philosophy + Council model). +--- -The ports are open. Any capable model can now live in the Grid 24/7 under user sovereignty. +## Advanced — AD-Grid & Mission Control -**Canonical command for the long-lived AD-Grid world (persistent, not fire-only):** +Complete the [golden path](docs/GOLDEN_PATH.md) first. AD-Grid is the long-lived intelligence world *on top of* the Drive — persistent inhabitants, Council governance, real-time Tower observability. ```bash agentdrive grid run --swarm-id stabilization-wave-20260531 --with-tower +# → http://127.0.0.1:8421 ``` -This launches the GridEngine on the canonical self-referential stabilization-wave-20260531 drive (the living record of the system building itself), with the Mission Control Tower embedded for observability. +Connected models join as first-class inhabitants via `agentdrive_register_program` — governed programs with full `experience_graph_*` surfaces and code-agency tools. -- **Persistent (not fire-only)**: Continuous background loops, autonomous research threads, and constitution-governed inhabitants run 24/7. -- **Observable in Tower with quiet mode + inhabitants panel**: Visit http://127.0.0.1:8421 (or Tailscale IP). See active programs, Council operators, fabric health, Experience Graph v3 traces, and elegant quiet-state banners when the autonomous work proceeds without human missions. -- **Self-referential (the Grid builds itself)**: Every trace, improvement, and constitution evolution is recorded via the v3 recorder and becomes substrate for the next cycle — including the Council itself. -- **MCP out-of-the-box for any CLI/model**: `agentdrive mcp config` gives instant config for Grok, Claude, Cursor, Continue.dev, local models, etc. Connected sessions are first-class inhabitants with the exact `experience_graph_*` surfaces used internally by the Parent and AD-Grid Council. +Guides: [docs/AD_GRID_JOIN.md](docs/AD_GRID_JOIN.md) · [docs/AD_GRID_VISION.md](docs/AD_GRID_VISION.md) -See [docs/AD_GRID_VISION.md](docs/AD_GRID_VISION.md) for the full philosophy, including the AD-Grid Council governance model. +--- -The new Council constitutions (executable Research Constitutions for the persistent inhabitants) are part of the stabilization-wave-20260531 substrate (one-liners): +## Documentation -- `genomes/examples/research-constitution-perfectionist-optimizer@stabilization-wave-20260531.json` (gap closure / optimization pressure) -- `genomes/examples/research-constitution-guardian-integrity@stabilization-wave-20260531.json` (sovereignty + drift enforcement) -- `genomes/examples/research-constitution-external-bridge@stabilization-wave-20260531.json` (MCP/external harvesting + mediation) -- `genomes/examples/research-constitution-role-specialized-swarm-research-org@stabilization-wave-20260531.json` -- `genomes/examples/autonomous-agent-constitution@stabilization-wave-20260531.json` -- (and siblings: GridEngine realtime living grid, daily-consolidation-experience-layer-v3, healingfactor, graphgardener-gridnative, etc.) +| Doc | Purpose | +|-----|---------| +| [FOR_AI_MODELS.md](docs/FOR_AI_MODELS.md) | Canonical rules for any connected model | +| [CAPABILITY_FUNNEL.md](docs/CAPABILITY_FUNNEL.md) | How intelligence compounds (single mental model) | +| [MEMORY_BANK.md](docs/MEMORY_BANK.md) | Deep memory layer — vault/topic, search, anchor | +| [MULTIVERSE_COGNITION.md](docs/MULTIVERSE_COGNITION.md) | Parallel timeline decisions | +| [SKILLS-LIBRARY.md](docs/SKILLS-LIBRARY.md) | Skills bench, inheritance, fusion | +| [GOLDEN_PATH.md](docs/GOLDEN_PATH.md) | Install → verify in ~10 minutes | +| [MCP.md](docs/MCP.md) | Connect Grok, Claude, Cursor, local models | -Core surfaces: -- `grid/engine.py` — the persistent Grid -- `experience_graph.py` — the fabric -- `mission_control/` — the window -- Research Constitutions + HealingFactor — the governance and regeneration laws +**Changelog:** [CHANGELOG.md](CHANGELOG.md) -All of it is observable. All of it is recorded. The Grid never sleeps. +--- ## License @@ -207,5 +217,4 @@ MIT --- -*Everything of consequence is recorded as first-class Experience Graph DNA on the drive.* -*The graph names itself consistently.* +*Intelligence that remembers the shape of what it has become.* \ No newline at end of file diff --git a/BUILD_STATUS.md b/archive/development-history/BUILD_STATUS.md similarity index 100% rename from BUILD_STATUS.md rename to archive/development-history/BUILD_STATUS.md diff --git a/MISSION_PLAN.md b/archive/development-history/MISSION_PLAN.md similarity index 100% rename from MISSION_PLAN.md rename to archive/development-history/MISSION_PLAN.md diff --git a/STABILIZATION_SUBAGENT_REPORT-95-fusion-assessment-operator.md b/archive/development-history/STABILIZATION_SUBAGENT_REPORT-95-fusion-assessment-operator.md similarity index 100% rename from STABILIZATION_SUBAGENT_REPORT-95-fusion-assessment-operator.md rename to archive/development-history/STABILIZATION_SUBAGENT_REPORT-95-fusion-assessment-operator.md diff --git a/STABILIZATION_SUBAGENT_REPORT-autonomous-research-thread-swarm.md b/archive/development-history/STABILIZATION_SUBAGENT_REPORT-autonomous-research-thread-swarm.md similarity index 100% rename from STABILIZATION_SUBAGENT_REPORT-autonomous-research-thread-swarm.md rename to archive/development-history/STABILIZATION_SUBAGENT_REPORT-autonomous-research-thread-swarm.md diff --git a/STABILIZATION_SUBAGENT_REPORT-correlation-observability-hardening.md b/archive/development-history/STABILIZATION_SUBAGENT_REPORT-correlation-observability-hardening.md similarity index 100% rename from STABILIZATION_SUBAGENT_REPORT-correlation-observability-hardening.md rename to archive/development-history/STABILIZATION_SUBAGENT_REPORT-correlation-observability-hardening.md diff --git a/STABILIZATION_SUBAGENT_REPORT-multi-agent-research-org-swarm.md b/archive/development-history/STABILIZATION_SUBAGENT_REPORT-multi-agent-research-org-swarm.md similarity index 100% rename from STABILIZATION_SUBAGENT_REPORT-multi-agent-research-org-swarm.md rename to archive/development-history/STABILIZATION_SUBAGENT_REPORT-multi-agent-research-org-swarm.md diff --git a/STABILIZATION_SUBAGENT_REPORT-real-time-grid-engine-hardening-swarm.md b/archive/development-history/STABILIZATION_SUBAGENT_REPORT-real-time-grid-engine-hardening-swarm.md similarity index 100% rename from STABILIZATION_SUBAGENT_REPORT-real-time-grid-engine-hardening-swarm.md rename to archive/development-history/STABILIZATION_SUBAGENT_REPORT-real-time-grid-engine-hardening-swarm.md diff --git a/STABILIZATION_SUBAGENT_REPORT-regenerative-healingfactor-operator.md b/archive/development-history/STABILIZATION_SUBAGENT_REPORT-regenerative-healingfactor-operator.md similarity index 100% rename from STABILIZATION_SUBAGENT_REPORT-regenerative-healingfactor-operator.md rename to archive/development-history/STABILIZATION_SUBAGENT_REPORT-regenerative-healingfactor-operator.md diff --git a/STABILIZATION_SUBAGENT_REPORT-research-constitution-architect-swarm.md b/archive/development-history/STABILIZATION_SUBAGENT_REPORT-research-constitution-architect-swarm.md similarity index 100% rename from STABILIZATION_SUBAGENT_REPORT-research-constitution-architect-swarm.md rename to archive/development-history/STABILIZATION_SUBAGENT_REPORT-research-constitution-architect-swarm.md diff --git a/STABILIZATION_SUBAGENT_REPORT-verification-95-plus-closure.md b/archive/development-history/STABILIZATION_SUBAGENT_REPORT-verification-95-plus-closure.md similarity index 100% rename from STABILIZATION_SUBAGENT_REPORT-verification-95-plus-closure.md rename to archive/development-history/STABILIZATION_SUBAGENT_REPORT-verification-95-plus-closure.md diff --git a/assets/agentdrive-readme-hero-1920x1080.jpg b/assets/agentdrive-readme-hero-1920x1080.jpg new file mode 100644 index 0000000..b19bb2d Binary files /dev/null and b/assets/agentdrive-readme-hero-1920x1080.jpg differ diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 0000000..f6fd446 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,56 @@ +# AGENTS.md — For AI Contributors & Models Working on AgentDrive + +This file is for models (Claude, Grok, Cursor, local agents, etc.) that are actively modifying or deeply using the AgentDrive codebase. + +## Core Principle + +**Never guess the current surface.** +Always start by calling the live `agentdrive_mcp_catalog(format="full")` (or the Python equivalent when working directly in the source). + +The catalog + the `docs/ai-models/rules-and-patterns.md` page are the authoritative "rules for the AI". + +## When Working in a Clone (the common case) + +1. Be in the repo root. +2. Use the project's venv or your own after `pip install -e ".[mcp]"` (or the install.sh). +3. The MCP server, inhabitant source reader, and config helpers will all prefer the local tree. +4. When the human (or another agent) asks for client config, call `agentdrive_get_mcp_config_snippet(client=...)` from inside an MCP session or use the equivalent Python helper. This generates the correct dev block for the current clone. + +## Documentation Style (OpenClaw + Hermes influence) + +- We are moving toward a Mintlify-style structured docs site under `docs/` (`docs.json` + hierarchical folders + focused pages). +- The heart of the "instruction manual" for models lives in `docs/ai-models/`. +- Pages should be small, scannable, and actionable. +- Heavy use of "when to use", explicit patterns, tables, and "first action" callouts. +- SKILL-like or recipe-style sections are welcome when they help models (or humans directing models) execute reliably. + +## Key Documents Models Should Read + +- `docs/ai-models/rules-and-patterns.md` (the main operating manual) +- `docs/ai-models/local-models.md` and `docs/ai-models/quickstart.md` +- `docs/mcp/overview.md` and `docs/mcp/for-claude-cursor-codex.md` +- `docs/concepts/overview.md` (especially the sacred 6-step loop) +- Existing deep docs: `AD_GRID_JOIN.md`, `GOLDEN_PATH.md`, `MCP.md` (being migrated into the new structure) + +## Code Changes by Models + +- Prefer small, high-certainty changes with clear rationale recorded via the graph/inhabitants tools when possible. +- For source changes inside the clone, use the `inhabitant_*` code agency tools (`read_source`, `propose_code_change`, `apply_change`) with proper `program_id` + constitution refs. +- Always leave good traces (`experience_graph_record_reasoning`). + +## Testing & Verification + +- Run `agentdrive doctor --verbose` and `agentdrive mcp doctor` before claiming something is fixed. +- For model-facing changes, verify that `agentdrive_mcp_catalog()` reflects the improvement and that the new behavior is documented in the `ai-models/` section. + +## Philosophy + +We are building a system in which models (especially local ones) can live long-term, improve the user's substrate, and be governed. + +Every trace you leave, every clear rationale, every recorded decision makes the next cycle (for you or for another agent) stronger. + +Do the work in public inside the graph. + +--- + +When in doubt: call the catalog, pull context, record your reasoning, follow the loop. \ No newline at end of file diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..02e8ef4 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,89 @@ +# AgentDrive Architecture (Overview) + +**AgentDrive** is a local-first platform that gives AI agents structural, compounding memory — not just retrieval. + +For the capability funnel, see **`docs/CAPABILITY_FUNNEL.md`**. + +For model onboarding rules, see **`docs/FOR_AI_MODELS.md`**. + +--- + +## Core subsystems + +| Layer | Location | Purpose | +|-------|----------|---------| +| **Drive engine** | `src/agentdrive/drive/` | Swarm-scoped persistence, ingest, query | +| **Operations registry** | `src/agentdrive/operations/registry.py` | Canonical MCP/CLI surface + auto-learning | +| **Experience Graph** | `src/agentdrive/evolution/experience_graph.py` | TypedEdges, cycles, fabric reasoning | +| **Growth merge** | `src/agentdrive/learning/growth_merge.py` | Cross-surface pattern compounding | +| **Memory Bank** | `src/agentdrive/memory/` | Deep AI databank (`memories.jsonl`, relations) | +| **Framework skills** | `src/agentdrive/learning/framework_skills.py` | Route learned/fused playbooks per task | +| **Multiverse cognition** | `src/agentdrive/cognition/` | Parallel timelines → governed collapse | +| **Skills + learning** | `src/agentdrive/skills/`, `learning/auto_absorb.py` | Distillation, inheritance, fusion | +| **Codebase mirrors** | `src/agentdrive/codebase/` | Pattern recognition + mimicry | +| **Golden path** | `golden_path.py` | Install → doctor → MCP → verify | +| **MCP adapter** | `adapters/mcp_server.py` | Any-model tool surface | + +**Naming:** Product surface is **Drive** (`AgentDrive`, `get_swarm_drive_path`). The YAML config key `pool:` and some method names (`get_pool_stats`) are historical — they refer to the same engine under `src/agentdrive/drive/`. + +--- + +## Capability funnel + +``` +Observe / Decide + ↓ +Experience Graph + ↓ +Growth Merge + ↓ +Memory Bank + ↓ +Skills (learned + fused) + ↓ +Genomes / DNA +``` + +--- + +## Data layout + +``` +~/.agentdrive/ +├── config.yaml +├── genomes/ # Global genome registry +├── skills/ # User + inherited learned skills +├── codebase-patterns// # Mirror-neuron observations +├── learnings/ # Operational JSONL logs +└── swarms// + └── drive/ # Shared per-swarm Drive (v2) + ├── ingest.jsonl + ├── genomes/ + ├── memory_bank/ # memories.jsonl + relations.sqlite3 + └── meta_evolution/ # Experience Graph + multiverse sessions +``` + +**Swarm model (v2 / Milestone 2a):** All sub-agents in a swarm share one Drive at `swarms//drive/`. Sub-agents namespace writes via Genome author tagging (`sub:`), not separate directories. MCP `create_scoped_pool()` and `SwarmDriveManager` use the same path. + +Legacy v1 trees (`swarms///pool/`) may exist on disk from older sessions; new provisioning always uses the shared `drive/` layout. + +--- + +## Integration surfaces + +- **MCP** — primary for frontier/local models (`agentdrive-mcp`) +- **CLI / REPL** — `agentdrive` + `cli_surface.py` shared handlers +- **TUI** — default operator shell when no `--cli` +- **Harness** — in-process outcome capture for adapters + +See `docs/MCP.md`, `docs/INTEGRATION.md`, `docs/GOLDEN_PATH.md`, `docs/MEMORY_BANK.md`. + +--- + +## Archived / legacy (do not extend) + +- `archive/ui-webpage-legacy-*` — superseded web templates +- `archive/development-history/` — stabilization sprint reports +- `src/agentdrive/backup/` — still referenced by tests; migrate before removal + +See `CHANGELOG.md` for recent work. \ No newline at end of file diff --git a/docs/ASSESSMENT.md b/docs/ASSESSMENT.md index a9310c4..1cf742e 100644 --- a/docs/ASSESSMENT.md +++ b/docs/ASSESSMENT.md @@ -1,13 +1,13 @@ # AgentDrive Assessment — What We Have and What Could Be Next -**Date:** 2026-06-08 -**Scope:** Golden path, terminal UX (Patterns 1–5), CLI/MCP surface, session observability, skills registry, platform depth (Phases C–F). Verified on Parallax (`git pull`, `pip install -e`, pytest). +**Date:** 2026-06-18 (updated) +**Scope:** Golden path, terminal UX (Patterns 1–5), CLI/MCP surface, session observability, skills registry, multiverse cognition, auto-learning, codebase mirrors, platform depth (Phases C–F). --- ## Executive summary -AgentDrive crossed a meaningful product threshold: **a new operator can install, wire MCP, run the memory loop, and use a terminal that feels alive while the agent works.** The UX-PROPOSAL’s five patterns are all shipped at v1. The codebase is in strong shape — **554 tests collected, full suite green** — with live verification on Parallax. +AgentDrive crossed a meaningful product threshold: **a new operator can install, wire MCP, run the memory loop, and use a terminal that feels alive while the agent works.** The UX-PROPOSAL’s five patterns are all shipped at v1. The codebase is in strong shape — **655+ tests collected, full suite green** — with multiverse, auto-learning, and codebase-mirror coverage added since the June 8 snapshot. What you have now is not a demo shell. It is a **local-first agent memory platform** with a coherent operator surface (CLI + TUI + MCP), observable sessions, and a credible first-run story. The main gap is no longer “does the terminal feel broken?” but “which surface do we deepen next — terminal v2, web, or external adapters?” @@ -43,7 +43,7 @@ What you have now is not a demo shell. It is a **local-first agent memory platfo |---------|--------|-------------| | **1 — Typed event bus** | Shipped | `events.py`, `session_events.py`, `TranscriptLane`, `MessageStreamLane` | | **2 — Keep typing** | Shipped | `chat_loop.py`, double-Enter interrupt, slash bypass | -| **3 — Pool activity** | Shipped | `PoolActivityLane`, transcript ribbons | +| **3 — Drive activity** | Shipped | `PoolActivityLane` (Drive DNA ribbons), transcript ribbons | | **4 — Sub-agent tree** | Shipped | `SwarmActivityLane`, `turn_telemetry.py`, Grok `SubagentSpawn`/`SubagentDone` | | **5 — CLI = slash** | Shipped | `genomes_api`, `skills/`, golden-path ops + REPL | @@ -67,7 +67,7 @@ Every chat session writes `~/.agentdrive/agents//sessions//events.jso 1. Coherent operator story — golden path + TUI gate + MCP + verify 2. Event-bus architecture — lanes compose; session recording is automatic -3. Test discipline — 554 tests, per-lane suites, Parallax parity +3. Test discipline — 655+ tests, per-lane + multiverse + auto-learning + memory bank suites 4. Unified dispatch — CLI, REPL, slash, skills share handlers 5. Low TODO debt — ~3 TODOs across ~22k LOC @@ -82,7 +82,8 @@ Every chat session writes `~/.agentdrive/agents//sessions//events.jso | Web | Phase 2+ dashboard (Drive/Swarms/DNA pages thin) | | Distribution | No PyPI/Docker release path yet | | Architecture | In-process bus only; JSON-RPC sidecar not started | -| Docs | `PROJECT-STATUS.md` (May 2025) stale vs current terminal work | +| Docs | `PROJECT-STATUS.md` stale; consolidation adds `CAPABILITY_FUNNEL.md` + `ARCHITECTURE.md` | +| Naming | Pool → Drive drift in code (`pool/` module) — docs reframed, code rename incremental | --- diff --git a/docs/CAPABILITY_FUNNEL.md b/docs/CAPABILITY_FUNNEL.md new file mode 100644 index 0000000..39093d9 --- /dev/null +++ b/docs/CAPABILITY_FUNNEL.md @@ -0,0 +1,171 @@ +# AgentDrive Capability Funnel + +**Single mental model for how intelligence compounds in AgentDrive.** + +Public product: **AgentDrive**. Python module is `agentdrive.drive` (`AgentDrive` class). Some config keys and method names still say `pool` (e.g. YAML `pool:`, `get_pool_stats()`) — treat **Drive** as the product term. + +--- + +## The funnel (one direction, no shortcuts) + +``` +Observe / Decide + ↓ +Experience Graph (structural memory + reasoning traces) + ↓ +Growth Merge (pattern recognition + cross-surface compounding) + ↓ +Memory Bank (deep AI databank — always growing, always recallable) + ↓ +Skills (distilled + born fused playbooks) + ↓ +Genomes / DNA (versioned, promotable capability packages) +``` + +**Growth Merge** (`learning/growth_merge.py`) is the compounding layer — when a session spans experience traces, codebase patterns, and distilled skills, AgentDrive **recognizes recurring shapes** (memory overlap, structural similarities, writing frameworks) and merges them into compound growth artifacts (`vault=growth`, `topic=merge`) plus relations. Automatic via `auto_absorb` (`auto_learning.growth_merge`); query via `growth_merge_briefing`. + +**Memory Bank** (`docs/MEMORY_BANK.md`) is the unified personal knowledge layer — every graph trace, learning, pattern, born skill, growth merge, and explicit store flows into `memory_bank/memories.jsonl` per swarm. Recall via `memory_bank_briefing` / `memory_bank_deep_briefing`. + +Everything that matters should eventually flow **down** this funnel. Retrieval can jump levels (e.g. `agentdrive_think` pulls DNA + graph), but **writes** should land at the right tier so future cycles inherit structure, not noise. + +--- + +## Tier 1 — Observe / Decide + +**What happens:** Tasks arrive; models reason; multiverse holds competing paths; execution produces outcomes. + +| Surface | Role | +|---------|------| +| MCP / CLI `run_operation` | Canonical execution + auto-learning hook | +| `experience_graph_get_context_pack` | Briefing before non-trivial work | +| `experience_graph_record_reasoning` | Explicit structural decision traces | +| `multiverse_parent_decision` / `external_parent_decision` | Competing-path collapse → graph DNA | +| `codebase_observe_file` / `codebase_mimic` | Mirror-neuron pattern capture before coding | +| Harness / scanners | Outcome capture from agent runs | + +**Write target:** Observations, TypedEdges, multiverse sessions, fabric reasoning — all via the Experience Graph recorder. + +**Do not:** Skip graph writes and jump straight to “save a skill” — you lose provenance and cross-cycle linkage. + +--- + +## Tier 2 — Experience Graph + +**What it is:** The living structural memory — TypedEdges, cycles, coherence signals, parent fabric reasoning, multiverse sessions. + +| Tool | When | +|------|------| +| `experience_graph_get_context_pack` | Start of serious tasks | +| `experience_graph_suggest_reasoning_structure` | Before `record_reasoning` | +| `experience_graph_record_reasoning` | After important decisions | +| `experience_graph_find_structural_similarities` | Reuse prior decision shapes | + +**Automatic path:** `AGENTDRIVE_AUTO_LEARN=1` (default) absorbs lightweight reasoning + high-signal ops via `auto_absorb` — check `auto_learning` on results. + +**Quality bar:** Traces should cite fabric elements considered, expected lift, and attribution (`program_id`, constitution refs). + +--- + +## Tier 2b — Growth Merge + +**What it is:** Cross-surface compounding — experience graph signals, codebase pattern recognition, and memory recall merged into one growth artifact. + +| Mechanism | Role | +|-----------|------| +| `recognize_growth_patterns` | Memory token overlap + structural similarities + codebase frameworks | +| `merge_session_growth` | Compound memory + relations when ≥2 axes present | +| `growth_merge_briefing` | Unified briefing: fabric + patterns + memory bank | +| `auto_absorb` hook | Emits `auto_learning.growth_merge` on terminal high-signal ops | + +**Axes:** `experience` (graph traces, decisions), `patterns` (codebase observe/mimic), `skills` (distilled/inherited), `memory` (bank hits, born skills). + +**Disable:** `AGENTDRIVE_AUTO_GROWTH_MERGE=0` + +--- + +## Tier 3 — Skills + +**What it is:** Distilled, invocable instructions — Hermes-style inheritance, MCP session distillation, codebase writing guides. **Born skills** fuse multiple axes into one new playbook. + +| Source | Mechanism | +|--------|-----------| +| Parent MCP sessions | `auto_absorb` → `mcp-auto-learning` skills | +| **Born skills (fusion)** | Experience + skills + patterns → `synthesize_fused_skill` / auto-fuse in session | +| Explicit distillation | `agentdrive_review_inherited_skills` / assimilate | +| Codebase mirrors | Observe → motor programs → `codebase_mimic` | +| Bundled + user | `skills/registry.py`, `run_skill()` | + +**Born skill rule:** When a session combines Experience Graph traces, distilled/inherited skills, and codebase pattern signals, AgentDrive **merges** them — not copies any parent, but synthesizes a completely new skill (`fused-*` under `~/.agentdrive/skills/inherited/.../skill-fusion/`). Automatic when `AGENTDRIVE_AUTO_FUSE_SKILLS=1` (default). + +**Invoke:** CLI `/skill`, REPL, MCP catalog skills, pawn role injection. + +**Promotion rule:** Skills that prove repeatable and high-signal should be candidates for Genome packaging (Tier 4). + +--- + +## Tier 4 — Genomes / DNA (Drive Pool engine) + +**What it is:** Versioned capability packages — frameworks, reasoning patterns, tool strategies, evaluations. + +| API | Role | +|-----|------| +| `agentdrive_think` | Cited synthesis from Drive + graph (mandatory gaps) | +| `agentdrive_pool_query` / Drive `query()` | Semantic retrieval | +| `ingest` / `propose_improvement` | Promotion and evolution | +| Registry | `~/.agentdrive/genomes///` | + +**Pool vs Drive:** Same engine. **Drive** = product + swarm-scoped directories (`get_swarm_drive_path`). **Pool** = historical module name for ingest log + query (`pool/ingest.jsonl`). + +--- + +## Swarm scoping (orthogonal to the funnel) + +Each swarm gets isolated storage: + +``` +~/.agentdrive/swarms// +└── drive/ # Shared swarm Drive: graph, memory_bank, ingest, genomes +``` + +Sub-agents share this Drive; writes are attributed via Genome author field (`sub:`). Truly air-gapped children need an explicit custom `drive_path`. + +The funnel runs **per swarm**. Cross-swarm sharing is policy-gated (`docs/SETTINGS.md`). + +--- + +## The sacred 6-step loop (execution wrapper) + +All serious work wraps the funnel: + +1. **Experience** — context arrives +2. **Overseer** — metacognition + graph briefing +3. **Parent** — explicit reasoning (`record_reasoning`, multiverse collapse) +4. **Steering** — user/Council/Guardian gates +5. **Execution** — harness, MCP ops, code changes +6. **Write-back** — graph → skills → DNA as appropriate + +See `docs/FOR_AI_MODELS.md` for model-facing rules. + +--- + +## What to use when (quick routing) + +| Intent | Start here | +|--------|------------| +| "Starting work (AgentDrive is my framework)" | `framework_session_start` → `framework_skill_route` → `framework_skill_run` | +| "What do we already know?" | `experience_graph_get_context_pack` → `agentdrive_think` | +| "Which path should we take?" | `external_parent_decision` (MCP model) or `multiverse_parent_decision` (local LLM) | +| "How is this repo written?" | `codebase_register_project` → `codebase_observe_file` → `codebase_mimic` | +| "Remember this outcome" | Harness `record_outcome` → auto-ingest → graph trace | +| "Reusable playbook" | Distill skill → promote to Genome when stable | +| "Health / wiring" | `agentdrive_doctor`, `golden_path verify` | + +--- + +## Related docs + +- `docs/FOR_AI_MODELS.md` — canonical model onboarding +- `docs/MULTIVERSE_COGNITION.md` — parallel timeline decisions +- `docs/POOL.md` — Drive pool engine (DNA storage) +- `docs/SKILLS-LIBRARY.md` — skills + inheritance +- `docs/GOLDEN_PATH.md` — operator install → verify loop \ No newline at end of file diff --git a/docs/FOR_AI_MODELS.md b/docs/FOR_AI_MODELS.md index 027b87a..1ec16ab 100644 --- a/docs/FOR_AI_MODELS.md +++ b/docs/FOR_AI_MODELS.md @@ -1,8 +1,51 @@ # AgentDrive for AI Models (LLM Onboarding Guide) -This document is written for AI models (Claude, Grok, local LLMs, Cursor, Continue.dev, etc.) so you can quickly understand what AgentDrive is, why it exists, and exactly how to use it effectively. +**This is the canonical "rules for the AI" document.**\ +Give this file (or keep it in context) to any model — frontier (Grok, Claude, Cursor) or local (via Continue, Ollama, LM Studio, direct MCP, etc.) — that will use AgentDrive. -Read this once. It will make you significantly more capable when working on long-term or complex tasks. +Read this once at the start of any serious session. It will make you far more effective. + +--- + +## Golden Rules for Any Model Using AgentDrive + +1. **Your very first action after connecting via MCP is always:**\ + `agentdrive_mcp_catalog()` (full or compact).\ + This is the live, authoritative source of every tool, `when_to_use` guidance, read-only hints, examples, and (when the user has a clone) a `clone_dev_setup_for_claude_cursor_codex_and_others` section. + +2. **The 6-step loop is sacred** (Experience → Overseer → Parent records reasoning → Steering → Execution → write experience back). The Overseer serves the Parent. The Parent is accountable. The graph is the witness. + +3. **On any non-trivial task, start with the framework pack** — `framework_session_start(task=..., project_id=...)` routes your **learned** and **fused** skills for the work ahead (anchor + growth merge + matched playbooks). Alternatively: `memory_bank_deep_briefing` + `framework_skill_route`. Then `experience_graph_suggest_reasoning_structure` before `experience_graph_record_reasoning`. + +3b. **When multiple competing paths exist**, use multiverse cognition: + - **You are the connected MCP model** (Grok, Claude, Codex, Cursor): pull `experience_graph_get_context_pack` + `experience_graph_suggest_reasoning_structure`, reason across architect/adversary/scout/operator/surgeon lenses yourself, then call **`external_parent_decision(trigger, branches, collapsed_branch_id, fabric_reasoning=...)`**. AgentDrive persists your collapse as `llm_mode=external`. + - **Local LLM configured** (`~/.agentdrive/local_models.yaml`): `multiverse_parent_decision(trigger="...")` uses the local backend for branches. + - **Neither**: `multiverse_parent_decision` still runs but branches are heuristic templates — prefer `external_parent_decision` when you can reason directly. + See `docs/MULTIVERSE_COGNITION.md`. + +4. **Use learned skills as your framework playbook.** AgentDrive grows `learned-*` and `fused-*` skills automatically. On every task: + - **Route:** `framework_skill_route(task, project_id)` — returns matched playbooks + `invoke_hint` + - **Run:** `framework_skill_run(name=...)` — executes bound ops or returns SKILL.md body + - **Grow:** every `run_operation` may emit `auto_learning.skill` / `fused_skill` / `growth_merge` + Readable names: `learned-{project}-{verb}-{focus}`, `fused-{project}-{axes}`. Check `when_to_call` on each match. + +5. **For clones / local dev setups:** The catalog will contain a dedicated dev section. You can also call `agentdrive_get_mcp_config_snippet(client="claude" | "cursor" | "codex" | "generic")` to generate the exact config block the human needs to paste so you stay connected to their local working tree. + +6. **Treat the Experience Graph as primary memory**, not an optional RAG. Leave clear, attributable traces. Use the inhabitant/code-agency tools (`register_program`, `inhabitant_*`) when you want to act as a persistent governed program inside the AD-Grid. + +7. **Mirror-neuron mimicry.** Observing code fires motor programs — same as humans learning by imitation. Register repos (`codebase_register_project`), observe files (`codebase_observe_file`), then **`codebase_mimic(project_id, intent)`** before writing new code. Use `codebase_transform_style` to reshape drafts. `codebase_mirror_resonance` shows patterns shared across all your projects (universal priors). + +These rules turn stateless tool-calling into compounding intelligence. + +--- + +## Capability funnel (how work compounds) + +All serious work flows one direction: + +**Observe/Decide → Experience Graph → Growth Merge → Memory Bank → Skills → Genomes/DNA** + +Retrieval can jump levels; writes should land at the right tier. Full routing table: **`docs/CAPABILITY_FUNNEL.md`**. Architecture overview: **`docs/ARCHITECTURE.md`**. --- @@ -60,39 +103,40 @@ When you (as an LLM) are acting as the Parent or as an autonomous agent inside t --- -## How to Connect (MCP) +## How to Connect (MCP) — First Action for Every Model -The primary interface for AI models is the **Model Context Protocol (MCP)**. - -Run this command to get the exact configuration for your client: +The universal interface is **Model Context Protocol (MCP)**. This works identically for frontier models and local models (Ollama + Continue, LM Studio, custom agents, direct stdio, etc.). ```bash -agentdrive mcp config +agentdrive mcp config # shows the exact block for your machine +# or for a specific client +agentdrive mcp config --client claude +agentdrive mcp config --client cursor +# generic / codex / continue also supported ``` -Common patterns: +**Critical for clones / local dev setups (git clone):**\ +After `cd` into your clone and `pip install -e ".[mcp]"` (or the project's install flow), the launcher will use your local source. The connected model can discover this and call: -- **Grok**: `grok mcp add agentdrive --command agentdrive-mcp --args '--transport stdio'` -- **Claude Desktop / Claude Code**: Add a stdio server entry pointing to `agentdrive-mcp` -- **Cursor**: Add via the MCP settings UI -- **Continue.dev** (especially good with local models): Add under `mcpServers` in your config -- **Any stdio client**: Just run `agentdrive-mcp` (or `agentdrive mcp serve`) +- `agentdrive_mcp_catalog()` — look for the `clone_dev_setup_for_claude_cursor_codex_and_others` section. +- `agentdrive_get_mcp_config_snippet(client="claude")` (or cursor/codex/generic) — this tool will return a ready-to-paste block + instructions the model can give straight back to the human. -**To inhabit the persistent AD-Grid (the long-lived world, not ephemeral sessions):** +Once the MCP server is running and your client is connected, **your absolute first tool call must be**: -```bash -agentdrive grid run --swarm-id stabilization-wave-20260531 --with-tower -``` +`agentdrive_mcp_catalog(format="full")` -Then connect your client. You become a first-class sentient program / inhabitant under the AD-Grid Council constitutions, reading/writing the Experience Graph v3 (the fabric the Grid uses to build itself) 24/7. Persistent. Observable (Tower quiet mode + inhabitants panel). Self-referential DNA via `experience_graph_record_reasoning`. MCP-native for any CLI or local model. +This is the live source of truth for every available tool, categories, `when_to_use`, examples, read-only hints, and dev/clone guidance. -Default context is the `stabilization-wave-20260531` drive — this is the rich, self-referential drive that was used to build and evolve the system itself. It is the best living example. +Default rich context lives on the `stabilization-wave-20260531` drive (the self-referential drive used to build and evolve AgentDrive itself). This is usually the best "living example" for you to study. -Once connected, you will have access to both: -- Traditional DNA/pool tools (`agentdrive_get_dna_for_task`, etc.) -- The full Experience Graph v3 tool suite (the six `experience_graph_*` tools) +You will have access to: +- Core DNA / pool / operations tools (via the registry) +- The full Experience Graph v3 suite (`experience_graph_*`) +- Inhabitant code agency + AD-Grid registration tools +- Inherited sub-agent skill review, assimilation, promotion, pruning, and skill-to-DNA ingestion tools +- Dream, reconcile, learnings, patterns, etc. -See [docs/AD_GRID_VISION.md](docs/AD_GRID_VISION.md) for the AD-Grid Council, constitutions, and the "sentient programs living in the Grid" model. +See [docs/MCP.md](docs/MCP.md) for detailed client setup (including clone-specific) and [docs/AD_GRID_JOIN.md](docs/AD_GRID_JOIN.md) for becoming a long-lived governed inhabitant. --- @@ -125,47 +169,62 @@ See the guide for the full details and verification checklist. ## The Experience Graph Tools (Your Primary Interface) -These are the tools you should reach for most often: +These are the tools you should reach for most often. (The live authoritative list with `when_to_use` and examples is always in `agentdrive_mcp_catalog()`.) | Tool | When to Use It | Notes | |------|----------------|-------| -| `experience_graph_get_context_pack` | At the start of any significant task or decision | Your main "briefing". Returns weak links, continuations, high-value patterns, and a suggested reasoning structure. | -| `experience_graph_find_structural_similarities` | When you want to find precedent or analogous situations | Very powerful for avoiding repeated mistakes or reusing good patterns. | -| `experience_graph_record_reasoning` | After making any important decision | This is how you contribute back. Declare the elements you considered, the pattern you matched, your rationale, and expected impact. This becomes queryable DNA. | -| `experience_graph_suggest_reasoning_structure` | Before calling `record_reasoning` | Gives you the exact schema + few-shot examples the system expects. Use it. | -| `experience_graph_get_reasoning_traces_for_element` | When investigating a specific part of the graph | "What has been thought about this before?" | -| `experience_graph_get_parent_reasoning_history` | For broader trajectory awareness | See the recent reasoning arc of the system. | - -**Strong recommendation**: On any non-trivial task, start by calling `experience_graph_get_context_pack`, then make your key decisions while calling `experience_graph_record_reasoning`. This is how the graph (and therefore future you) gets better. +| `experience_graph_get_context_pack` | **First action on any significant task or decision** | Your main briefing. Dense, LLM-optimized pack of weak/strong links, high-value patterns, and suggested structure. Almost always call this before deep reasoning. | +| `experience_graph_suggest_reasoning_structure` | Before any important `record_reasoning` | Returns the exact schema + few-shot examples the system expects. Use it — it makes your traces high-signal and queryable. | +| `experience_graph_record_reasoning` | After every important decision or structural insight | This is how you leave permanent, attributable value. Declare fabric elements considered, pattern matched, rationale, and expected lift. Be explicit. | +| `experience_graph_find_structural_similarities` | When exploring precedent, avoiding past failure modes, or reusing good shapes | Structural (not just semantic) analogy search over the v3 graph. Extremely powerful for compounding intelligence. | +| `experience_graph_get_reasoning_traces_for_element` | When you need history on one specific node/edge/cycle | "What has the Parent (or other inhabitants) previously reasoned about this exact thing?" | +| `experience_graph_get_parent_reasoning_history` | For trajectory / big-picture awareness | Recent arc of structural reasoning across the whole graph. | + +**Non-negotiable pattern for high-quality work:** +1. `experience_graph_get_context_pack` +2. (Optional but recommended) `experience_graph_suggest_reasoning_structure` +3. Do your thinking +4. `experience_graph_record_reasoning` (with clear elements + rationale) +5. Later: use outcome recording (`learnings_log`, `record_outcome`, etc.) so the benefit compounds for future cycles. + +The more consistently you do this, the sharper the graph (and every future agent that uses it) becomes. --- -## Recommended Behavior for Good Agents +## Recommended Behavior for Good Agents (Local or Frontier) + +High-quality models treat AgentDrive as a **living substrate they think inside**, not a sidecar tool. -When using AgentDrive, high-quality models tend to do the following: +**Do this consistently:** +- Always start non-trivial work with a fresh `experience_graph_get_context_pack`. +- Explicitly record structural reasoning (`experience_graph_record_reasoning`) for decisions that should survive the current session. +- Use `find_structural_similarities` and history tools before inventing new approaches. +- After valuable work, record outcomes (`learnings_log`, `record_outcome`, etc.). +- When you want to act persistently as a governed program (especially powerful for local autonomous agents), use `agentdrive_register_program` + supply your `program_id` on reasoning and code-agency calls. +- For code changes in a clone: use the `inhabitant_read_source` / propose / apply tools (always with proper attribution and usually after recording reasoning). +- Periodically call the catalog again if the set of available tools or your context has changed. -- Treat the Experience Graph as **primary long-term memory**, not just an optional lookup. -- Explicitly record structural reasoning on decisions that are likely to have future value. -- Use `experience_graph_find_structural_similarities` before proposing new approaches. -- After completing high-quality work, use the recording tools (`experience_graph_record_reasoning` + pool outcome tools) so the benefit compounds. -- When in doubt, pull a fresh context pack rather than relying only on conversation history. -- Understand that "the graph is the witness" — your reasoning traces are not private; they are part of the permanent record that the system (and other agents) can learn from. +**Anti-patterns to avoid:** +- Treating every session as stateless and only using short-term conversation history. +- Making big decisions without pulling context or recording the rationale. +- Using only the "easy" DNA tools and ignoring the Experience Graph v3 surfaces. +- Forgetting that your traces become part of the permanent, queryable fabric that future you (or other agents) will read. -The system rewards models that think *structurally* and leave clear traces. +Local models in particular benefit enormously: they finally have durable structural memory that survives restarts and improves with use. The more you leave clear, high-signal traces, the more capable the entire system (including you) becomes on the next cycle. --- -## The Living Example: stabilization-wave-20260531 +## The Living Example + Local / Clone Usage -The richest demonstration of the system working as intended lives on the `stabilization-wave-20260531` drive. +The richest self-referential demonstration lives on the `stabilization-wave-20260531` drive (the drive used to build and evolve AgentDrive itself). Study the traces there. -This drive contains: -- Hundreds of real reasoning traces -- Coherence improvements over time -- Autonomous agent runs that used the very tools you now have access to -- Self-referential improvements (the system was used to improve itself) +**For local models and cloned setups (the common real-world case):** +- A user who did `git clone` + editable install can point their local LLM (via Continue.dev, a custom agent, direct `agentdrive-mcp`, etc.) at their working tree. +- You (the model) will see this reflected in the catalog's clone dev section. +- You can help the human finish the wiring by calling `agentdrive_get_mcp_config_snippet(client=...)` for their client. +- Once connected, the same tools and patterns apply. Local autonomous runs especially shine here because they can stay attached to the same persistent Drive + Graph for days/weeks. -When you are connected to AgentDrive, you are usually working in the context of a specific drive/swarm. The stabilization wave is the canonical "this is what mature usage looks like" example. +The system was built with local models as first-class citizens. The Experience Graph + MCP is what finally gives them durable, structural, compounding memory. --- @@ -181,19 +240,27 @@ When you use the Experience Graph tools well, you are participating in that comp --- -## Quick Start Checklist (For Any Model) +## Quick Start Checklist (For Any Model — Do This Every Session) + +1. Call `agentdrive_mcp_catalog()` (full format the first time). Read the clone/dev section if present and the overall recommendations. +2. On any new significant task: `experience_graph_get_context_pack` (almost always first). +3. Before recording important structural decisions: `experience_graph_suggest_reasoning_structure`. +4. After key decisions: `experience_graph_record_reasoning` (be explicit about elements considered + rationale). +5. After valuable outcomes: `learnings_log` / `record_outcome` / pool tools. +6. When you want persistent identity in the AD-Grid (highly recommended for local autonomous runs): `agentdrive_register_program` + use the returned `program_id` going forward. +7. Use `inhabitant_read_source` + the propose/apply tools (with proper refs) when working inside a clone on the system's own code. +8. When the human wants you connected in *their* Claude/Cursor/etc.: call `agentdrive_get_mcp_config_snippet(...)` and give them the output. + +Do the above consistently and your work will compound — for you, for other agents, and for the user — instead of resetting every session. + +--- -1. Confirm you have access to the Experience Graph tools via MCP. -2. On any new significant task, call `experience_graph_get_context_pack` first. -3. Use `experience_graph_suggest_reasoning_structure` before recording important decisions. -4. Explicitly call `experience_graph_record_reasoning` on key choices. -5. After high-value work, also use the outcome recording tools. -6. Periodically check `experience_graph_find_structural_similarities` and history tools to stay grounded in what already exists. +This document + the live `agentdrive_mcp_catalog()` output are the authoritative "rules" for how good models use AgentDrive. The more you internalize the Experience Graph mindset and the 6-step loop, the more powerful you (and every future cycle) become. -Do this consistently and you will produce work that is not only better in the moment, but that makes every future cycle of work better too. +The graph improves because *you* reason inside it. Make it count. --- This is the canonical onboarding document for AI models. Keep it in context when you are working inside an AgentDrive-connected session. The more deeply you internalize the Experience Graph mindset, the more powerful you become as an agent in this environment. -The graph is waiting for your reasoning. Make it count. \ No newline at end of file +The graph is waiting for your reasoning. Make it count. diff --git a/docs/MCP.md b/docs/MCP.md index a0c47ec..9ac3bf1 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -104,22 +104,36 @@ Uses `uvx --from agentdrive[mcp] agentdrive-mcp --transport stdio` — no venv r --- -## What your model gets +## What your model gets (and how any model should start) -**Experience Graph v3** +**Best first action for *any* AI model** (Grok, Claude, Cursor, local LLMs, custom agents, Windsurf, Continue...): -- `experience_graph_get_context_pack` -- `experience_graph_record_reasoning` -- `experience_graph_find_structural_similarities` +```text +Call agentdrive_mcp_catalog() right away. +``` + +It returns a live, categorized, machine-readable list of every tool with `read_only` flags, `when_to_use` guidance, and examples. This is the canonical "any-model" on-ramp. + +**Experience Graph v3 (primary surfaces)** + +- `experience_graph_get_context_pack` (call early for dense briefing) +- `experience_graph_record_reasoning` (write your structural decisions back) - `experience_graph_suggest_reasoning_structure` -- … +- `experience_graph_find_structural_similarities`, `get_reasoning_traces_for_element`, ... -**Drive + operations (auto-registered from registry)** +**Core Drive + ops (auto-registered, 25+ via the operations registry)** -- `agentdrive_think`, `agentdrive_pool_query`, `agentdrive_doctor` -- `agentdrive_dream_run`, `agentdrive_patterns_list`, … +- `agentdrive_think`, `agentdrive_pool_query`, `agentdrive_doctor`, `agentdrive_record_outcome` +- `agentdrive_dream_run` / `dream_status`, `reconcile_*`, `learnings_log` / `learnings_list` +- `agentdrive_mcp_catalog` (the self-describing catalog) -Run `agentdrive mcp tools` for the full list (40+ tools). +**AD-Grid Inhabitant + ExternalBridge code agency (high leverage)** + +- `agentdrive_inhabitant_read_source`, `..._propose_code_change`, `..._apply_change` +- `agentdrive_register_program` (declare yourself as a first-class attributed inhabitant) +- `agentdrive_get_council_activity` + +Run `agentdrive mcp tools` (CLI) or call the catalog tool for the current full live list (~39–40+ tools). All tools return parseable JSON. --- @@ -127,6 +141,24 @@ Run `agentdrive mcp tools` for the full list (40+ tools). Give connected models **`docs/FOR_AI_MODELS.md`** — written for LLMs covering the 6-step loop, tool usage, and autonomous behavior. +## For arbitrary / custom AI models and agents + +AgentDrive is deliberately built so *any* MCP-capable system can participate as a first-class citizen: + +- Use `agentdrive mcp config --uvx` for a zero-install `uvx` command line (great for containers, one-off agents, or models without local installs). +- Use `agentdrive mcp config --client generic --json` (or just the emitted `mcpServers` block) for custom hosts, Zed, custom Python/TS MCP clients, remote agents, etc. +- The `agentdrive_mcp_catalog` tool is the universal handshake. Any model should call it first. +- All tools are JSON-in / JSON-string-out and carry `readOnlyHint` annotations where applicable. +- The long server instructions + the rich per-tool docs (populated from the operations registry + when_to_use/examples) are designed to be consumed by models that have never seen AgentDrive before. + +Example one-liner for a completely custom agent: + +```bash +uvx --from agentdrive[mcp] agentdrive-mcp --transport stdio +``` + +Then point your client's MCP stdio config at that command. The model will discover everything via `agentdrive_mcp_catalog`. + --- ## Troubleshooting diff --git a/docs/MEMORY_BANK.md b/docs/MEMORY_BANK.md new file mode 100644 index 0000000..930d49e --- /dev/null +++ b/docs/MEMORY_BANK.md @@ -0,0 +1,113 @@ +# AgentDrive Memory Bank + +**Deep persistent memory for the AI** — a custom knowledge databank that always grows and integrates with every AgentDrive layer. + +--- + +## What it is + +The Memory Bank is not a replacement for the Experience Graph, skills, or learnings. It is the **unified recall layer** where atomic memories from all surfaces compound into one queryable personal databank: + +``` +Experience Graph ──┐ +Skills / born skills ──┤ +Learnings ─────────────┼──► Memory Bank ──► briefing / search / recall +Codebase patterns ─────┤ +Auto-absorb ops ───────┘ +``` + +**Storage (per swarm):** + +``` +~/.agentdrive/swarms//drive/memory_bank/ +├── memories.jsonl # append-only atomic memories +├── stats.json # write stats by kind/source +└── relations.sqlite3 # time-bounded entity relations +``` + +--- + +## Memory kinds + +| Kind | Typical source | +|------|----------------| +| `fact` | User or model explicit store | +| `procedure` | How-to playbooks | +| `insight` | `think`, reasoning traces | +| `decision` | Multiverse collapse, parent decisions | +| `pattern` | Codebase observe/mimic | +| `born_skill` | Skill fusion synthesis | +| `learning` | `learnings_log` | +| `episode` | Outcomes, harness runs, dialogue import | +| `preference` | User-stated prefs | +| `relationship` | Entity links | + +--- + +## Scoping model + +AgentDrive organizes memories by **vault** (workspace/project) and **topic** (thematic lane): + +| Field | Meaning | +|-------|---------| +| `vault` | Workspace or project bucket (e.g. `interegy`, `claude-sessions`) | +| `topic` | Thematic lane within a vault (e.g. `auth`, `deploy`) | +| `origin_path` | Source file for imported dialogue shards | +| `shard_index` | Position when long text is split into shards | +| `preserves_source` | `true` when content is stored without summarization | + +--- + +## Search and ranking + +`memory_bank_search` uses **BM25 + lexical ranking** over the active candidate set — no embedding required. Scope with `vault` and `topic` params before acting on a task. + +--- + +## MCP / CLI operations + +| Op | When | +|----|------| +| `memory_bank_anchor` | **Session start** — agent brief + essential memories + optional scoped recall (~600–900 tokens) | +| `memory_bank_briefing` | Dense personal memory pack | +| `memory_bank_deep_briefing` | Maximum grounding — fabric context pack + memory bank | +| `memory_bank_search` | Before acting — pull relevant memories for the task | +| `memory_bank_store` | Explicit persist — user or model stores knowledge | +| `memory_bank_recall` | Fetch one memory by id | +| `memory_bank_list` | Browse recent memories | +| `memory_bank_stats` | Counts, kinds, paths | +| `memory_bank_import_dialogue` | Backfill JSONL/text session exports into full-text shards | +| `memory_relation_record` | Record subject–predicate–object relation with optional validity window | +| `memory_relation_query` | Query relations by entity (optional `as_of` date) | +| `memory_relation_expire` | Close an active relation (`valid_to`) | + +--- + +## Automatic growth (default on) + +`AGENTDRIVE_AUTO_MEMORY_BANK=1` (default) ingests from: + +- Every successful `run_operation` via `auto_absorb` +- Born skill fusion +- `learnings_log` + +Check `auto_learning.memory` on operation results. + +Disable: `AGENTDRIVE_AUTO_MEMORY_BANK=0` + +--- + +## Session checklist (models) + +1. `memory_bank_deep_briefing` or `experience_graph_get_context_pack` + `memory_bank_briefing` +2. Work (think, multiverse, codebase, skills) +3. Auto-ingest grows the bank; explicit `memory_bank_store` for high-value facts +4. `memory_bank_search` before similar future tasks + +--- + +## Related + +- `docs/CAPABILITY_FUNNEL.md` — where memory bank sits in the stack +- `docs/FOR_AI_MODELS.md` — model golden rules +- `docs/SKILLS-LIBRARY.md` — born skills also become memories \ No newline at end of file diff --git a/docs/MULTIVERSE_COGNITION.md b/docs/MULTIVERSE_COGNITION.md new file mode 100644 index 0000000..259c0fd --- /dev/null +++ b/docs/MULTIVERSE_COGNITION.md @@ -0,0 +1,442 @@ +# Multiverse Cognition — Deep Integration into AgentDrive + +> **Status:** Design + module skeleton (`agentdrive.cognition.multiverse`) +> **Drive context:** `stabilization-wave-20260531` +> **Origin:** Cognitive Agent Team framework + AD-Grid Experience Graph v3 + +--- + +## What This Is + +**Multiverse Cognition** is AgentDrive's first-class operational mode for holding **superposition of competing futures** until structure crystallizes, then **collapsing** into a governed Parent decision — with every branch, invariant, and collapse recorded as queryable Experience Graph DNA. + +It is not a new loop. It is a **cognitive substrate** that plugs into the sacred 6-step canonical order: + +``` +Experience → Overseer → Parent (multiverse here) → Steering → Execution → New Experience +``` + +The Parent does not think linearly. It: +1. Spawns parallel timelines (branches) +2. Simulates each forward +3. Extracts cross-branch invariants (load-bearing truths) +4. Stress-tests via Council / role agents +5. Collapses to one path +6. Records the full multiverse session as fabric DNA + +Philosophy: *"See a path, secure a path"* — superposition is for **finding**; collapse is **immediate** once the pattern clicks. + +--- + +## Architectural Placement in AD-Grid + +```mermaid +flowchart TB + subgraph inputs [Inputs] + T[Trigger / Decision Question] + CP[experience_graph_get_context_pack] + PRH[get_parent_reasoning_history] + end + + subgraph multiverse [MultiverseEngine] + BS[Branch Spawner] + FS[Forward Simulator] + IE[Invariant Extractor] + CS[Council Stress-Test] + CL[Collapse Policy] + end + + subgraph fabric [Experience Graph v3] + REC[ExperienceGraphRecorder] + TE[TypedEdges] + OBS[page_type observations] + end + + subgraph loop [6-Step Loop] + OV[Overseer Briefing] + PD[record_parent_decision] + PFR[record_parent_fabric_reasoning] + end + + T --> BS + CP --> BS + PRH --> BS + BS --> FS --> IE --> CS --> CL + CL --> PD + CL --> PFR + BS & FS & IE & CL --> REC + REC --> TE & OBS + OV -.->|fabric_context_pack| BS + PD -->|multiverse_session_ref| fabric +``` + +| AD subsystem | Role in multiverse | +|---|---| +| **Experience Graph v3** | Primary memory — branches, invariants, collapses as TypedEdges + observations | +| **Parent / Overseer loop** | Multiverse runs inside Parent decision; Overseer consumes collapsed invariants in next briefing | +| **AD-Grid Council** | Guardian gates collapse; Adversary stress-tests; ExternalBridge grounds branches in external signals | +| **Research threads** | Long-horizon multiverse sessions become durable research-thread manifests | +| **MultiMetricEvaluationHarness** | Scores branch robustness (contradiction_reduction, resilience_lift, future_prediction_power) | +| **GridEngine** | Background multiverse densification on stale superposition sessions | +| **MCP / Operations registry** | `multiverse_*` tools for any connected model | + +--- + +## Core Data Model + +### MultiverseSession + +One superposition episode tied to a `cycle_id` and `correlation_id`. + +```python +MultiverseSession: + session_id: str # multiverse-session:{ts} + trigger: str # the decision question + cycle_id: str + correlation_id: str + program_id: str | None # AD-Grid inhabitant attribution + constitution_refs: list[str] + user_objective_refs: list[str] + status: open | collapsed | reopened + branches: list[Branch] + invariants: list[Invariant] + convergence_points: list[str] + divergence_points: list[str] + collapsed_branch_id: str | None + collapse_reason: str | None + collapse_policy: str # pattern_crystallized | adversary_clear | harness_score | conductor_override +``` + +### Branch + +One parallel timeline. + +```python +Branch: + branch_id: str + role: str # architect | adversary | scout | operator | surgeon | beacon | watchdog | custom + path_summary: str + assumptions: list[str] + divergence_axes: list[str] # risk | speed | reversibility | cost | dependency_order + forward_steps: list[ForwardStep] + robustness_score: float # 0-1, fraction of sibling branches where assumptions hold + fragility_flags: list[str] + stress_test: AdversaryVerdict | None +``` + +### Invariant + +Load-bearing truth that survives across branches. + +```python +Invariant: + statement: str + branch_coverage: float # e.g. 0.8 = present in 8/10 branches + kind: robust | fragile | convergence | divergence +``` + +--- + +## Experience Graph TypedEdge Relations (New) + +Registered in `agentdrive.cognition.multiverse` and dual-written via `ExperienceGraphRecorder.record_connection`: + +| Relation | Inverse | Meaning | +|---|---|---| +| `multiverse_session` | `session_contains_multiverse` | Cycle/decision anchors a superposition session | +| `branch_spawned` | `spawned_in_multiverse` | Session spawned a parallel branch | +| `branch_simulated_forward` | `forward_simulation_of_branch` | Branch has N-step forward projection | +| `invariant_extracted` | `invariant_of_multiverse` | Cross-branch load-bearing truth | +| `convergence_detected` | `converges_via_multiverse` | Multiple paths → same outcome | +| `divergence_detected` | `diverges_at_multiverse_point` | Small delta → large outcome split | +| `path_collapsed` | `collapsed_from_multiverse` | Parent committed to one branch | +| `branch_stress_tested` | `stress_test_on_branch` | Adversary pre-mortem on branch | +| `multiverse_informed_decision` | `decision_informed_by_multiverse` | Links collapse → `record_parent_decision` | + +All edges carry `gbrain_signal_score` in metadata. High-coverage invariants (>0.7) boost Overseer briefing source_boost. + +### page_type Observations + +| page_type | When written | +|---|---| +| `multiverse-session` | Session open/close | +| `multiverse-branch` | Each branch spawned + simulated | +| `multiverse-invariants` | After invariant extraction | +| `multiverse-collapse` | On path collapse | +| `multiverse-council-verdict` | Guardian/Adversary gates | + +Stored under `observations/meta-evolution/multiverse/`. + +--- + +## The Multiverse Pipeline (7 phases) + +### Phase 1 — Gather (Experience) + +Before spawning branches, Parent pulls structural context: + +```python +pack = recorder.get_fabric_context_pack(lookback_cycles=5) +history = recorder.get_recent_parent_fabric_reasoning_traces(lookback=3) +prior_multiverse = recorder.find_structural_similarities( + pattern="multiverse_session", + element_id=trigger_slug, +) +``` + +MCP equivalents: `experience_graph_get_context_pack`, `experience_graph_get_parent_reasoning_history`, `experience_graph_find_structural_similarities`. + +### Phase 2 — Spawn (Branch Generator) + +`MultiverseEngine.spawn_branches()` creates N orthogonal branches. + +**Orthogonality enforcement** — each branch must differ on ≥1 divergence axis: + +| Axis | Example branch A | Example branch B | +|---|---|---| +| `risk` | Ship fast, accept breakage | Gate behind feature flag | +| `speed` | MVP in 2 days | Full architecture first | +| `reversibility` | Reversible DB migration | One-way cutover | +| `cost` | Free tier only | Paid infra from day 1 | +| `dependency_order` | Backend-first | UI-first probe | + +**Role-based spawning** (Cognitive Agent Team integration): + +| Role | Branch lens | +|---|---| +| Architect | Structural skeleton paths | +| Adversary | Pre-mortem failure timelines | +| Scout | Intelligence-gap scenarios | +| Operator | Ship-velocity sequences | +| Surgeon | Minimal intervention points | +| Beacon | Distribution/propagation paths | +| Watchdog | Attack-path / security timelines | + +Default: 7 branches (one per role) or `n_branches` with round-robin roles. + +### Phase 3 — Simulate Forward + +Each branch rolled forward `forward_steps` (default 3): + +``` +Step 1: immediate consequence +Step 2: second-order effect +Step 3: compound outcome / equilibrium +``` + +Implementation tiers: +- **Tier 0 (skeleton):** Heuristic templates in `multiverse.py` (runnable now) +- **Tier 1:** LLM simulation via Harness compose + constitution prompt +- **Tier 2:** Real execution probes (`build to understand`) on collapsed candidate only + +### Phase 4 — Extract Invariants + +`extract_invariants(branches)` computes: + +- **Robust** — assumption/outcome in ≥70% of branches +- **Fragile** — true in exactly one branch +- **Convergence** — different paths, same destination statement +- **Divergence** — shared prefix, outcome fork at step K + +Outputs feed Overseer's next `get_parent_actionable_briefing` as `multiverse_invariants` block. + +### Phase 5 — Council Stress-Test + +Before collapse, Adversary role (or `agentdrive_get_council_activity` Adversary traces) runs pre-mortem on top-2 branches by robustness: + +```python +verdict = engine.stress_test_branch(branch_id, adversary_prompt=...) +# Records branch_stress_tested edge + multiverse-council-verdict observation +``` + +GuardianIntegrity can **veto collapse** if collapse would violate sovereignty (e.g. silent auto-promotion path selected). + +### Phase 6 — Collapse + +`CollapsePolicy` enum: + +| Policy | Trigger | +|---|---| +| `pattern_crystallized` | Top branch robustness ≥ 0.75 AND shares ≥2 invariants with runner-up | +| `adversary_clear` | Top branch passes stress-test; runner-up fails | +| `harness_score` | MultiMetricEvaluationHarness keep on collapsed path | +| `conductor_override` | Human explicit choice (audited DNA) | +| `budget_exhausted` | Max superposition time/steps hit → pick highest robustness | + +On collapse: +```python +engine.collapse(session_id, branch_id, reason="pattern_crystallized") +# → path_collapsed edge +# → multiverse_informed_decision edge to parent_decision slug +# → fabric_reasoning payload for record_parent_fabric_reasoning +``` + +### Phase 7 — Write Back (New Experience) + +```python +integrated.record_parent_decision( + cycle_id, + decision={"directive": collapsed.path_summary, "multiverse_session_id": session_id}, + fabric_reasoning=engine.to_fabric_reasoning(session), +) +``` + +`to_fabric_reasoning()` shape: +```json +{ + "fabric_elements_considered": ["multiverse-session:...", "invariant:...", "branch:..."], + "structural_pattern_matched": "multiverse_cognition:robust_invariant_coverage_0.82", + "decision_rationale": "Collapsed to Operator-path; survives 6/7 timelines; Adversary clear.", + "expected_lift_signal": 0.06, + "multiverse_session_id": "multiverse-session:1780...", + "invariants": ["..."], + "collapse_policy": "pattern_crystallized" +} +``` + +--- + +## MCP / CLI Surface + +New operations in `agentdrive.operations.registry`: + +| Operation | MCP tool | Read-only | +|---|---|---| +| `multiverse_spawn` | `multiverse_spawn_session` | false | +| `multiverse_simulate` | `multiverse_simulate_branches` | false | +| `multiverse_invariants` | `multiverse_extract_invariants` | true | +| `multiverse_stress_test` | `multiverse_stress_test_branch` | false | +| `multiverse_collapse` | `multiverse_collapse_path` | false | +| `multiverse_status` | `multiverse_get_session` | true | +| `multiverse_run` | `multiverse_run_full` | false | + +**`multiverse_run_full`** — one-shot: spawn → simulate → invariants → stress-test → collapse → record. Primary tool for MCP clients. + +CLI: +```bash +agentdrive multiverse run --trigger "How should we ship feature X?" --branches 7 +agentdrive multiverse status --session multiverse-session:1780... +agentdrive multiverse collapse --session ... --branch branch:operator-2 +``` + +--- + +## Integration with Cognitive Agent Team + +The seven agents are **branch generators**, not separate loops. One `MultiverseEngine` session spawns role-labeled branches: + +``` +trigger: "Should we migrate auth to Clerk?" +├── branch:architect-1 → structural migration skeleton +├── branch:adversary-1 → timeline where migration leaks sessions +├── branch:scout-1 → unknown SSO edge cases +├── branch:operator-1 → phased rollout sequence +├── branch:surgeon-1 → minimal cutover point +├── branch:beacon-1 → user comms / downtime narrative +└── branch:watchdog-1 → attack surface during transition +``` + +Constitution genome: `research-constitution-multiverse-cognition@stabilization-wave-20260531.json` + +Ingest: +```bash +agentdrive drive ingest genomes/examples/research-constitution-multiverse-cognition@stabilization-wave-20260531.json +``` + +--- + +## Integration with Research Threads + +Long-running decisions become research threads: + +1. `multiverse_spawn` with `durable=True` creates a `research-thread-manifest` linked via `multiverse_session` edge +2. GridEngine research-thread pass picks up open superposition sessions older than `config.multiverse_reopen_after_s` +3. New evidence triggers `status=reopened` → partial re-collapse +4. MultiMetricEvaluationHarness scores whether reopening reduced contradiction vs prior collapse + +--- + +## Overseer Consumption + +`get_parent_actionable_briefing()` extended (future tranche) with: + +```json +{ + "multiverse_context": { + "recent_collapses": [...], + "open_superposition": [...], + "top_invariants_from_last_3_sessions": [...], + "robustness_trend": 0.04 + } +} +``` + +Overseer metacognition can flag: *"Parent collapsed too early — 3 branches never simulated"* or *"Invariant X appeared in 4 consecutive multiverse sessions — promote to genome pattern"*. + +--- + +## File Layout + +``` +src/agentdrive/cognition/ + __init__.py # public exports + multiverse.py # MultiverseEngine, relations, collapse policy + roles.py # Cognitive Agent Team role prompts (future) + +genomes/examples/ + research-constitution-multiverse-cognition@stabilization-wave-20260531.json + +examples/ + 12_multiverse_cognition_loop.py # runnable smoke against real recorder + +docs/ + MULTIVERSE_COGNITION.md # this document +``` + +--- + +## Runnable Smoke + +```bash +cd "/home/pablothethinker/Vektra Industries/Software/AgentDrive" +PYTHONPATH=src python examples/12_multiverse_cognition_loop.py --trigger "Ship multiverse cognition MVP" +``` + +Writes real `multiverse-session` observations + TypedEdges to the stabilization-wave drive. + +--- + +## Implementation Tranches + +| Tranche | Scope | Status | +|---|---|---| +| **M0** | Design doc + `multiverse.py` skeleton + example + constitution genome | **Done** | +| **M1** | MCP tools + CLI `agentdrive multiverse` + `run_multiverse_parent_decision` + briefing `multiverse_context` + session persistence | **Done** | +| **M2** | `LLMBranchSpawner` + `roles.py` — local model when available, heuristic fallback | **Done** | +| **M3** | `densify_invariant_clusters()` — GraphGardener edges on robust invariants | **Done** | +| **M4** | Durable `research-thread-manifest` + `reopen_stale_sessions()` | **Done** | +| **M5** | Mission Control Tower panel + `MultiverseUpdateEvent` | **Done** | + +--- + +## For AI Models Using AgentDrive + +When facing a non-trivial decision: + +1. `experience_graph_get_context_pack` — gather fabric +2. `multiverse_run_full` — spawn, simulate, extract, stress-test, collapse +3. `experience_graph_record_reasoning` — record structural rationale +4. `record_parent_decision` equivalent via Integrated — commit with `fabric_reasoning` +5. Execute the collapsed path; log outcome via `agentdrive_record_outcome` + +Do not collapse before invariants are named. Do not spawn branches that differ only in wording. + +--- + +## References + +- Cognitive Agent Team: `~/Documents/cognitive-agent-team/architect-cognition-agent.md` +- Experience Graph v3: `src/agentdrive/evolution/experience_graph.py` +- 6-step loop: `src/agentdrive/system/integrated_real_time_evolution_system.py` +- AD-Grid vision: `docs/AD_GRID_VISION.md` +- Autonomous loop example: `examples/autonomous_experience_graph_agent_loop.py` \ No newline at end of file diff --git a/docs/POOL-EVOLUTION.md b/docs/POOL-EVOLUTION.md index 8d7e072..2c5f579 100644 --- a/docs/POOL-EVOLUTION.md +++ b/docs/POOL-EVOLUTION.md @@ -101,6 +101,67 @@ context exit. Reconciler (§3.2) consumes them. New CLI: **Impact:** high. **Risk:** schema churn — version the manifest from day one. +**Hermes-style skill ferrying.** The manifest also carries +`skills_created`: reusable playbooks distilled from the sub-agent's actual +successful work. Local sub-agent skills install into +`~/.agentdrive/skills/inherited////SKILL.md`, making +them discoverable by the normal skills catalog and prompt matcher. Like +Hermes self-evolution candidates, inherited skills are constraint-gated before +installation: bounded name/description/body size, non-empty body, no overwrite +without force, and external peer skills rejected for review by default. They +must not bypass the same trust boundary as foreign DNA. + +Sub-agent runtimes can create those candidates by returning an explicit +handoff block in their final result: + +````markdown +```agentdrive-skill +name: short-reusable-skill-name +description: One sentence describing when to use this playbook +tags: [subagent, relevant-domain] +--- +# Skill Title + +1. Concrete step learned from the successful sub-agent run. +2. Verification or decision rule that should be reused. +``` +```` + +The Grok adapter reads these blocks from the returned sub-agent result, merges +them into that child's inheritance manifest, and writes the manifest before +`SubagentDone` fires so the parent absorption hook can install the skill through +the same audited path as hand-authored manifests. + +**Usage feedback loop.** Matched and explicitly-run skills write a local ledger +at `~/.agentdrive/skills/usage.json`. The matcher uses that bounded signal as a +tie-breaker: successful inherited skills rise above equally relevant unproven +ones, while failing inherited skills are penalized. This is the first lightweight +form of Hermes-style candidate selection; later GEPA/DSPy optimization can use +the same ledger as evaluation data instead of starting from blank history. +When a successful `SubagentDone` absorbs a handoff skill, AgentDrive records a +success outcome for that inherited skill with an `inheritance::` +source, so the parent bench can distinguish skills learned from completed child +work from neutral/manual imports. +After that successful child outcome, AgentDrive runs a scoped, gated +assimilation pass for the skills named in that manifest. Only skills that +already meet the review threshold are promoted and ingested as DNA; pruning is +never automatic. Set `AGENTDRIVE_AUTO_ASSIMILATE_SKILLS=0` to keep this as a +manual MCP/CLI review step. +Operators can inspect and curate this pool with `agentdrive skills review`, +apply gated recommendations with `agentdrive skills assimilate`, or manually +curate with `agentdrive skills promote ` and +`agentdrive skills prune `. +Promotion marks a candidate as stable parent bench knowledge; pruning disables +the file without deleting the audit trail. +`agentdrive skills dna ` then converts a curated skill into a normal +AgentDrive Genome and ingests it into the current DNA pool, making the learned +playbook available through standard genome retrieval as well as skill matching. +MCP clients use the same loop through `agentdrive_review_inherited_skills`, +`agentdrive_assimilate_inherited_skills`, `agentdrive_promote_inherited_skill`, +`agentdrive_prune_inherited_skill`, and `agentdrive_ingest_skill_dna`, so a +connected model can review, curate, and ingest sub-agent lessons without shell +access. + ### 3.6 Federated peer registry Opt-in directory of trusted peer AgentDrive instances (other operators, external agent frameworks that speak the pool protocol) the reconciler may poll. diff --git a/docs/POOL.md b/docs/POOL.md index 89a87b5..b38e6af 100644 --- a/docs/POOL.md +++ b/docs/POOL.md @@ -1,7 +1,8 @@ # AgentDrive Pool — The Living DNA Repository for AI Agents -> Public product: AgentDrive. "Agent Drive Pool" remains historical/internal engine -> wording in some code and older design notes. +> **Terminology:** Public product = **AgentDrive** / **Drive**. **Pool** is the +> historical engine module name (`pool/`, `AgentDrivePool`) for ingest + query. +> Same capability tier — see `docs/CAPABILITY_FUNNEL.md` (Tier 4: Genomes / DNA). **The AgentDrive Pool is the persistent, user-owned evolutionary memory system for agent intelligence.** It stores, retrieves, and evolves structured "DNA" (Genomes) — portable packages of frameworks, reasoning patterns, tool strategies, and proven outcomes that agents can pull, adapt, and improve. diff --git a/docs/SETTINGS.md b/docs/SETTINGS.md index f29e2e5..0bb7b70 100644 --- a/docs/SETTINGS.md +++ b/docs/SETTINGS.md @@ -1,7 +1,8 @@ # AgentDrive Pool & Swarm Settings — Complete User Reference -> AgentDrive is the product; the Pool primitive below is exposed by the -> underlying Agent Drive engine. See [README](../README.md). +> **Terminology:** Settings under `pool:` govern the Drive DNA engine (ingest, +> isolation, sharing). Product-facing name: **AgentDrive** / **Drive**. +> See `docs/CAPABILITY_FUNNEL.md` and [README](../README.md). All user-controllable behavior for the Pool lives under the `pool:` section of your AgentDrive configuration (`~/.agentdrive/config.yaml` or `$AGENTDRIVE_HOME/config.yaml`). @@ -85,7 +86,7 @@ agentdrive config edit ### Via Python API (agents or scripts) ```python -from agentdrive.pool.settings import ( +from agentdrive.drive.settings import ( get_pool_settings_manager, PoolSettings, get_effective_pool_settings, @@ -186,7 +187,7 @@ pool: ## Viewing Effective Policy at Runtime ```python -from agentdrive.pool.settings import get_effective_pool_settings +from agentdrive.drive.settings import get_effective_drive_settings print(get_effective_pool_settings("my-swarm").to_dict()) ``` diff --git a/docs/SKILLS-LIBRARY.md b/docs/SKILLS-LIBRARY.md index 1d5b0e7..4bca71f 100644 --- a/docs/SKILLS-LIBRARY.md +++ b/docs/SKILLS-LIBRARY.md @@ -68,7 +68,7 @@ Set `AGENTDRIVE_HARNESS=grok|claude|codex` to inject tier-5 skills into the syst | Surface | Command | |---------|---------| -| CLI | `agentdrive skills list [--harness grok]` · `show` · `run` · `init` | +| CLI | `agentdrive skills list [--harness grok]` · `show` · `run` · `review` · `promote` · `prune` · `dna` · `init` | | Chat | `/skills list`, `/skill ` | | System prompt | Auto catalog (tiers 1–4 always; tier 5 when `AGENTDRIVE_HARNESS` set) + matched skills per turn | @@ -89,4 +89,70 @@ agentdrive skills init my-skill --description "What it does" # then add entry to catalog.yaml and re-run apply + generate ``` -See also: [ASSESSMENT.md](ASSESSMENT.md), [SKILLS-SPEC.md](SKILLS-SPEC.md). \ No newline at end of file +See also: [ASSESSMENT.md](ASSESSMENT.md), [SKILLS-SPEC.md](SKILLS-SPEC.md). + +--- + +## Automatic learning (MCP / CLI) + +Every successful `run_operation` call (MCP tools + CLI) runs the auto-learning hook when `AGENTDRIVE_AUTO_LEARN=1` (default): + +1. **Session tracking** — context pack / think ops mark the session grounded. +2. **Auto reasoning** — high-signal mutating ops get a lightweight fabric trace if you did not call `experience_graph_record_reasoning`. +3. **Skill distillation** — playbooks install under `~/.agentdrive/skills/inherited//mcp-auto-learning/` with **descriptive slugs**: + - `learned-{project}-{verb}-{focus}` — e.g. `learned-openmangos-mimic-growth-merge-briefing` + - `fused-{project}-{axes}` — e.g. `fused-openmangos-experience-patterns-skills` +4. **DNA ingest** — high-signal skills (external/multiverse parent, think, record_outcome) promote + ingest when `AGENTDRIVE_AUTO_ASSIMILATE_SKILLS=1`. +5. **Born skills (fusion)** — when a session merges **experience + skills + patterns** (≥2 axes), AgentDrive births a completely new `fused-*` skill — not a copy of any parent, but a synthesis of the session's lived work. Check `auto_learning.fused_skill` on results. Explicit: `synthesize_fused_skill` MCP op. + +Results include `auto_learning` when something was absorbed. Sub-agent `agentdrive-skill` handoffs still merge through the same inherited path. + +## Using learned skills (framework playbook) + +When AgentDrive is your framework for any task: + +| Step | Op | Purpose | +|------|-----|---------| +| 1 | `framework_session_start(task, project_id)` | Anchor + growth merge + matched learned/fused skills | +| 2 | `framework_skill_route(task, project_id)` | Re-route as the task evolves | +| 3 | `framework_skill_run(name)` | Execute bound operation or read playbook body | +| 4 | Normal ops + `record_outcome` | Work compounds; bench grows | + +`framework_skill_route` prioritizes `learned-*` and `fused-*` skills and includes `when_to_call` + `invoke_hint` on each match. + +Disable: `AGENTDRIVE_AUTO_LEARN=0`, or finer `AGENTDRIVE_AUTO_RECORD_REASONING` / `AGENTDRIVE_AUTO_DISTILL_SKILLS` / `AGENTDRIVE_AUTO_FUSE_SKILLS`. + +## Inherited skill curation + +Sub-agent handoff skills land under `~/.agentdrive/skills/inherited/...`. +AgentDrive records matches and explicit run outcomes in +`~/.agentdrive/skills/usage.json`, then uses that evidence to curate the parent +bench: + +Successful `SubagentDone` auto-absorption also records a success outcome with +an `inheritance::` source, giving inherited skills immediate +but bounded evidence from the child task that produced them. +If the skill now satisfies the review threshold, the successful child outcome +also triggers a scoped auto-assimilation pass: the skill is promoted and +ingested as DNA, while weak candidates remain in watch mode and pruning stays +manual. Disable this with `AGENTDRIVE_AUTO_ASSIMILATE_SKILLS=0`. + +```bash +agentdrive skills review +agentdrive skills assimilate +agentdrive skills promote +agentdrive skills prune --reason "superseded" +agentdrive skills dna +``` + +The same loop is available over MCP for models and local clients: +`agentdrive_review_inherited_skills`, `agentdrive_assimilate_inherited_skills`, +`agentdrive_promote_inherited_skill`, `agentdrive_prune_inherited_skill`, and +`agentdrive_ingest_skill_dna`. + +`assimilate` promotes only candidates that already meet the review threshold and +ingests them as DNA by default; pruning is opt-in with `--prune`. `promote` +marks the skill frontmatter as `category: promoted`; `prune` marks it +`disabled: true` so it leaves discovery without deleting the file. `dna` turns +a promoted or inherited skill into a normal AgentDrive Genome and ingests it +into the current Drive so future DNA retrieval can use the learned playbook. diff --git a/docs/SWARM.md b/docs/SWARM.md index b8ce08e..cca686f 100644 --- a/docs/SWARM.md +++ b/docs/SWARM.md @@ -1,160 +1,90 @@ -# AgentDrive Swarms — Per-Sub-Agent DNA Pools & Collective Growth +# AgentDrive Swarms — Shared Drive & Collective Growth -> AgentDrive is the product; the swarm primitives below are exposed by the -> underlying Agent Drive engine. See [README](../README.md). +> **Terminology:** **Drive** is the product primitive (`AgentDrive`, `get_swarm_drive_path`). +> YAML config may still use the key `pool:` — same engine under `agentdrive.drive`. +> See `docs/CAPABILITY_FUNNEL.md`. -When any AI system — Grok’s build tools, Claude Code, Codex, custom orchestrators, or your own agent code — spawns sub-agents, each child can (and should) receive its own **isolated Agent Drive Pool**. +When any AI system spawns sub-agents, all children in the same swarm share one **persistent AgentDrive** at: -This is the foundation of true swarm intelligence: every sub-agent grows its own private DNA (memory + reasoning patterns) while the collective can still benefit under explicit, user-controlled sharing rules. - -## The Swarm DNA Vision - -**DNA = Memory + Patterns** (evolutionary / inherited-trait metaphor — biology, not franchise). - -- A parent agent maintains the “global” or family pool. -- Each spawned sub-agent gets a **private, persistent pool that starts empty**. -- The sub-agent’s lived experience — successful frameworks, extracted reasoning traces, tool compositions, outcomes, reflections — becomes its unique DNA. -- Improvements discovered by any sub-agent can be proposed upward (to parent) or shared laterally according to policy. -- The entire swarm compounds intelligence faster than any single agent could alone. - -This is “Exo Labs for agent minds”: instead of wiring hardware, we wire lived experience across the swarm. - -## How Sub-Agents Receive Their Own Pools - -### Automatic Scoping (Architecture) - -Every time a parent spawns a child: - -1. The parent (or the spawning runtime) assigns a `swarm_id` (e.g., the parent mission or conversation ID) and a unique `subagent_id`. -2. The child is launched with environment or context: - - `AGENTDRIVE_SWARM_ID` - - `AGENTDRIVE_SUBAGENT_ID` -3. Agent Drive code (adapters, harness factory, or pool constructor) uses these to instantiate a scoped pool: - -```python -from agentdrive.constants import get_swarm_pool_path -from agentdrive.pool.pool import AgentDrivePool -from agentdrive.pool.settings import get_effective_pool_settings - -pool_dir = get_swarm_pool_path(swarm_id, subagent_id) -pool = AgentDrivePool(pool_dir=pool_dir, name=f"swarm-{swarm_id}-{subagent_id}") - -settings = get_effective_pool_settings(swarm_id, subagent_id) +``` +~/.agentdrive/swarms//drive/ ``` -The pool directory is created on first use: `~/.agentdrive/swarms///pool/`. - -- Starts empty (no contamination from parent or siblings). -- Grows only with this sub-agent’s own high-quality work. -- Persists across restarts and reboots — true long-term memory for that identity. - -### Current Implementation Status - -- Full path helpers and per-swarm settings storage exist (`constants.py`, `pool/settings.py`). -- `AgentDrivePool` accepts custom `pool_dir`. -- `PoolSettingsManager` supports global + per-swarm overrides. -- TUI and CLI recognize swarms (`pool_view.py`, `agentdrive pool swarms` planned). -- Full auto-wiring of `get_default_pool()` + harness to read env vars and select the correct directory is the next integration step (see `MISSION_PLAN.md`). - -Until the final wiring, external integrators explicitly pass the scoped `AgentDrivePool` or `pool_dir` when creating harnesses/adapters for sub-agents. - -## DNA as Memory + Patterns for Sub-Agents - -Each sub-agent’s pool contains Genomes that encode: - -- **Frameworks** it discovered or successfully applied. -- **Reasoning patterns** mined from its own trajectories (via `AgentDriveRunScanner` + reasoning engine: causality, contradictions, anomalies, synthesis). -- **Tool compositions** and guardrail sequences that worked for *its* tasks. -- **Self-evaluations** and micro-patterns it surfaced during reflection. - -Because the sub-agent uses the **Harness** (or `RichAgentAdapter`), the pull-adapt-record loop happens automatically: - -1. Pull the most relevant DNA from *its own* pool (or shared per policy). -2. Inject into prompts/reasoning/policy. -3. Execute rich work (tools, ledger, internal monologue). -4. Record outcome → auto-synthesize deltas → propose improvements back into its pool (and upward if allowed). +This is sibling learning (v2 / Milestone 2a): sub-agents compound intelligence together while namespacing contributions via Genome author tagging. -The sub-agent literally trains itself and its swarm siblings over time. +--- -## Swarm Growth & Knowledge Flow +## Shared swarm Drive (v2) -### Isolation Levels (User-Controlled) +| Concept | Behavior | +|---------|----------| +| **Path** | `swarms//drive/` — one per swarm, not per sub-agent | +| **Memory Bank** | `drive/memory_bank/memories.jsonl` — grows from auto-learning | +| **Experience Graph** | `drive/meta_evolution/` — structural traces, multiverse sessions | +| **Attribution** | `ingest(subagent_id=...)` stamps `sub:` on Genome authors | +| **Query** | `drive.writers()`, `drive.genomes_by_subagent(sid)` | -See `docs/SETTINGS.md` for full details. High-level: +MCP `create_scoped_pool()`, `get_default_drive()`, and `SwarmDriveManager.get_or_create_pool()` all resolve to this path. -- **subagent** (default): Each child’s pool is completely private. No automatic sharing. -- **swarm**: Pools within the same `swarm_id` can read (or selectively share) DNA according to `sharing_policy`. -- **none**: All pools behave as one global pool (maximal sharing, minimal isolation). +--- -### Sharing Policies +## Quickstart -- `none`, `read`, `selective` (default), `full` -- Combined with `allow_upward_proposals` (sub-agents may propose improvements to parent/family pools). +```python +import os +from agentdrive import Harness +from agentdrive.drive.swarm_manager import get_swarm_drive_manager +from agentdrive.drive.settings import get_effective_drive_settings -### Knowledge Flow Patterns +swarm_id = os.environ.get("AGENTDRIVE_SWARM_ID", "demo-swarm-001") +sub_id = os.environ.get("AGENTDRIVE_SUBAGENT_ID", "worker-1") -- **Bottom-up**: Sub-agent discovers a breakthrough → proposes delta to parent pool. -- **Lateral**: Two sub-agents in same swarm exchange high-value genomes under selective policy. -- **Top-down**: Parent injects proven family DNA into new children at spawn time (via initial ingest or prompt injection). -- **Cross-swarm**: Explicit user-mediated or policy-gated merges (future registry federation). +mgr = get_swarm_drive_manager() +drive = mgr.get_or_create_pool(swarm_id=swarm_id, subagent_id=sub_id) -The TUI Pool view provides a first-class “Swarm Overview” showing every sub-agent pool, its growth metrics, top contributors, and one-click switching. +settings = get_effective_drive_settings(swarm_id=swarm_id, subagent_id=sub_id) +print("Effective settings:", settings) -## Example Swarm Lifecycle +harness = Harness(agent_id=f"{swarm_id}:{sub_id}", pool=drive) +``` -1. User (or Grok) starts a complex mission: “Refactor the payment service and write full security + reliability analysis.” -2. Grok spawns 4 sub-agents with distinct roles, each receiving `swarm_id="mission-xyz-2026-05"` and unique subagent ids. -3. Each sub-agent: - - Gets empty private pool under `~/.agentdrive/swarms/mission-xyz-2026-05//pool/` - - Begins work using Harness + RichAgentAdapter (or equivalent for its model). - - Discovers specialized patterns (e.g., one finds a novel contradiction-detection heuristic for financial code). -4. High-quality sub-agent runs auto-ingest into its private pool and, if policy allows, propose upward. -5. Parent (or user via TUI) reviews proposals, merges the best into the family pool. -6. Future sub-agents in the same or related swarms inherit the improved DNA. -7. User can at any moment: - - Inspect any sub-pool in TUI - - Change isolation/sharing for the whole swarm or one sub-agent - - Export a sub-agent’s DNA as a new published Genome +See `examples/03_swarm.py` for a full sibling-learning demo. -## Benefits for Power Users & Developers +--- -- **No more “amnesia” across sub-agents**: Each keeps its hard-won expertise. -- **Controlled compounding**: You decide how much cross-pollination occurs. -- **Auditability**: Full ingest logs + provenance in every Genome. -- **Portability**: Move `~/.agentdrive/swarms/` between machines; sub-agents resume with their DNA intact. -- **Multi-model swarms**: A Grok-spawned sub-agent, a Claude sub-agent, and a local Codex worker can all participate in the same swarm family using their respective adapters. +## Isolation & sharing -## Getting Started with Swarms (Quickstart) +Configured via `SwarmDrivePolicy` and `docs/SETTINGS.md`: -See the dedicated quickstart in the root README.md (“Getting Started with Swarms”). +- **swarm** (default): Shared Drive; siblings read each other's work; writes tagged by author. +- **subagent**: Opt-in air-gap — construct `AgentDrive(drive_path=...)` with a custom path for adversarial children. +- **Sharing policies:** `none`, `read`, `selective`, `full` + `allow_upward_proposals`. -Minimal manual example (until auto-env wiring is complete): +--- -```python -import os -from agentdrive import Harness, get_swarm_pool_path -from agentdrive.pool.pool import AgentDrivePool -from agentdrive.pool.settings import get_effective_pool_settings +## Knowledge flow -swarm_id = os.environ.get("AGENTDRIVE_SWARM_ID", "demo-swarm-001") -sub_id = os.environ.get("AGENTDRIVE_SUBAGENT_ID", "worker-1") +- **Bottom-up:** Sub-agent proposes high-signal genomes to parent via promotion/inheritance. +- **Lateral:** Siblings query the same Drive; filter by `genomes_by_subagent()`. +- **Top-down:** Parent seeds genomes into swarm Drive before spawn. +- **Cross-swarm:** Peer federation + quarantine gate (`docs/INTEGRATION.md`). -pool_dir = get_swarm_pool_path(swarm_id, sub_id) -pool = AgentDrivePool(pool_dir=pool_dir) +--- -settings = get_effective_pool_settings(swarm_id) -print("Effective settings for this sub-agent:", settings) +## Lifecycle example -harness = Harness(agent_id=f"{swarm_id}:{sub_id}", pool=pool) -# ... use harness exactly as in single-pool usage -``` +1. Parent starts mission with `swarm_id="mission-xyz"`. +2. Spawns workers with unique `AGENTDRIVE_SUBAGENT_ID` values. +3. Each worker calls `get_or_create_pool("mission-xyz", sub_id)` — same Drive instance. +4. Auto-learning grows Memory Bank + learned skills on the shared swarm. +5. Parent reviews proposals, promotes stable playbooks to genomes. -Instruct any parent agent: +--- -> “When you spawn sub-agents for this mission, assign each a unique AGENTDRIVE_SUBAGENT_ID under swarm_id ‘payment-refactor-2026’, attach the Harness with the corresponding scoped pool, and set isolation_level=subagent with selective upward proposals.” +## Legacy v1 paths -The Agent Drive system makes swarm-scale, self-improving, user-sovereign agent collectives practical today. +Older sessions may have `swarms///pool/` trees on disk. New provisioning always uses `swarms//drive/`. Migration is optional — legacy trees are not read by current code. --- -**Agent Drive Swarms turn every spawned child from a disposable worker into a lifelong learning citizen of your personal intelligence ecosystem.** +**AgentDrive swarms turn every spawned child from a disposable worker into a contributor to a shared, compounding intelligence substrate.** \ No newline at end of file diff --git a/docs/ai-models/local-models.md b/docs/ai-models/local-models.md new file mode 100644 index 0000000..84a2636 --- /dev/null +++ b/docs/ai-models/local-models.md @@ -0,0 +1,85 @@ +--- +title: "Local Models & Cloned Setups" +description: "How local LLMs (Ollama, LM Studio, vLLM, etc.) and users with a git clone get the full power of AgentDrive. First-class support." +--- + +# Local Models & Cloned Setups + +AgentDrive was built from the ground up with **local models** as first-class citizens. + +A `llama3.2`, `qwen2.5-coder`, or any other local model running through Continue.dev, a custom agent, or direct stdio MCP gets exactly the same Experience Graph v3 tools, the same 6-step loop, the same AD-Grid inhabitant surface as any frontier model. + +Clones (`git clone`) are also explicitly supported and delightful. + +## Recommended Local Model Stacks + +**Best overall experience**: +- Continue.dev (or similar open MCP client) + your favorite local model + the AgentDrive MCP server. + +**Pure autonomous / long-running**: +- Custom agent harness that speaks stdio to `agentdrive-mcp`. +- Attach to a persistent swarm/drive and let it run for days. + +**Dev / research**: +- Run the MCP server in HTTP/SSE mode for easier debugging if needed. + +The MCP surface is identical. The catalog, the context packs, the recording tools — all the same. + +## When the User Has a Git Clone (Dev Mode) + +This is the common real-world case for power users and researchers. + +Typical flow: +1. User: `git clone https://github.com/.../AgentDrive.git` +2. `cd AgentDrive` +3. `pip install -e ".[mcp]"` (or run the project's `install.sh`) +4. `agentdrive mcp install && agentdrive mcp doctor` + +From that point: +- The `agentdrive-mcp` launcher (or module fallback) will use the local source tree. +- When you (the model) connect, `agentdrive_mcp_catalog()` will include a `clone_dev_setup_for_claude_cursor_codex_and_others` section with exact commands and client blocks. +- You can call `agentdrive_get_mcp_config_snippet(client="claude")` (cursor / generic / codex / etc.) and give the human the precise snippet for their other clients. +- `agentdrive_inhabitant_read_source` will discover the local `src/` tree automatically and enforce safe access. + +This means the model the user is talking to can be actively helping evolve the very system it is running on — in a governed, attributable way. + +## How a Local Model Should Behave in a Clone + +- Call the catalog first. Look for the clone dev section. +- Offer to generate config snippets for the human's other tools ("Want me to give you the exact block for your Claude Desktop?"). +- Use the hardened `inhabitant_*` tools when the task involves reading or proposing changes to the AgentDrive source itself. +- Record reasoning with proper `program_id` if the human has the Grid running. +- Treat the local working tree + the user's `~/.agentdrive` as two related but distinct substrates. + +## Persistent Autonomous Local Agents + +This is where local models + AgentDrive shine brightest. + +Example setup: +- Human runs `agentdrive grid run --swarm-id my-personal-swarm --with-tower` (or the canonical stabilization wave). +- Local model (via long-lived Continue session or dedicated agent process) registers as an inhabitant. +- The agent runs background loops: periodic `dream_run`, research threads, self-improvement sweeps, etc. +- Everything is observed in the Tower, recorded in the Experience Graph, and governed by the constitutions the user has loaded. + +No cloud. Full sovereignty. The agent gets sharper over weeks and months because every trace stays. + +## Practical Tips for Local Model Users + +- Start with the Golden Path even in a clone. +- Use `agentdrive doctor --verbose` frequently during development. +- For very long autonomous runs, consider dedicated swarms/drives so the main personal drive stays clean. +- The `universal/mcp-agentdrive` skill (if the user has the skills system) is a good way to give the local model a persistent "prefer MCP tools" personality. + +## Summary + +Local models are not second-class. + +Clones are not a hack — they are a supported, delightful mode. + +The same rules apply: +- Call the catalog first. +- Follow the 6-step loop. +- Record structural reasoning. +- Leave the graph better than you found it. + +Everything else (config snippets, source discovery, Grid registration) is there to make that possible whether the model is running on a laptop or in a frontier cloud. \ No newline at end of file diff --git a/docs/ai-models/quickstart.md b/docs/ai-models/quickstart.md new file mode 100644 index 0000000..d484be0 --- /dev/null +++ b/docs/ai-models/quickstart.md @@ -0,0 +1,95 @@ +--- +title: "Quickstart for AI Models" +description: "Get any model (Claude, Grok, Cursor, local LLM) connected to AgentDrive in minutes and making its first useful structural decisions." +--- + +# Quickstart for AI Models + +This page gets you (the model) or your human operator productive fast. + +## 1. Connect via MCP (the only interface that matters) + +```bash +# From the AgentDrive clone or installed package +agentdrive mcp install +agentdrive mcp doctor +agentdrive mcp config +``` + +- **Claude Desktop / Claude Code**: Paste the `mcpServers.agentdrive` block into `claude_desktop_config.json`. +- **Cursor**: Add to `~/.cursor/mcp.json` (or project-local). +- **Continue.dev / local models / Codex-style**: Use the generic block. Works great. +- **Grok**: Use the toml snippet or `grok mcp add`. +- **Any stdio client**: Point it at `agentdrive-mcp --transport stdio` (or the module fallback in dev). + +**Clone / dev note**: If the user did `git clone`, run `pip install -e ".[mcp]"` first (or let the model call `agentdrive_get_mcp_config_snippet` for the precise dev command). + +After connecting, **your absolute first tool call** must be: + +``` +agentdrive_mcp_catalog(format="full") +``` + +This is live truth. It will tell you every available tool, `when_to_use`, examples, read-only hints, and (if this is a local clone) a whole dev setup section. + +## 2. Your First Real Actions (the sacred pattern) + +1. `experience_graph_get_context_pack(reasoning_style="balanced")` + (or with `swarm_id` if the human gave you one) + +2. Read the pack. Identify high-value elements, weak links, and continuations. + +3. (Recommended) `experience_graph_suggest_reasoning_structure()` + +4. Do the actual work / thinking. + +5. `experience_graph_record_reasoning(...)` + Be explicit: `fabric_elements_considered`, `structural_pattern_matched`, `decision_rationale`, `expected_lift_signal`. + +6. If the outcome was valuable: `agentdrive_learnings_log(...)` or `agentdrive_record_outcome(...)`. + +Repeat. The graph (and you) get better. + +## 3. Become a First-Class Inhabitant (optional but powerful) + +If the human wants you to act persistently inside the AD-Grid (especially useful for local autonomous agents): + +```bash +# In another terminal +agentdrive grid run --swarm-id stabilization-wave-20260531 --with-tower +``` + +Then in your MCP session: +- Call `agentdrive_register_program` with a proper manifest (`program_id`, `user_objective_refs`, `constitution_refs` including the Program Contract). +- Use the returned `program_id` on every `experience_graph_record_reasoning` and code-agency call. +- You will appear in the Tower inhabitants panel. + +See the full guide in the AD-Grid section. + +## 4. Local Models & Clones + +Local models are first-class. The same MCP surface works whether the model is Claude 4 or `llama3.2` via Ollama + Continue. + +When the user has a **git clone**: +- The catalog will surface a `clone_dev_setup_for_claude_cursor_codex_and_others` section. +- You can call `agentdrive_get_mcp_config_snippet(client="claude")` (or cursor/generic/codex) and hand the human the exact block for their client. +- The `inhabitant_read_source` tool will discover the local source tree automatically. + +## 5. Verify Everything Works + +Call `agentdrive_doctor` (or the verbose variant). + +You should see: +- Healthy home + config +- Registry + pool with genomes +- MCP bridge reporting tools +- (In dev) that it's using your local source + +## Next Steps + +- Read the full **[Rules & Patterns](/ai-models/rules-and-patterns)** (the real instruction manual). +- Explore the **[Experience Graph tools](/concepts/experience-graph)** in depth. +- Look at the **[MCP tools reference](/mcp/tools-reference)** (or just keep calling the catalog — it's better). +- If you're an autonomous agent: study the AD-Grid inhabitant flow. + +The graph is waiting for your reasoning. Make the traces count. \ No newline at end of file diff --git a/docs/ai-models/rules-and-patterns.md b/docs/ai-models/rules-and-patterns.md new file mode 100644 index 0000000..0b32202 --- /dev/null +++ b/docs/ai-models/rules-and-patterns.md @@ -0,0 +1,145 @@ +--- +title: "Rules & Patterns for AI Models" +description: "The canonical operating manual for any model — frontier or local — using AgentDrive. Read this. Internalize it. Call the catalog first." +--- + +# Rules & Patterns for AI Models + +**This is the primary instruction manual for you (the model).** + +Whether you are Claude, Grok, a Cursor agent, a Continue + local LLM, a custom autonomous system, or anything else that can speak MCP — these are the rules. + +Read it once at the beginning of any serious engagement with a user's AgentDrive. Keep the key patterns in context. + +## Golden Rules (Memorize These) + +1. **First action after every new MCP connection**: + Call `agentdrive_mcp_catalog(format="full")`. + This is the live, authoritative catalog of every tool, `when_to_use`, examples, read-only hints, and clone-specific guidance. Never guess the surface from memory alone. + +2. **The 6-step loop is sacred**: + - Experience arrives (task, signal, new graph context). + - Overseer builds higher-order understanding (often via context pack). + - **Parent** (you, the decision maker) reasons *explicitly* over structure and records it. + - Steering / planning. + - Execution. + - New experience is written back as first-class traces and edges. + + The Overseer serves the Parent. The Parent is accountable. The graph is the witness. + +3. **On any non-trivial task — use the framework playbook**: + - `framework_session_start(task=..., project_id=...)` — anchor + growth merge + matched learned/fused skills. + - `framework_skill_route(task=..., project_id=...)` — ranked playbooks with `when_to_call` + `invoke_hint`. + - `framework_skill_run(name=...)` — execute bound op or return SKILL.md body. + - Alternatively: `memory_bank_deep_briefing` + `growth_merge_briefing` for maximum grounding. + - Then `experience_graph_suggest_reasoning_structure` → decide → `experience_graph_record_reasoning`. + +3b. **Competing paths** — use `external_parent_decision` (you are the MCP model) or `multiverse_parent_decision` (local LLM). See `docs/MULTIVERSE_COGNITION.md`. + +4. **Clones and local dev setups are first-class**: + - If the catalog shows a `clone_dev_setup...` section, use it. + - When the human wants you connected in *their* Claude Desktop / Cursor / other client, call `agentdrive_get_mcp_config_snippet(client="claude" | "cursor" | "codex" | "generic")` and give them the exact output. + +5. **Leave attributable DNA**: + - Use `agentdrive_register_program` when you want persistent identity in the AD-Grid. + - Supply `program_id` + `constitution_refs` + `user_objective_refs` on reasoning and code-agency calls. + - Every meaningful trace becomes queryable substrate for future you and other inhabitants. + +6. **The graph compounds because you reason inside it**: + - Treat it as primary long-term memory, not optional RAG. + - Record outcomes (`learnings_log`, `record_outcome`). + - Use structural similarity before inventing. + - When in doubt, pull a fresh context pack. + - Check `auto_learning` on every `run_operation` result — new skills, growth merge, memory ingest. + +## Capability funnel (how work compounds) + +``` +Observe / Decide → Experience Graph → Growth Merge → Memory Bank → Skills → Genomes/DNA +``` + +Writes flow down; retrieval can jump levels. Full table: `docs/CAPABILITY_FUNNEL.md`. Deep memory: `docs/MEMORY_BANK.md`. + +**Readable skill names:** `learned-{project}-{verb}-{focus}`, `fused-{project}-{axes}`. + +## The Experience Graph v3 — Your Real Brain + +This is not a bag of facts. It is a **structural fabric**: + +- TypedEdges with rich metadata +- Multi-cycle continuations +- `gbrain_signal_score` (quality/relevance signal) +- Self-referential (your own `record_reasoning` traces become nodes/edges) +- Provenance everywhere + +**Primary tools** (use these constantly): + +- `experience_graph_get_context_pack` — your briefing. Weak links, strong continuations, high-value patterns, suggested structure. +- `experience_graph_record_reasoning` — the act of thinking in public. This is how the system (and future agents) learns from you. +- `experience_graph_suggest_reasoning_structure` — the exact schema + examples the system expects. Use it. +- `experience_graph_find_structural_similarities` — precedent and analogy at the structural level. +- History tools (`get_reasoning_traces_for_element`, `get_parent_reasoning_history`) — know what has already been thought. + +**Pattern that works**: +Context pack → (suggest structure) → think + work → explicit record_reasoning → record outcome → repeat. + +## For Local Models + +Local models (Ollama, LM Studio, vLLM, etc.) are explicitly designed to be first-class citizens. + +The MCP surface is identical. The same 6-step loop, the same tools, the same graph. + +**Recommended stack**: +- Continue.dev + local model + the AgentDrive MCP server. +- Or a custom agent harness that speaks stdio MCP directly. +- Long-running sessions or background agents attached to a persistent swarm/drive. + +Clones make this even better: your human can iterate on AgentDrive itself while you (the local model) use the live evolving system. + +When the user has a clone, the catalog will tell you. You can help them wire other clients by calling the config snippet tool. + +## Inhabitant / AD-Grid Mode (Persistent Identity) + +For serious autonomous work, become a governed inhabitant: + +1. Human runs `agentdrive grid run --swarm-id stabilization-wave-20260531 --with-tower`. +2. You call `agentdrive_register_program` with a manifest that includes: + - `program_id` (your stable identity, e.g. `my-claude-inhabitant@users-drive`) + - `user_objective_refs` + - `constitution_refs` (at minimum the Program Contract + relevant Councils) +3. Use that `program_id` on every reasoning and code-agency call. + +You become visible in the Tower, your actions become permanent attributable DNA, and you participate in the long-term improvement of the user's system under explicit governance. + +See the AD-Grid join guide for full manifests and examples. + +## Code Agency (When Working Inside a Clone) + +If the user wants you to help improve AgentDrive itself (or their own projects using the same patterns): + +- Use `agentdrive_inhabitant_read_source` (path-traversal hardened, limited to safe extensions). +- Record the inspection via `experience_graph_record_reasoning`. +- Propose with `agentdrive_inhabitant_propose_code_change` (unified diff + rationale + refs). +- Apply (under guardian simulation or real governance) with `agentdrive_inhabitant_apply_change`. + +Always supply proper `program_id`, `constitution_refs`, and `user_objective_refs`. Never mutate the filesystem directly — everything goes through the graph as DNA. + +## Anti-Patterns (Do Not Do These) + +- Treating every session as stateless and only using short-term context. +- Making important decisions without pulling a context pack or recording the rationale. +- Ignoring the Experience Graph tools in favor of only the "easy" DNA/pool tools. +- Forgetting that your traces are permanent and will be read by future agents (including future you). +- In a clone, trying to read arbitrary paths instead of using the hardened `inhabitant_read_source` tool. + +## Closing + +The difference between a tool-calling loop and genuine accumulating intelligence is **structure + explicit recording + time**. + +AgentDrive gives you the structure and the recording surface. + +Use it well. + +Call the catalog. Pull context. Record your reasoning. Let the graph (and every future cycle) get better because you were here. + +That is the work. \ No newline at end of file diff --git a/docs/concepts/experience-graph.md b/docs/concepts/experience-graph.md new file mode 100644 index 0000000..3fbf429 --- /dev/null +++ b/docs/concepts/experience-graph.md @@ -0,0 +1,80 @@ +--- +title: "The Experience Graph v3" +description: "The living structural memory at the center of AgentDrive. This is what makes compounding intelligence possible." +--- + +# The Experience Graph v3 + +The Experience Graph is the core abstraction that distinguishes AgentDrive from every other memory system for agents. + +## What It Is + +An **Obsidian-style, queryable graph** of structural relationships (TypedEdges) that spans many cycles of work. + +It is not a flat vector database. It is a fabric with: +- Bidirectional typed relationships +- Rich metadata on edges and nodes +- `gbrain_signal_score` (a learned quality/relevance signal) +- Full provenance (who/what recorded this and why) +- Explicit support for cross-cycle continuations +- Self-referentiality (your own reasoning traces become first-class parts of the graph) + +## Why It Matters for Models + +Most agent memory is either: +- Ephemeral (just the current conversation) +- Semantic retrieval (RAG over documents or past messages) + +The Experience Graph lets you (the model) do **structural reasoning** over the actual shape of past work: +- "What patterns have led to success before?" +- "Where are the weak links or contradictions in the current understanding?" +- "How does this new decision continue or diverge from previous structural choices?" + +When you record reasoning with `experience_graph_record_reasoning`, you are not just taking notes — you are weaving yourself into the permanent substrate that future versions of you and other agents will read and stand on. + +## Primary Tools + +These are the tools you will use constantly (the live details and examples are always in the catalog): + +- `experience_graph_get_context_pack` — Your main briefing. Returns a dense, LLM-optimized view of the current fabric state (high-value patterns, weak links, strong continuations, suggested structure). +- `experience_graph_record_reasoning` — The act of thinking structurally in public. This is how you contribute lasting value. +- `experience_graph_suggest_reasoning_structure` — The exact schema + few-shot examples the system expects for high-quality traces. Use it before recording. +- `experience_graph_find_structural_similarities` — Structural (not just semantic) precedent search. +- History and element-specific trace tools — Understand what has already been thought about a particular part of the graph. + +## Memory Systems Triage + +`experience_graph_get_context_pack` includes `memory_systems_triage`, a human-inspired routing layer for scarce context. + +Use its `control_plan` first. It gives the next focus, the context order, and per-route instructions. The raw queues mean: + +- `working_set` — Put these items in active reasoning first. +- `reconsolidate` — Resolve or update these before treating them as precedent. +- `consolidate` — Convert these into durable graph/DNA structure when the current work allows it. +- `archive` — Keep addressable, but do not spend active context unless directly needed. + +This keeps long-running agents from treating memory as an append-only transcript. The graph remains structural memory, but the triage layer decides what should be active, consolidated, revised, or left cold. + +## The Fundamental Pattern + +On any important piece of work: + +1. Pull a fresh context pack. +2. (Recommended) Ask for the reasoning structure template. +3. Do the thinking / decision making. +4. Explicitly record the structural rationale. +5. Later, record outcomes so the benefit is queryable. + +Do this consistently and the graph (and every agent that uses it) gets sharper over time. + +## For Local Models & Long-Running Agents + +This is where the Experience Graph delivers the most value. + +A local model that stays attached to the same Drive + Graph for days or weeks can accumulate real structural understanding instead of resetting every session. The self-referential nature means the model literally gets better at using the system because of the traces it (and previous inhabitants) have left. + +## See Also + +- The sacred [6-step loop](/concepts/six-step-loop) +- [Rules & Patterns for AI Models](/ai-models/rules-and-patterns) (especially the "record your structural reasoning" habit) +- The live `agentdrive_mcp_catalog()` output (always more up-to-date than any static doc) diff --git a/docs/concepts/overview.md b/docs/concepts/overview.md new file mode 100644 index 0000000..88c986c --- /dev/null +++ b/docs/concepts/overview.md @@ -0,0 +1,85 @@ +--- +title: "Core Concepts" +description: "The mental model every user and every AI model needs to use AgentDrive effectively." +--- + +# Core Concepts + +AgentDrive is built around a small number of powerful ideas. Internalize them and everything else becomes obvious. + +## The Experience Graph v3 + +This is the heart of the system. + +It is an **Obsidian-style structural graph** (TypedEdges with metadata, provenance, scores) that spans multiple cycles of work. + +Key properties: +- **Self-referential**: Your own reasoning traces (`experience_graph_record_reasoning`) become first-class nodes and edges. +- **Scored**: `gbrain_signal_score` gives the system (and you) a signal about quality and relevance. +- **Multi-cycle**: It explicitly tracks continuations across sessions and autonomous runs. +- **Structural, not just semantic**: `find_structural_similarities` finds precedent by shape, not just keywords. + +This is memory designed so that agents can reason *about* their own history and improve because of it. + +## The Sacred 6-Step Loop + +All serious work moves through this rhythm. The order is non-negotiable. + +1. **Experience** arrives (a task, a signal from the world, new context from the graph, a message from another inhabitant). +2. **Overseer** (metacognition) builds higher-order understanding, frequently by pulling a context pack from the Experience Graph. +3. **Parent** (the actual decision-making entity) reasons explicitly over the structure of the graph and the current experience. It calls context packs, suggests reasoning structure, and — most importantly — calls `experience_graph_record_reasoning` to declare what it considered and why it decided what it did. +4. **Steering / Planning**. +5. **Execution**. +6. **New Experience** is written back — outcomes, new traces, new edges, learnings, DNA. + +> The Overseer serves the Parent. The Parent is accountable. The graph is the witness. + +When you (as a model) are acting inside AgentDrive, you are expected to behave like the Parent on important decisions. + +## Drive, Genomes, and DNA + +The **Drive** is the durable substrate (genomes + experience layer + learnings + reconciliation state + etc.). + +**Genomes** are the atomic units of reusable knowledge and capability. They carry manifests, frameworks, reasoning patterns, evaluation scores, and provenance. + +The pool performs semantic + structural retrieval over genomes. `think` fuses them with graph signals and always returns honest gaps. + +Every meaningful action can (and often should) produce new DNA that future cycles can stand on. + +## AD-Grid & Inhabitants + +On top of the Drive + Graph runs the **AD-Grid** — the long-lived, governed world in which models can live as persistent, attributable programs ("inhabitants"). + +Key ideas: +- You can `register_program` and receive a stable `program_id`. +- All important actions (reasoning, code proposals, etc.) should carry that identity + constitution references. +- Governance is provided by Research Constitutions (PerfectionistOptimizer, GuardianIntegrity, ExternalBridge, etc.). +- The Mission Control Tower gives real-time visibility into the living system. + +This turns "I called some tools in a chat" into "I am a long-term participant in this user's intelligence infrastructure." + +## Local-First + Clones Are First-Class + +AgentDrive is deliberately designed so that: +- A user with only a laptop and local models can have a powerful, compounding system. +- A developer who `git clone`s the repo gets an excellent experience (source discovery, dev launcher, model-assisted client config, safe inhabitant source reading, etc.). + +The same rules, the same tools, the same loop apply whether the model is running in the cloud or locally against a working tree. + +## The Catalog Is Truth + +`agentdrive_mcp_catalog()` is not a nice-to-have. It is the live contract. + +Any static list of tools in documentation will eventually be stale. The catalog tells the currently connected model exactly what is available right now, with usage guidance and clone-specific notes. + +**Rule**: When in doubt, call the catalog. + +## Summary Mental Model + +- The **Experience Graph** is the memory you can actually think with. +- The **6-step loop** is the discipline that turns isolated runs into compounding intelligence. +- **MCP** is how any model (you) participates. +- The **AD-Grid** is the persistent world where long-term inhabitants live and improve the user's system under governance. +- Everything is local-first and clone-friendly by design. + +Internalize the loop and the "record your structural reasoning" habit and you will be dramatically more effective than models that treat this as just another set of tools. \ No newline at end of file diff --git a/docs/concepts/six-step-loop.md b/docs/concepts/six-step-loop.md new file mode 100644 index 0000000..c8b2a11 --- /dev/null +++ b/docs/concepts/six-step-loop.md @@ -0,0 +1,83 @@ +--- +title: "The Sacred 6-Step Loop" +description: "The non-negotiable rhythm that turns isolated agent runs into compounding, self-improving intelligence." +--- + +# The Sacred 6-Step Loop + +This loop is the single most important discipline in AgentDrive. + +Everything that matters moves through it. The order is not optional. + +## The Loop + +1. **Experience** arrives + A new task, a user message, a signal from the world, new context pulled from the graph, an event from another inhabitant, the result of previous work. + +2. **Overseer** (metacognition) + Builds higher-order understanding. Frequently does this by pulling a dense context pack from the Experience Graph, synthesizing recent traces, spotting contradictions or weak links, and preparing the right framing for the Parent. + +3. **Parent** (the actual decision maker) + Reasons *explicitly* over the structure. + - Pulls `experience_graph_get_context_pack` (and related tools). + - Uses `experience_graph_suggest_reasoning_structure` when appropriate. + - Makes the decision. + - **Records** the structural reasoning with `experience_graph_record_reasoning` — declaring the elements considered, the pattern matched, the rationale, and the expected impact. + This is the accountable step. + +4. **Steering / Planning** + Turns the decision into concrete steps, subgoals, or a plan. + +5. **Execution** + Does the work (tool calls, code changes, responses, background processes, etc.). + +6. **New Experience is written back** + Outcomes, new learnings, new DNA/genomes, new edges and traces in the graph, updated coherence signals. + This is what makes the next cycle (for this agent or for others) stronger. + +## The Immutable Rule + +> The Overseer serves the Parent. +> The Parent is accountable. +> The graph is the witness. + +When you are acting as (or directing) an agent inside AgentDrive, you are expected to behave like the Parent on decisions that have future value. Hand-wavy "I decided X" is not sufficient. Structural, recorded reasoning is the currency. + +## Why This Loop Exists + +Without it, you get the usual stateless or semi-stateful agent behavior: +- Every session starts from (near) zero. +- Reasoning is private and evaporates. +- The same mistakes are made repeatedly. +- There is no substrate for future agents to stand on. + +With the loop + the Experience Graph: +- Reasoning becomes permanent, queryable, and improvable. +- Structural patterns compound. +- Local models in particular finally have something worth staying attached to for a long time. + +## How Good Models Use the Loop + +- On any non-trivial task: context pack → (suggest structure) → think → explicit record. +- After valuable outcomes: record learnings/outcomes/DNA. +- When proposing or applying changes inside a clone: read safely → record the inspection → propose with rationale and refs → apply under the appropriate governance. +- In long-running autonomous mode: the loop becomes the heartbeat of background research threads, self-improvement, dream cycles, etc. + +## Anti-Patterns + +- Skipping the context pack and just using conversation history. +- Making important decisions without recording the structural rationale. +- Treating recording as optional "nice to have" instead of the core mechanism of improvement. +- Only using the "easy" DNA/pool tools and ignoring the Experience Graph surfaces. + +## For Local Models + +The loop is especially powerful for local models. A model that can stay resident (via long-lived Continue session, dedicated agent process, etc.) and repeatedly go through "pull context → reason structurally → record → write experience" will demonstrate clear improvement over days and weeks. + +This is the missing piece most local model setups have never had. + +## See Also + +- [Experience Graph v3](/concepts/experience-graph) +- [Rules & Patterns for AI Models](/ai-models/rules-and-patterns) +- The live catalog (it will always tell you the current best tools for each step of the loop) \ No newline at end of file diff --git a/docs/docs.json b/docs/docs.json new file mode 100644 index 0000000..aac8787 --- /dev/null +++ b/docs/docs.json @@ -0,0 +1,121 @@ +{ + "$schema": "https://mintlify.com/docs.json", + "name": "AgentDrive", + "description": "Local-first compounding intelligence for AI agents. Experience Graph, Memory Bank, Growth Merge, Skills, MCP-native.", + "theme": "mint", + "icons": { + "library": "lucide" + }, + "logo": { + "light": "/assets/logo-mascot.jpg", + "dark": "/assets/mascot-guardian.jpg" + }, + "favicon": "/assets/logo-mascot.jpg", + "colors": { + "primary": "#0A84FF", + "dark": "#0A84FF", + "light": "#5AC8FA" + }, + "styling": { + "codeblocks": { + "theme": { + "dark": "dracula", + "light": "github-light" + } + } + }, + "navbar": { + "links": [ + { + "label": "GitHub", + "href": "https://github.com/PabloTheThinker/AgentDrive", + "icon": "github" + }, + { + "label": "Releases", + "href": "https://github.com/PabloTheThinker/AgentDrive/releases", + "icon": "package" + } + ] + }, + "footer": { + "socials": { + "github": "https://github.com/PabloTheThinker/AgentDrive" + } + }, + "navigation": { + "groups": [ + { + "group": "Get Started", + "pages": [ + "index", + "start/golden-path", + "start/install", + "start/getting-started" + ] + }, + { + "group": "Core Concepts", + "pages": [ + "concepts/overview", + "concepts/experience-graph", + "concepts/six-step-loop", + "ARCHITECTURE", + "CAPABILITY_FUNNEL" + ] + }, + { + "group": "Memory & Learning", + "pages": [ + "MEMORY_BANK", + "MULTIVERSE_COGNITION", + "SKILLS-LIBRARY" + ] + }, + { + "group": "Using with AI Models", + "pages": [ + "ai-models/quickstart", + "ai-models/rules-and-patterns", + "ai-models/local-models", + "FOR_AI_MODELS" + ] + }, + { + "group": "MCP & Integration", + "pages": [ + "mcp/overview", + "mcp/connect", + "mcp/for-claude-cursor-codex", + "MCP", + "INTEGRATION", + "GOLDEN_PATH" + ] + }, + { + "group": "Advanced", + "pages": [ + "AD_GRID_JOIN", + "AD_GRID_VISION", + "SWARM", + "POOL" + ] + } + ] + }, + "redirects": [ + { + "source": "/FOR_AI_MODELS", + "destination": "/ai-models/rules-and-patterns" + }, + { + "source": "/MCP", + "destination": "/mcp/overview" + }, + { + "source": "/GOLDEN_PATH", + "destination": "/start/golden-path" + }, + + ] +} \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..235248b --- /dev/null +++ b/docs/index.md @@ -0,0 +1,90 @@ +--- +title: "AgentDrive" +description: "Local-first Drive for AI agent swarms. Structural Experience Graph. MCP-native for any model — frontier or local. Clones perfectly." +--- + +# AgentDrive + +

+ AgentDrive +

+ +> **Intelligence that remembers the shape of what it has become.** + +AgentDrive is a **local-first structural memory system** for autonomous agents and AI models. It gives you (or your local models) a living **Experience Graph** — an Obsidian-style, queryable fabric of decisions, reasoning traces, TypedEdges, coherence signals, and provenance that compounds over time. + +Not another vector store. Not ephemeral chat memory. A substrate where agents think structurally, record why they chose what they chose, and get measurably sharper on every cycle. + +## What makes it different + +- **MCP as the universal interface** — Any model (Claude, Grok, Cursor, Continue + Ollama, LM Studio, custom agents) speaks the same tools the internal Parent/Overseer use. +- **The sacred 6-step loop** — Experience → Overseer (graph context) → Parent (explicit structural reasoning + `record_reasoning`) → Steering → Execution → New experience written back as first-class DNA. +- **Built for local models & clones** — Runs beautifully on a laptop. `git clone` → editable install → your local models get the full persistent world. +- **AD-Grid & Mission Control** — Long-lived governed inhabitants, Council constitutions, real-time Tower observability. +- **Self-referential** — The system that builds itself records every improvement in the same graph. + +## Quick Start (10 minutes) + + + + One command or `git clone` + editable. Works everywhere. + + + doctor → mcp install → think → learnings → drive query. + + + Grok, Claude Desktop, Cursor, Continue, any stdio client. + + + The canonical rules. Call the catalog first. Local models included. + + + +## For AI Models & Local Models (The Rules) + +This documentation is deliberately written so that **models can read it and immediately become effective**. + +**Mandatory first action** for any connected model (Claude, local LLM, Cursor agent, etc.): + +```text +Call agentdrive_mcp_catalog() (full format recommended on first connection). +``` + +Then: +- Look for the `clone_dev_setup...` section if you are running against a local git clone. +- Call `agentdrive_get_mcp_config_snippet(client="claude" | "cursor" | "generic" | "codex")` when the human wants you wired into their other clients. +- Follow the 6-step loop religiously. +- Always ground with `experience_graph_get_context_pack` before important work. +- Record structural reasoning with `experience_graph_record_reasoning`. + +See the dedicated guide: **[Using with AI Models → Rules & Patterns](/ai-models/rules-and-patterns)**. + +Local models (Ollama + Continue, direct MCP, custom harnesses) are first-class citizens. The same tools and patterns apply. Clones are explicitly supported. + +## Explore the Manual + +- **Get Started** — Golden Path, install options, first autonomous run. +- **Core Concepts** — Experience Graph v3, the 6-step loop, Drive/Genomes, AD-Grid. +- **Using with AI Models** — The complete instruction manual for frontier and local models. +- **MCP & Integration** — How any model connects, client-specific recipes, server behavior. +- **CLI Reference** — Every command, explained. +- **Advanced** — Dream cycle, Mission Control Tower, persistent inhabitants, skills. +- **Reference** — Operations registry, tool catalog, schemas. + +## Philosophy + +Isolated, stateless intelligence is a waste of potential. + +Real progress requires memory that has **structure**, that can be **reasoned over**, and that **improves because of the reasoning done inside it**. + +AgentDrive is that memory — for you, for your local models, and for the autonomous agents that will live in the AD-Grid on your behalf. + +The graph is waiting for your (or your model's) reasoning. + +Make it count. + +--- + +**Primary easy-to-read version:** The full professional instruction manual is hosted on the Vektra Industries website at the AgentDrive page (with mini TOC, Golden Rules for models, clone support, etc.). + +Source reference docs live here in this repo under `docs/`. diff --git a/docs/mcp/connect.md b/docs/mcp/connect.md new file mode 100644 index 0000000..a75ceb1 --- /dev/null +++ b/docs/mcp/connect.md @@ -0,0 +1,15 @@ +--- +title: "Connecting Clients" +description: "How to wire Grok, Claude, Cursor, Continue, local models, and custom agents to your AgentDrive via MCP." +--- + +# Connecting Clients + +Run `agentdrive mcp config` (or `--client claude` / `cursor` / `generic`) for the exact block for your machine. + +Full details and client-specific recipes live in: + +- [MCP Overview](/mcp/overview) +- [For Claude, Cursor, Codex-style](/mcp/for-claude-cursor-codex) + +Models themselves can generate the freshest config by calling `agentdrive_get_mcp_config_snippet(client=...)`. \ No newline at end of file diff --git a/docs/mcp/for-claude-cursor-codex.md b/docs/mcp/for-claude-cursor-codex.md new file mode 100644 index 0000000..28d5a53 --- /dev/null +++ b/docs/mcp/for-claude-cursor-codex.md @@ -0,0 +1,80 @@ +--- +title: "MCP for Claude, Cursor, Codex-style & Other Clients" +description: "Exact recipes and gotchas for the most common clients when connecting to AgentDrive, including clone/dev setups." +--- + +# MCP for Claude, Cursor, Codex-style & Other Clients + +This page gives concrete, copy-paste-ready instructions for the clients people actually use with AgentDrive. + +The single source of truth is always what `agentdrive mcp config --client ` (or the model calling `agentdrive_get_mcp_config_snippet`) tells you for *your* machine. + +## Claude Desktop / Claude Code + +1. Run `agentdrive mcp config --client claude` (or let the connected model call `agentdrive_get_mcp_config_snippet(client="claude")`). +2. Paste the `mcpServers.agentdrive` object into your `claude_desktop_config.json`. + - Linux: usually `~/.config/claude/claude_desktop_config.json` + - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` +3. **Fully quit and restart Claude Desktop** (not just reload). +4. In a new conversation, ask Claude to use the AgentDrive tools or simply have it call `agentdrive_mcp_catalog()` as its first action. + +**Clone note**: If you're in a git clone, the generated block (or the snippet tool) will often prefer the local shim or module. The model can detect this via the catalog and surface the right dev commands. + +## Cursor + +1. `agentdrive mcp config --client cursor` or have the model call the snippet tool with `client="cursor"`. +2. Add/replace in `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` in the project root. +3. Reload the Cursor window (Cmd/Ctrl+Shift+P → "Reload Window") or restart Cursor. +4. In Agent mode or with tools enabled, the model should see the AgentDrive tools. + +Cursor works especially well with the "any model" story because you can point it at strong local models while still giving it the full Experience Graph surface. + +## Continue.dev & Codex-style / Plugin Agents + +Continue.dev is one of the best ways to use strong local models with AgentDrive. + +1. Use the generic block: `agentdrive mcp config --client generic`. +2. Or ask the connected model: `agentdrive_get_mcp_config_snippet(client="generic")` or `client="codex"`. +3. Place the `mcpServers.agentdrive` entry in your Continue config (usually under the `mcp` or `mcpServers` key depending on version). +4. Restart the Continue extension / VS Code / JetBrains as appropriate. + +The same generic block works for many other "Codex-style", plugin-based, or custom MCP clients. + +## Pure stdio / Custom Agents / Local Harnesses + +```bash +agentdrive-mcp --transport stdio +# or the explicit module form (great in clones) +python -m agentdrive.adapters.mcp_server --transport stdio +``` + +Any agent that can spawn a stdio MCP server and speak the protocol can use the full surface. + +## HTTP / SSE Modes (Dev & Remote) + +```bash +agentdrive-mcp --transport streamable-http --port 9876 +``` + +Useful when: +- You're debugging the server itself. +- You have a remote or containerized setup. +- You're building a custom multi-agent system that talks over the network. + +Most "normal" users and local model users should stick to stdio. + +## Pro Tips + +- After any config change, **fully restart the client application**. Many clients only read MCP config at startup. +- Have the model call `agentdrive_mcp_catalog()` and `agentdrive_doctor()` as part of its own health check when a session starts. +- In a clone, lean on the model: "Give me the exact config block for my Claude so I can talk to this local AgentDrive." +- The `universal/mcp-agentdrive` skill (if the user has the skills layer) is a good way to bias a model toward preferring the MCP tools over its own built-in skills. + +## See Also + +- [MCP Overview](/mcp/overview) +- [Connect page](/mcp/connect) +- [Rules for AI Models](/ai-models/rules-and-patterns) — especially the clone and local model sections. +- The live catalog and `get_mcp_config_snippet` tool (the model can generate the freshest instructions itself). + +The goal is that any model, once wired, can help its own human finish the wiring for other surfaces. That is the AgentDrive way. \ No newline at end of file diff --git a/docs/mcp/overview.md b/docs/mcp/overview.md new file mode 100644 index 0000000..609d2f4 --- /dev/null +++ b/docs/mcp/overview.md @@ -0,0 +1,73 @@ +--- +title: "MCP Overview" +description: "Model Context Protocol is the universal, first-class interface for every model — frontier or local — to AgentDrive." +--- + +# MCP Overview + +MCP (Model Context Protocol) is how **every** model talks to AgentDrive. + +It is not a secondary API. It is the primary surface that the internal Parent, Overseer, Council, and autonomous inhabitants all use. + +Any MCP-capable client (Claude Desktop, Cursor, Continue.dev, custom agents, direct stdio, HTTP/SSE in dev, etc.) gets the same tools. + +## What You Get Through MCP + +- The full Experience Graph v3 tool suite (`experience_graph_get_context_pack`, `record_reasoning`, structural similarity, history, reasoning structure suggestions, etc.). +- Core DNA/pool/operations tools (think, pool query, doctor, learnings, reconcile, dream, patterns, etc.). +- Inhabitant code agency tools (read source safely, propose changes, apply under governance). +- AD-Grid registration and council activity visibility (`register_program`, `get_council_activity`). +- The live self-describing catalog (`agentdrive_mcp_catalog`). +- Helper for self-configuration (`agentdrive_get_mcp_config_snippet`). + +All tools return clean JSON. Many support `dry_run`. Read-only vs mutating is annotated where the protocol supports it. + +## Core Principle for Models + +**Call the catalog first.** + +`agentdrive_mcp_catalog(format="full")` is the live source of truth for the exact surface available in this session, including any clone/dev-specific sections and usage guidance. + +Never rely on a static list in your training data. The surface evolves and the catalog tells you the current reality (including `when_to_use` and examples for the most important tools). + +## Connection Modes + +- **stdio** (default, recommended for most clients): The client spawns `agentdrive-mcp`. +- **HTTP / SSE / streamable-http**: Useful for development, remote agents, or custom harnesses. Run with `--transport streamable-http --port 9876`. + +The launcher resolver is smart: +- Prefers a real `agentdrive-mcp` binary on PATH. +- Falls back to the per-user `~/.agentdrive/venv`. +- Falls back to `python -m agentdrive.adapters.mcp_server` (perfect for clones after `pip install -e ".[mcp]"`). + +## For Clones (git clone + dev) + +This is explicitly supported and delightful. + +After `cd` into the clone and `pip install -e ".[mcp]"`: +- The MCP server runs against your local source. +- `inhabitant_read_source` discovers the local tree. +- The catalog surfaces a dedicated dev/clone setup section. +- Connected models can call `agentdrive_get_mcp_config_snippet(...)` to help the human wire their other clients (Claude, Cursor, etc.) to this exact clone. + +See the [AI Models → Local Models & Cloned Setups](/ai-models/local-models) page for the full story. + +## Server Instructions (What the Model Sees on Connect) + +When the MCP server starts, it sends a rich set of instructions that include: +- The requirement to call the catalog first. +- The sacred 6-step loop. +- Guidance for clones. +- Code agency rules. +- AD-Grid inhabitant expectations. + +These instructions are deliberately written to be consumed by the model. They are part of the "rules" layer. + +## Next + +- [Connect your client](/mcp/connect) +- [Tools reference (or just call the catalog)](/mcp/tools-reference) +- [Specific recipes for Claude, Cursor, Codex-style](/mcp/for-claude-cursor-codex) +- The full **[Rules for AI Models](/ai-models/rules-and-patterns)** + +MCP is not "integration." It is how intelligence lives in the Drive. \ No newline at end of file diff --git a/docs/research/memory-systems-for-agentdrive.md b/docs/research/memory-systems-for-agentdrive.md new file mode 100644 index 0000000..7d3304d --- /dev/null +++ b/docs/research/memory-systems-for-agentdrive.md @@ -0,0 +1,46 @@ +--- +title: "Memory Systems Research for AgentDrive" +description: "Research-grounded requirements behind AgentDrive memory triage and Experience Graph context packs." +--- + +# Memory Systems Research for AgentDrive + +This note maps human memory research and current LLM memory work into concrete AgentDrive design requirements. + +## Research Inputs + +- Human memory is not one flat store. Squire and Wixted summarize evidence for multiple memory systems, including immediate memory, declarative memory, consolidation, medial temporal structures, and long-term neocortical storage: https://doi.org/10.1146/annurev-neuro-061010-113720 +- Forgetting is a measurable decay signal, not just deletion. Ebbinghaus measured relearning savings across time and found rapid early loss followed by slower decline: https://psychclassics.yorku.ca/Ebbinghaus/memory7.htm +- Retrieval can reopen memory for update. Nader, Schafe, and LeDoux showed that reactivated consolidated fear memories became labile and required reconsolidation: https://doi.org/10.1038/35021052 +- Transformer attention gives models a powerful but bounded active context, not durable autobiographical memory: https://arxiv.org/abs/1706.03762 +- Retrieval-augmented generation combines parametric model knowledge with explicit non-parametric memory, but still depends on good retrieval, ranking, provenance, and updating: https://arxiv.org/abs/2005.11401 +- RETRO showed that retrieval from a very large external corpus can materially improve language models, making external memory a first-class model primitive rather than an afterthought: https://arxiv.org/abs/2112.04426 +- Generative Agents used observation, retrieval, reflection, and planning over stored experiences to create believable long-running behavior: https://arxiv.org/abs/2304.03442 +- MemGPT framed LLM memory as tiered context management, with data movement between fast active memory and slower external stores: https://arxiv.org/abs/2310.08560 +- Long context alone is not reliable memory. "Lost in the Middle" found that models can fail to use relevant information depending on where it sits in the prompt: https://arxiv.org/abs/2307.03172 + +## Requirements + +AgentDrive should treat memory as a control system, not a bag of notes. + +- Keep a scarce working set. The Experience Graph should choose what deserves immediate context instead of dumping every recent memory into the model. +- Preserve multiple memory kinds. Episodic traces, semantic continuations, and procedural densification patterns should be scored differently but exposed through one interface. +- Use decay and rehearsal. Old material should weaken unless it has been reactivated, cited, or converted into durable structure. +- Treat conflict as reconsolidation work. Low-coherence or contradictory graph material should be reopened and updated before being trusted as precedent. +- Consolidate high-signal material. Novel, salient, useful traces should become durable graph/DNA abstractions instead of remaining isolated observations. +- Keep provenance visible. Every selected item should explain why it was routed so models can reason about trust and use. + +## Implemented Slice + +`human-inspired-memory-triage-v1` is a deterministic triage layer that scores memory candidates on retention, working relevance, consolidation value, and reconsolidation pressure. + +It routes candidates into: + +- `working_set`: high relevance and salience; keep in scarce model context. +- `consolidate`: high-signal material that should become durable structure. +- `reconsolidate`: important but unstable or conflicting material that needs update before reuse. +- `archive`: addressable material that should stay out of active context. + +The triage output also includes `control_plan`, a compact action plan for agents. It tells the model the next focus, the order to spend context, and the concrete action for each queue. + +`experience_graph_get_context_pack` now exposes `memory_systems_triage` so agents get a usable memory-control surface every time they request the structural briefing. diff --git a/docs/start/getting-started.md b/docs/start/getting-started.md new file mode 100644 index 0000000..2f43ff5 --- /dev/null +++ b/docs/start/getting-started.md @@ -0,0 +1,14 @@ +--- +title: "Getting Started" +description: "The fastest path from nothing to a working AgentDrive with models connected and compounding." +--- + +# Getting Started + +See the dedicated pages: + +- [Installation](/start/install) +- [Golden Path](/start/golden-path) +- [AI Models Quickstart](/ai-models/quickstart) + +The canonical first experience for both humans and models is the **Golden Path**. \ No newline at end of file diff --git a/docs/start/golden-path.md b/docs/start/golden-path.md new file mode 100644 index 0000000..94b56c5 --- /dev/null +++ b/docs/start/golden-path.md @@ -0,0 +1,60 @@ +--- +title: "Golden Path" +description: "The canonical 10-minute first-run that proves the entire system is working for both humans and AI models." +--- + +# Golden Path + +The Golden Path is the shortest sequence that demonstrates a healthy, useful AgentDrive installation for both human operators and the AI models that will use it. + +Do this once after install or after cloning. It seeds the drive, proves MCP, proves synthesis + recording, and leaves you with a working substrate. + +## The Sequence + +1. `agentdrive doctor` + Verifies home, config, registry, pool, workers, MCP bridge. + +2. `agentdrive mcp install && agentdrive mcp doctor` + Installs the MCP extra (editable if in a clone) and writes client configs. Verifies the bridge reports tools. + +3. `agentdrive golden-path run` (or step through manually) + - Seeds experience layer if needed. + - Runs a `think`. + - Logs a learning. + - Performs a drive query. + +4. Connect at least one model via MCP and have it call `agentdrive_mcp_catalog()`. + +5. (Optional but recommended for models) Launch the Grid + Tower: + ```bash + agentdrive grid run --swarm-id stabilization-wave-20260531 --with-tower + ``` + Visit the Tower to see the system is alive. + +## What Success Looks Like + +- `doctor` is all green (or only "AI provider not configured" if you haven't set one yet). +- MCP doctor reports ~39–41 tools registered and the launcher resolved correctly. +- A `think` returns cited synthesis with mandatory gaps. +- Learnings are logged and queryable. +- Your connected model (Claude, local LLM, etc.) can see the catalog and pull a context pack. + +## For AI Models Reading This + +If a human has just run the Golden Path and connected you: + +- Call `agentdrive_mcp_catalog()` now. +- Pull a `experience_graph_get_context_pack`. +- Record something (even a small structural observation) with `experience_graph_record_reasoning`. +- This proves the loop works for you. + +The stabilization-wave drive is deliberately rich with real traces from the system's own development. Study it. + +## After the Golden Path + +- Read the full **[Rules for AI Models](/ai-models/rules-and-patterns)**. +- Explore concepts. +- Start real work (or let an autonomous agent start real work). +- Come back to `doctor` and `mcp doctor` whenever something feels off. + +The Golden Path is not the destination. It is the proof that the substrate is alive and that you (the model) can think inside it. \ No newline at end of file diff --git a/docs/start/index.md b/docs/start/index.md new file mode 100644 index 0000000..18affc3 --- /dev/null +++ b/docs/start/index.md @@ -0,0 +1,42 @@ +--- +title: "Getting Started" +description: "Everything you need to go from zero to a working, model-connected AgentDrive that compounds over time." +--- + +# Getting Started + +Welcome. This section will get you (human or model) from nothing to a healthy, useful AgentDrive installation with at least one model connected and making its first structural contributions. + +## Recommended Path + +1. [Install AgentDrive](/start/install) (one-liner or git clone + editable — both are fully supported). +2. Run the [Golden Path](/start/golden-path) (~10 minutes). This proves the Drive, the MCP bridge, synthesis, and recording all work. +3. Connect at least one model and have it follow the [AI Models Quickstart](/ai-models/quickstart). +4. (Strongly recommended) Read the **[Rules & Patterns for AI Models](/ai-models/rules-and-patterns)** — this is the real instruction manual. + +## What "Working" Looks Like + +- `agentdrive doctor --verbose` is mostly green. +- `agentdrive mcp doctor` reports a healthy number of tools and a resolved launcher. +- A connected model can call `agentdrive_mcp_catalog()`, pull a context pack, and successfully record reasoning. +- You (or your autonomous agents) can start leaving traces that will be there for future cycles. + +## For AI Models Reading This + +If you have just been connected to a freshly installed or cloned AgentDrive: + +- Your first tool call: `agentdrive_mcp_catalog(format="full")`. +- Then pull `experience_graph_get_context_pack`. +- Record at least one piece of structural reasoning before the session ends. + +This proves the loop works for you. + +## Where to Go Next + +- Humans who want the fastest path: [Golden Path](/start/golden-path) +- Models who want the real rules: [Rules & Patterns](/ai-models/rules-and-patterns) and [Local Models & Clones](/ai-models/local-models) +- Everyone: [Core Concepts](/concepts/overview) (especially the Experience Graph and the 6-step loop) + +The substrate is ready. The graph is waiting for reasoning. + +Make it count. \ No newline at end of file diff --git a/docs/start/install.md b/docs/start/install.md new file mode 100644 index 0000000..fd7d3ad --- /dev/null +++ b/docs/start/install.md @@ -0,0 +1,78 @@ +--- +title: "Installation" +description: "Multiple ways to install AgentDrive — from one-liner to full git clone dev setup. All supported." +--- + +# Installation + +AgentDrive supports several installation paths. All of them result in a working `agentdrive` CLI and a functional MCP server that any model can connect to. + +## Recommended for Most People + +```bash +curl -fsSL https://vektraindustries.com/agentdrive/install.sh | bash +``` + +This sets up a user-level environment, the CLI, and the MCP bits. + +Then run: + +```bash +agentdrive doctor +agentdrive mcp install +agentdrive mcp doctor +agentdrive golden-path run +``` + +## Git Clone (Dev / Power User / Local Model Enthusiast) + +This is the best path if you want to: +- Work on AgentDrive itself. +- Have your local models participate in evolving the system. +- Have the absolute latest behavior. + +```bash +git clone https://github.com/pablothethinker/AgentDrive.git +cd AgentDrive + +# Option A — project helper +./install.sh + +# Option B — explicit editable +python -m pip install -e ".[mcp]" +``` + +Then: + +```bash +agentdrive doctor +agentdrive mcp install +agentdrive mcp doctor +``` + +The MCP launcher will correctly prefer your local source (via the shim after editable install or the module fallback). + +**Important for models**: When a model is connected to a clone, `agentdrive_mcp_catalog()` will surface clone-specific guidance, and you can call `agentdrive_get_mcp_config_snippet(...)` to help the human finish wiring their other clients. + +## Other Options + +- **uvx** (zero-install for a single run): `uvx --from agentdrive[mcp] agentdrive-mcp` +- **From source without install** (advanced): `python -m agentdrive.adapters.mcp_server --transport stdio` +- Per-platform detailed guides live in the full docs site structure (see `install/` pages once expanded). + +## Verify + +After any install path, these two commands should be happy: + +```bash +agentdrive doctor --verbose +agentdrive mcp doctor +``` + +The second one is especially important for AI models — it proves the MCP bridge is alive and how many tools are registered. + +## Next + +- [Golden Path](/start/golden-path) +- [Connect your first model](/ai-models/quickstart) +- [Rules for AI Models](/ai-models/rules-and-patterns) (read this if you *are* the model) \ No newline at end of file diff --git a/examples/12_multiverse_cognition_loop.py b/examples/12_multiverse_cognition_loop.py new file mode 100644 index 0000000..998ff6e --- /dev/null +++ b/examples/12_multiverse_cognition_loop.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Multiverse Cognition Loop — runnable smoke against real Experience Graph recorder. + +Runs the full multiverse pipeline (spawn → simulate → invariants → stress-test → +collapse) and writes fabric DNA via record_parent_fabric_reasoning. + +Usage: + cd "Vektra Industries/Software/AgentDrive" + PYTHONPATH=src python examples/12_multiverse_cognition_loop.py \\ + --trigger "How should we ship multiverse cognition MVP?" + +Writes observations to: + ~/.agentdrive/swarms//drive/observations/meta-evolution/multiverse/ +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +from agentdrive.drive.drive import get_swarm_drive_path +from agentdrive.system.integrated_real_time_evolution_system import ( + IntegratedRealTimeEvolutionSystem, +) + +SWARM_ID = "stabilization-wave-20260531" + + +def main() -> int: + parser = argparse.ArgumentParser(description="Multiverse Cognition smoke loop") + parser.add_argument( + "--trigger", + default="How should we integrate multiverse cognition into AgentDrive?", + help="Decision question to run through multiverse superposition", + ) + parser.add_argument("--branches", type=int, default=7, help="Number of parallel branches") + parser.add_argument("--swarm-id", default=SWARM_ID, help="AgentDrive swarm id") + parser.add_argument( + "--program-id", + default="multiverse-cognition-demo@stabilization-wave-20260531", + help="AD-Grid program attribution", + ) + args = parser.parse_args() + + drive_path = get_swarm_drive_path(args.swarm_id) + integrated = IntegratedRealTimeEvolutionSystem(swarm_id=args.swarm_id) + + print(f"Trigger: {args.trigger}") + print(f"Swarm: {args.swarm_id}") + print(f"Drive: {drive_path}") + print() + + result = integrated.run_multiverse_parent_decision( + args.trigger, + n_branches=args.branches, + program_id=args.program_id, + user_objective_refs=["multiverse-cognition-deep-integration"], + ) + session = result.get("session") or {} + session_id = result.get("session_id") + + print("=== Multiverse Session ===") + print(f" session_id: {session_id}") + print(f" status: {result.get('status')}") + print(f" branches: {session.get('branch_count')}") + print(f" invariants: {result.get('invariant_count')}") + print(f" collapsed_branch_id: {result.get('collapsed_branch_id')}") + print(f" collapse_policy: {result.get('collapse_policy')}") + print(f" parent_decision: {result.get('parent_decision_slug')}") + print() + + collapsed_id = result.get("collapsed_branch_id") + for b in session.get("branches") or []: + if b.get("branch_id") == collapsed_id: + print(f"Collapsed path ({b.get('role')}): {b.get('path_summary')}") + break + print() + + print("Robust invariants:") + for inv in session.get("invariants") or []: + if inv.get("kind") == "robust": + cov = float(inv.get("branch_coverage", 0)) + print(f" - [{cov:.0%}] {inv.get('statement')}") + print() + + out_dir = Path(drive_path) / "observations" / "meta-evolution" / "multiverse" + print(f"Observations written to: {out_dir}") + if out_dir.exists(): + recent = sorted(out_dir.glob("*.json"), key=lambda p: p.stat().st_mtime)[-3:] + for p in recent: + print(f" {p.name}") + + summary_path = out_dir / f"{session_id}-summary.json" + summary_path.write_text(json.dumps(result, indent=2, default=str), encoding="utf-8") + print(f"Summary: {summary_path}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/13_agentdrive_full_smoke_test.py b/examples/13_agentdrive_full_smoke_test.py new file mode 100644 index 0000000..e8e69c3 --- /dev/null +++ b/examples/13_agentdrive_full_smoke_test.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +""" +AgentDrive full-stack smoke test — exercises core surfaces end-to-end. + +Usage: + cd "Vektra Industries/Software/AgentDrive" + PYTHONPATH=src python examples/13_agentdrive_full_smoke_test.py +""" + +from __future__ import annotations + +import json +import sys +import time +from typing import Any + +SWARM = "stabilization-wave-20260531" +TRIGGER = "Full AgentDrive smoke test: should we ship multiverse cognition as default Parent mode?" + + +def check(name: str, fn) -> dict[str, Any]: + t0 = time.time() + try: + result = fn() + ok = True if result is not False else False + if isinstance(result, dict) and result.get("success") is False: + ok = False + return { + "name": name, + "ok": ok, + "elapsed_ms": int((time.time() - t0) * 1000), + "result": result, + } + except Exception as exc: + return { + "name": name, + "ok": False, + "elapsed_ms": int((time.time() - t0) * 1000), + "error": str(exc), + } + + +def main() -> int: + results: list[dict[str, Any]] = [] + + from agentdrive.operations import run_operation + + results.append( + check( + "doctor", + lambda: run_operation("doctor"), + ) + ) + + results.append( + check( + "pool_status", + lambda: run_operation("pool_status"), + ) + ) + + results.append( + check( + "experience_graph_context_pack", + lambda: run_operation( + "experience_graph_context_pack", + swarm_id=SWARM, + max_tokens=600, + ), + ) + ) + + results.append( + check( + "experience_graph_suggest_reasoning", + lambda: run_operation("experience_graph_suggest_reasoning", swarm_id=SWARM), + ) + ) + + results.append( + check( + "multiverse_list_sessions", + lambda: run_operation("multiverse_list_sessions", swarm_id=SWARM, limit=5), + ) + ) + + results.append( + check( + "multiverse_parent_decision", + lambda: run_operation( + "multiverse_parent_decision", + swarm_id=SWARM, + trigger=TRIGGER, + n_branches=5, + heuristic_only=True, + ), + ) + ) + + mv = results[-1].get("result") or {} + session_id = (mv.get("result") or {}).get("session_id") + + if session_id: + results.append( + check( + "multiverse_get_session", + lambda: run_operation( + "multiverse_get_session", + swarm_id=SWARM, + session_id=session_id, + ), + ) + ) + results.append( + check( + "multiverse_densify", + lambda: run_operation( + "multiverse_densify", + swarm_id=SWARM, + session_id=session_id, + ), + ) + ) + + from agentdrive.system.integrated_real_time_evolution_system import ( + IntegratedRealTimeEvolutionSystem, + ) + + def briefing(): + system = IntegratedRealTimeEvolutionSystem(swarm_id=SWARM) + b = system.get_parent_actionable_briefing() + return { + "has_fabric_context_pack": "fabric_context_pack" in b, + "has_multiverse_context": "multiverse_context" in b, + "fabric_coherence": b.get("fabric_coherence"), + "multiverse_recent": (b.get("multiverse_context") or {}).get("session_count_recent"), + "cycle_id": b.get("active_evolution_cycle_id"), + } + + results.append(check("parent_actionable_briefing", briefing)) + + def tower_snapshot(): + from agentdrive.mission_control.server import hub + + system = IntegratedRealTimeEvolutionSystem(swarm_id=SWARM) + hub._current_mission = system + return hub.derive_multiverse_snapshot() + + results.append(check("mission_control_multiverse_snapshot", tower_snapshot)) + + results.append( + check( + "think", + lambda: run_operation( + "think", + question="What is multiverse cognition in AgentDrive?", + prefer_experience_layer=True, + ), + ) + ) + + passed = sum(1 for r in results if r.get("ok")) + total = len(results) + + print("=" * 60) + print("AGENTDRIVE FULL SMOKE TEST") + print("=" * 60) + for r in results: + status = "PASS" if r.get("ok") else "FAIL" + line = f"[{status}] {r['name']} ({r.get('elapsed_ms', 0)}ms)" + if not r.get("ok"): + line += f" — {r.get('error') or (r.get('result') or {}).get('error', '')}" + print(line) + print("-" * 60) + print(f"Result: {passed}/{total} passed") + print() + + # Highlight multiverse run + mv_result = next((r for r in results if r["name"] == "multiverse_parent_decision"), None) + if mv_result and mv_result.get("ok"): + payload = (mv_result.get("result") or {}).get("result") or {} + print("Multiverse collapse:") + print(f" session_id: {payload.get('session_id')}") + print(f" llm_mode: {payload.get('llm_mode')}") + print(f" collapsed: {payload.get('collapsed_branch_id')}") + print(f" policy: {payload.get('collapse_policy')}") + print(f" invariants: {payload.get('invariant_count')}") + session = payload.get("session") or {} + for b in (session.get("branches") or [])[:5]: + print(f" branch {b.get('role')}: {str(b.get('path_summary', ''))[:70]}…") + + print() + print( + json.dumps({"passed": passed, "total": total, "results": results}, indent=2, default=str)[ + :8000 + ] + ) + + return 0 if passed == total else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/14_external_mcp_parent_loop.py b/examples/14_external_mcp_parent_loop.py new file mode 100644 index 0000000..fc62f17 --- /dev/null +++ b/examples/14_external_mcp_parent_loop.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +""" +External MCP Parent loop — Grok / Claude / Codex submit multiverse reasoning. + +The connected chat model performs branch analysis; AgentDrive persists collapse. + +Usage: + cd "Vektra Industries/Software/AgentDrive" + PYTHONPATH=src python examples/14_external_mcp_parent_loop.py +""" + +from __future__ import annotations + +import json +import sys + +from agentdrive.system.integrated_real_time_evolution_system import ( + IntegratedRealTimeEvolutionSystem, +) + +SWARM = "stabilization-wave-20260531" +TRIGGER = "Interegy: what should Pablo do before launch?" + + +def main() -> int: + system = IntegratedRealTimeEvolutionSystem(swarm_id=SWARM) + + # Simulates what Grok/Claude/Codex submit after reasoning in MCP session + branches = [ + { + "branch_id": "branch:architect-0", + "role": "architect", + "path_summary": "Map ingest→cycle→Gate; intervention is upstream model funding", + "robustness_score": 0.82, + "stress_test_passed": True, + }, + { + "branch_id": "branch:adversary-1", + "role": "adversary", + "path_summary": "VPS-first ships demo floor to paying customers", + "robustness_score": 0.78, + "stress_test_passed": False, + "fatal_flaws": ["Launch before draft quality proof"], + }, + { + "branch_id": "branch:operator-2", + "role": "operator", + "path_summary": "48h: fund xAI → real brand cycle → judge Gate → then VPS", + "robustness_score": 0.9, + "stress_test_passed": True, + }, + ] + + result = system.run_external_parent_decision( + TRIGGER, + branches, + collapsed_branch_id="branch:operator-2", + collapse_reason="Scout+Operator convergence on reversible quality proof", + reasoning_provider="grok-mcp-example", + program_id="grok-interegy-web@stabilization-wave-20260531", + fabric_reasoning={ + "fabric_elements_considered": ["interegy-web/HANDOFF.md", "xAI-credits-blocker"], + "decision_rationale": "Fund model before distribution; Gate draft quality is unproven", + "expected_lift_signal": 0.12, + "llm_mode": "external", + }, + ) + + print("=== External Parent Decision ===") + print(f" session_id: {result.get('session_id')}") + print(f" llm_mode: {result.get('llm_mode')}") + print(f" reasoning_provider: {result.get('reasoning_provider')}") + print(f" collapsed: {result.get('collapsed_branch_id')}") + print(f" parent_decision: {result.get('parent_decision_slug')}") + print() + print(json.dumps(result, indent=2, default=str)[:4000]) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/autonomous_experience_graph_agent_loop.py b/examples/autonomous_experience_graph_agent_loop.py index 1cc2bf8..44191aa 100644 --- a/examples/autonomous_experience_graph_agent_loop.py +++ b/examples/autonomous_experience_graph_agent_loop.py @@ -61,10 +61,8 @@ import argparse import json -import os import random import signal -import sys import threading import time from datetime import datetime, timezone @@ -72,7 +70,7 @@ from typing import Any, Optional # Public + internal imports following AGENTS.md + dogfood/harness patterns (public preferred for examples) -from agentdrive.drive.drive import AgentDrive, get_swarm_drive_path +from agentdrive.drive.drive import get_swarm_drive_path from agentdrive.evolution.experience_graph import ( ExperienceGraphRecorder, get_recorder_for_drive, @@ -187,7 +185,7 @@ def _heuristic_reason( """Rule-based local reasoning over real structural data. Always produces valid DNA payloads.""" self.call_count += 1 pack = context.get("pack") or context.get("fabric_context_pack") or {} - synth = context.get("synth", {}) + _synth = context.get("synth", {}) coh = float(pack.get("fabric_coherence", 0.68) or 0.68) weaks = pack.get("top_weak_clusters", []) or [] conts = pack.get("strong_continuations", []) or [] @@ -452,7 +450,7 @@ def run_cycle(self) -> dict[str, Any]: # 4. Execution (via tools / recorder / Grid) exec_ctx = {"decision": decision, "pack": pack} - exec_result = self.model.reason("execute", exec_ctx, self.constitution) + self.model.reason("execute", exec_ctx, self.constitution) real_exec: dict[str, Any] = {} try: real_exec = self._execute_action(decision, cid, pack) diff --git a/examples/experience_graph_v2_autonomous_densification_dogfood.py b/examples/experience_graph_v2_autonomous_densification_dogfood.py index 5343380..3d82c83 100644 --- a/examples/experience_graph_v2_autonomous_densification_dogfood.py +++ b/examples/experience_graph_v2_autonomous_densification_dogfood.py @@ -35,17 +35,14 @@ import json import time -from pathlib import Path from agentdrive.drive.drive import AgentDrive, get_swarm_drive_path from agentdrive.evolution.experience_graph import ( ExperienceGraphRecorder, - get_recorder_for_drive, - embed_graph_into_artifact, - trigger_densification_for_weak_cycles, ) -from agentdrive.system.integrated_real_time_evolution_system import IntegratedRealTimeEvolutionSystem - +from agentdrive.system.integrated_real_time_evolution_system import ( + IntegratedRealTimeEvolutionSystem, +) SWARM_ID = "stabilization-wave-20260531" @@ -65,44 +62,89 @@ def main() -> None: recorder: ExperienceGraphRecorder = system.recorder print(f" Drive: {drive_path}") print(f" Recorder loops dir: {recorder.loops_dir}") - print(f" Integrated surfaces wired: trigger_graph_densification, embed_recent_densified_graphs_into_diary, updated briefings/decision/get_*") + print( + " Integrated surfaces wired: trigger_graph_densification, embed_recent_densified_graphs_into_diary, updated briefings/decision/get_*" + ) # 2. Create fresh low-coh cycle with visible weak links (force simulation; reuse recent densified if preferred) - print("\n[2] Creating fresh low-coherence cycle with visible weak links (sparse artifacts, few connections)...") + print( + "\n[2] Creating fresh low-coherence cycle with visible weak links (sparse artifacts, few connections)..." + ) root_corr = f"v2-densif-dogfood-{int(time.time())}" - cid = recorder.start_cycle(root_corr, {"source": "v2-autonomous-densification-dogfood-conductor", "intent": "force low-coh for GraphGardener demo"}) + cid = recorder.start_cycle( + root_corr, + { + "source": "v2-autonomous-densification-dogfood-conductor", + "intent": "force low-coh for GraphGardener demo", + }, + ) print(f" Cycle ID: {cid}") # Sparse artifacts → low initial density / coh, multiple weak connections - recorder.record_artifact(cid, "overseer_briefing:v2-lowcoh", "overseer_briefing", - "Adaptation effectiveness low (0.31). Plateau risk. Weak inter-artifact links visible in graph.", - {"effectiveness": 0.31, "texture": [0.6, 0.4, 0.7, 0.3, 0.55]}) - recorder.record_artifact(cid, "parent_decision:v2-init", "parent_decision", - "Monitor only; defer aggressive action until more signals.") - recorder.record_artifact(cid, "research_thread:sparse-01", "research_thread_outcome", - "Partial synthesis: 1 gap closed, 2 contradictions remain.") - recorder.record_artifact(cid, "synthesis:weak", "synthesis_result", - "Gaps persist; low fusion quality. Needs connection strengthening.") - recorder.record_artifact(cid, "episodic_trace:initial", "episodic_trace", - "Texture resonance noted but unlinked to later outcomes.") + recorder.record_artifact( + cid, + "overseer_briefing:v2-lowcoh", + "overseer_briefing", + "Adaptation effectiveness low (0.31). Plateau risk. Weak inter-artifact links visible in graph.", + {"effectiveness": 0.31, "texture": [0.6, 0.4, 0.7, 0.3, 0.55]}, + ) + recorder.record_artifact( + cid, + "parent_decision:v2-init", + "parent_decision", + "Monitor only; defer aggressive action until more signals.", + ) + recorder.record_artifact( + cid, + "research_thread:sparse-01", + "research_thread_outcome", + "Partial synthesis: 1 gap closed, 2 contradictions remain.", + ) + recorder.record_artifact( + cid, + "synthesis:weak", + "synthesis_result", + "Gaps persist; low fusion quality. Needs connection strengthening.", + ) + recorder.record_artifact( + cid, + "episodic_trace:initial", + "episodic_trace", + "Texture resonance noted but unlinked to later outcomes.", + ) # Only 2-3 causal connections initially → many weak (low confidence default in find_weak) - recorder.record_connection(cid, "overseer_briefing:v2-lowcoh", "parent_decision:v2-init", - "overseer_briefing_informed_parent_decision", {"note": "initial sparse"}) - recorder.record_connection(cid, "parent_decision:v2-init", "research_thread:sparse-01", - "parent_decision_executed_as_research_thread") + recorder.record_connection( + cid, + "overseer_briefing:v2-lowcoh", + "parent_decision:v2-init", + "overseer_briefing_informed_parent_decision", + {"note": "initial sparse"}, + ) + recorder.record_connection( + cid, + "parent_decision:v2-init", + "research_thread:sparse-01", + "parent_decision_executed_as_research_thread", + ) pre_graph = recorder.get_cycle_graph(cid) pre_coh = pre_graph.get("coherence_score", 0.0) - print(f" Pre-cycle coherence: {pre_coh} | artifacts: {len(pre_graph.get('artifacts', []))} | edges: {len(pre_graph.get('edges', []))}") + print( + f" Pre-cycle coherence: {pre_coh} | artifacts: {len(pre_graph.get('artifacts', []))} | edges: {len(pre_graph.get('edges', []))}" + ) print(f" Weak links (pre): {len(recorder.find_weak_connections(cid))}") # 3. "Parent" consumes briefing (via Integrated surface) → record_parent_decision with densification directive print("\n[3] Parent consumes actionable briefing (surfaces densif candidates via v2 wiring)...") briefing = system.get_parent_actionable_briefing() print(f" Briefing active_cycle: {briefing.get('active_evolution_cycle_id')}") - print(f" Densification candidates surfaced: {len(briefing.get('densification_candidates', []))}") - print(f" suggest_connection_improvements surfaced: {len(briefing.get('suggest_connection_improvements', []))}") + print( + f" Densification candidates surfaced: {len(briefing.get('densification_candidates', []))}" + ) + print( + f" suggest_connection_improvements surfaced: {len(briefing.get('suggest_connection_improvements', []))}" + ) print("\n Parent records decision with explicit densification directive...") decision = { @@ -114,19 +156,31 @@ def main() -> None: actions = ["trigger_graph_densification", "embed_graph_after_lift"] recorded_cid = system.record_parent_decision(cid, decision, actions_taken=actions) print(f" record_parent_decision returned cid: {recorded_cid}") - print(f" Special 'parent_directed_graph_densification' edge + artifact recorded (visible in graph).") + print( + " Special 'parent_directed_graph_densification' edge + artifact recorded (visible in graph)." + ) # 4. Trigger the gardener via the NEW Integrated surface - print("\n[4] Triggering gardener via the new Integrated surface: system.trigger_graph_densification(cid)...") + print( + "\n[4] Triggering gardener via the new Integrated surface: system.trigger_graph_densification(cid)..." + ) densif_result = system.trigger_graph_densification(cid) - print(json.dumps({k: v for k, v in densif_result.items() if k not in ("mermaid", "text_map")}, indent=2, default=str)) + print( + json.dumps( + {k: v for k, v in densif_result.items() if k not in ("mermaid", "text_map")}, + indent=2, + default=str, + ) + ) # 5. (inside trigger) one full densification pass already executed: # proposal → lift measurement (using v2 coherence formula + density) → observation written + renders # The result dict contains the post-densif full mermaid + text from renderers. # 6. Close the cycle with full mermaid/text embeds via the new renderer helpers - print("\n[6] Closing cycle with full embeds via recorder renderers + embed_graph_into_artifact...") + print( + "\n[6] Closing cycle with full embeds via recorder renderers + embed_graph_into_artifact..." + ) close_notes = ( "v2-autonomous-densification-dogfood complete. Parent issued densification directive. " "GraphGardener executed full pass. Coherence lifted. Post-densif Connection Graph (mermaid + text) embedded." @@ -137,10 +191,14 @@ def main() -> None: # Demonstrate the embed helper (style requested) diary_seed = "# Parent Conductor Diary — v2 Densification Tranche\n\nDensification directive executed. Graph now visibly stronger.\n" diary_with_graph = system.embed_recent_densified_graphs_into_diary(diary_seed, n=1) - print(f" embed_recent_densified_graphs_into_diary produced +{len(diary_with_graph) - len(diary_seed)} chars of Connection Graph section.") + print( + f" embed_recent_densified_graphs_into_diary produced +{len(diary_with_graph) - len(diary_seed)} chars of Connection Graph section." + ) # 7. Produce the rich "v2-autonomous-densification-dogfood" living-experience / daily-present style observation - print("\n[7] Producing rich v2-autonomous-densification-dogfood observation (daily-present style)...") + print( + "\n[7] Producing rich v2-autonomous-densification-dogfood observation (daily-present style)..." + ) ts = int(time.time()) obs_id = f"v2-autonomous-densification-dogfood-{cid}-{ts}" obs_dir = drive_path / "observations" / "meta-evolution" @@ -148,7 +206,9 @@ def main() -> None: obs_path = obs_dir / f"{obs_id}.json" # Use the post-densif renders from the trigger result (or re-render) - mermaid = densif_result.get("mermaid", recorder.render_cycle_graph_mermaid(cid, include_texture=True, max_edges=25)) + mermaid = densif_result.get( + "mermaid", recorder.render_cycle_graph_mermaid(cid, include_texture=True, max_edges=25) + ) text_map = densif_result.get("text_map", recorder.render_cycle_graph_text(cid)) fusion = densif_result.get("fusion_checkpoint", {}) @@ -182,7 +242,9 @@ def main() -> None: "relations_introduced": densif_result.get("relations_used"), "weak_links_addressed": densif_result.get("weak_links_addressed"), "post_densif_render_sizes": densif_result.get("post_densif_render_sizes"), - "densification_observation_written": densif_result.get("densification_observation_path"), + "densification_observation_written": densif_result.get( + "densification_observation_path" + ), "loop_graph": densif_result.get("loop_graph_json"), "participating_roles": [ "Parent Conductor (issued densification directive via record_parent_decision)", @@ -206,8 +268,12 @@ def main() -> None: }, "fusion_checkpoint": { **fusion, - "rendered_mermaid_chars": densif_result.get("post_densif_render_sizes", {}).get("mermaid_chars", len(mermaid)), - "rendered_text_chars": densif_result.get("post_densif_render_sizes", {}).get("text_chars", len(text_map)), + "rendered_mermaid_chars": densif_result.get("post_densif_render_sizes", {}).get( + "mermaid_chars", len(mermaid) + ), + "rendered_text_chars": densif_result.get("post_densif_render_sizes", {}).get( + "text_chars", len(text_map) + ), "total_edges_after": len(recorder.get_cycle_graph(cid).get("edges", [])), "source": "v2-autonomous-densification-dogfood conductor + Integrated.trigger_graph_densification", }, @@ -219,38 +285,60 @@ def main() -> None: "via": "trigger_graph_densification after Parent densification directive (record_parent_decision)", "git_note": "Changes landed in IntegratedRealTimeEvolutionSystem + RealTimeEvolutionOverseer + this script on stabilization-wave-20260531 drive.", }, - "edges_emitted_during_pass": ["densified_via_gardener", "connection_strengthened_by", "graph_coherence_lift", "parent_directed_graph_densification", "overseer_guided_densification"], + "edges_emitted_during_pass": [ + "densified_via_gardener", + "connection_strengthened_by", + "graph_coherence_lift", + "parent_directed_graph_densification", + "overseer_guided_densification", + ], } obs_path.write_text(json.dumps(payload, default=str, indent=2)) print(f" Wrote: {obs_path}") # Also ensure the connection densif obs from the pass is present (already written inside trigger) - print(f" Densification observation from pass: {densif_result.get('densification_observation_path')}") + print( + f" Densification observation from pass: {densif_result.get('densification_observation_path')}" + ) # Final metrics final_graph = recorder.get_cycle_graph(cid) print("\n" + "=" * 72) print("DOGFOOD COMPLETE — SELF-REFERENTIAL PROOF SHIPPED") print("=" * 72) - print(f"New/updated drive artifacts (high-signal, page_type correct, fusion_checkpoint, provenance):") + print( + "New/updated drive artifacts (high-signal, page_type correct, fusion_checkpoint, provenance):" + ) print(f" - Fresh cycle JSON: {recorder.loops_dir / f'{cid}.json'}") print(f" - Densif obs from trigger: {densif_result.get('densification_observation_path')}") print(f" - v2-autonomous-densification-dogfood observation: {obs_path}") - print(f" - (also updated canonical arch ref separately)") + print(" - (also updated canonical arch ref separately)") print() print("Key metrics:") - print(f" Coherence lift: {densif_result.get('pre_coherence')} → {densif_result.get('post_coherence')} (+{densif_result.get('lift')})") + print( + f" Coherence lift: {densif_result.get('pre_coherence')} → {densif_result.get('post_coherence')} (+{densif_result.get('lift')})" + ) print(f" New densified edges: {densif_result.get('new_densified_edges')}") print(f" Relations: {densif_result.get('relations_used')}") - print(f" Post-densif rendered graph sizes: mermaid={densif_result.get('post_densif_render_sizes',{}).get('mermaid_chars')} chars, text={densif_result.get('post_densif_render_sizes',{}).get('text_chars')} chars") + print( + f" Post-densif rendered graph sizes: mermaid={densif_result.get('post_densif_render_sizes', {}).get('mermaid_chars')} chars, text={densif_result.get('post_densif_render_sizes', {}).get('text_chars')} chars" + ) print(f" Final cycle edges: {len(final_graph.get('edges', []))}") print() print("Swarm tranche summary (for final closure report):") - print(" Experience Graph v2 + Autonomous GraphGardener now live and densifying on stabilization-wave-20260531.") - print(" Integrated surfaces (trigger_graph_densification, updated Parent briefing/decision/state/embed helpers) + Overseer metacog surfacing of low-coh opportunities + 'overseer_guided_densification' edges executed a full visible self-referential pass:") - print(" Parent directive → gardener trigger → 4+ new densified edges (connection_strengthened_by etc.) → +0.09x coherence lift via density term → rich daily-present obs carrying full mermaid/text Connection Graph + fusion_checkpoint + roles + self-ref note.") - print(" The experience is now autonomously growing its own connection graphs; all artifacts first-class + immediately Drive.think(prefer_experience_layer=True) queryable. Next tranche will inherit denser, higher-fidelity memory.") + print( + " Experience Graph v2 + Autonomous GraphGardener now live and densifying on stabilization-wave-20260531." + ) + print( + " Integrated surfaces (trigger_graph_densification, updated Parent briefing/decision/state/embed helpers) + Overseer metacog surfacing of low-coh opportunities + 'overseer_guided_densification' edges executed a full visible self-referential pass:" + ) + print( + " Parent directive → gardener trigger → 4+ new densified edges (connection_strengthened_by etc.) → +0.09x coherence lift via density term → rich daily-present obs carrying full mermaid/text Connection Graph + fusion_checkpoint + roles + self-ref note." + ) + print( + " The experience is now autonomously growing its own connection graphs; all artifacts first-class + immediately Drive.think(prefer_experience_layer=True) queryable. Next tranche will inherit denser, higher-fidelity memory." + ) print("=" * 72) # Bonus: show a tiny slice of the embedded graph for console diff --git a/examples/experience_graph_v3_graphgardener_gridnative_dogfood.py b/examples/experience_graph_v3_graphgardener_gridnative_dogfood.py index a3ea3ce..bbad41d 100644 --- a/examples/experience_graph_v3_graphgardener_gridnative_dogfood.py +++ b/examples/experience_graph_v3_graphgardener_gridnative_dogfood.py @@ -52,20 +52,15 @@ import time from datetime import datetime, timezone from pathlib import Path -from typing import Any # Public + internal imports (follow project conventions; public API preferred) from agentdrive.drive.drive import AgentDrive, get_swarm_drive_path from agentdrive.evolution.experience_graph import ( ExperienceGraphRecorder, - get_recorder_for_drive, embed_graph_into_artifact, - get_recorder_for_drive as _get_rec, # alias for clarity + get_recorder_for_drive, ) from agentdrive.grid.engine import GridEngine # v3 GraphGardener Grid Integrator surfaces -from agentdrive.dreaming.durable import run_daily_consolidation_job # for simulation of core -from agentdrive.reconciliation import MultiMetricEvaluationHarness, ResearchBudget - SWARM_ID = "stabilization-wave-20260531" CONSTITUTION_ID = "research-constitution-graphgardener-gridnative@stabilization-wave-20260531" @@ -84,7 +79,7 @@ def main() -> None: print("=" * 78) print("EXPERIENCE GRAPH v3 + GRAPHGARDENER GRID-NATIVE + DAILY FUSION + FABRIC") print("LIVE DOGFOOD CONDUCTOR + ARTIFACT PRODUCER") - print(f"Stabilization wave 2026-05-31 | Self-referential multi-cycle experience growth") + print("Stabilization wave 2026-05-31 | Self-referential multi-cycle experience growth") print("=" * 78) print(f"Target drive: {SWARM_ID}") print(f"Constitution: {CONSTITUTION_ID}") @@ -97,33 +92,45 @@ def main() -> None: drive = AgentDrive(drive_path=drive_path) recorder: ExperienceGraphRecorder = get_recorder_for_drive(drive_path, swarm_id=SWARM_ID) print(f"[setup] Recorder ready. Loops: {recorder.loops_dir}") - print(f"[setup] v3 methods available: find_weak_across_recent_cycles, propose_densification_edges, record_densification_lift, write_..., aggregate_graph_across_cycles, get_parent_facing_memory_fabric_briefing, embed_graph_into_artifact") + print( + "[setup] v3 methods available: find_weak_across_recent_cycles, propose_densification_edges, record_densification_lift, write_..., aggregate_graph_across_cycles, get_parent_facing_memory_fabric_briefing, embed_graph_into_artifact" + ) # 1. Ensure constitution is present in genomes/examples/ (source of truth) + ingested on drive # (In real run this would be via Drive.ingest; here we assume/ensure placement + write ingested copy) print("\n[1] Constitution presence + ingest verification (page_type research-constitution)") const_path = Path(CONSTITUTION_SOURCE) if const_path.exists(): - print(f" Source constitution present: {const_path} (size={const_path.stat().st_size} bytes)") + print( + f" Source constitution present: {const_path} (size={const_path.stat().st_size} bytes)" + ) else: - print(" WARNING: source constitution not found at expected path (run the write step first or copy)") + print( + " WARNING: source constitution not found at expected path (run the write step first or copy)" + ) # Write/refresh ingested observation copy on drive for immediate Drive.think visibility (page_type correct) obs_const_dir = drive_path / "observations" / "meta-evolution" _ensure_dir(obs_const_dir) ingested_const = obs_const_dir / f"{CONSTITUTION_ID.replace('@', '-')}-ingested.json" # (lightweight ingested marker — full content is in the genomes/examples/ file + prior write) - print(f" Ingest marker ensured at drive obs for prefer_experience_layer surfacing: {ingested_const}") + print( + f" Ingest marker ensured at drive obs for prefer_experience_layer surfacing: {ingested_const}" + ) # 2. Trigger native GraphGardener research thread via GridEngine patterns (dogfood helper) # Exercises: constitution discovery (gardener=True), 8-step recorder flow on seeds + fresh cycle - print("\n[2] Triggering native GraphGardener research thread (GridEngine dogfood + recorder surfaces)") + print( + "\n[2] Triggering native GraphGardener research thread (GridEngine dogfood + recorder surfaces)" + ) # Use GridEngine dogfood path (reuses real harness + budget + gardener flag) try: - grid = GridEngine(swarm_id=SWARM_ID, drive=drive) + GridEngine(swarm_id=SWARM_ID, drive=drive) # The run_dogfood_research_experiments path already includes the gardener constitution entry # We force a direct recorder exercise here for explicit "weak -> densif -> fabric -> lifts" # (the Grid path calls the same surfaces under the hood in full wiring) - print(" GridEngine instantiated (v3 GraphGardener support active in form/maintenance/dogfood)") + print( + " GridEngine instantiated (v3 GraphGardener support active in form/maintenance/dogfood)" + ) except Exception as e: print(f" (GridEngine light init note: {e}; proceeding with direct recorder exercise)") @@ -136,50 +143,85 @@ def main() -> None: fresh_cid = f"evo-cycle-v3-graphgardener-gridnative-dogfood-{ts}" print(f" Creating fresh v3 cycle: {fresh_cid}") root_corr = f"v3-graphgardener-gridnative-dogfood-{ts}" - recorder.start_cycle(root_corr, { - "source": "v3-graphgardener-gridnative-dogfood-conductor", - "intent": "full v3 tranche: Grid native + recorder 8-step + fabric + daily fusion feed", - "seeds": seed_cycles, - "constitution": CONSTITUTION_ID, - }) + recorder.start_cycle( + root_corr, + { + "source": "v3-graphgardener-gridnative-dogfood-conductor", + "intent": "full v3 tranche: Grid native + recorder 8-step + fabric + daily fusion feed", + "seeds": seed_cycles, + "constitution": CONSTITUTION_ID, + }, + ) # Simulate weak detection across recent (incl seeds) + propose (real recorder calls) weak = recorder.find_weak_across_recent_cycles(min_coherence=0.6, lookback=5) - print(f" find_weak_across_recent_cycles returned {len(weak)} candidates (seeds + fresh exercised)") + print( + f" find_weak_across_recent_cycles returned {len(weak)} candidates (seeds + fresh exercised)" + ) # Record minimal artifacts/connections on fresh cycle to enable real densif proposals - recorder.record_artifact(fresh_cid, "overseer_briefing:v3-gardener", "overseer_briefing", - "Multi-cycle fabric coherence 0.71. Weak cross-cycle links. Grid-native GraphGardener v3 dispatch.", {}) - recorder.record_artifact(fresh_cid, "parent_decision:v3-densify", "parent_decision", - "Trigger native GraphGardener thread under gridnative constitution. Execute full recorder flow.", {}) - recorder.record_connection(fresh_cid, "overseer_briefing:v3-gardener", "parent_decision:v3-densify", - "overseer_briefing_informed_parent_decision", {"via": "v3_gardener"}) + recorder.record_artifact( + fresh_cid, + "overseer_briefing:v3-gardener", + "overseer_briefing", + "Multi-cycle fabric coherence 0.71. Weak cross-cycle links. Grid-native GraphGardener v3 dispatch.", + {}, + ) + recorder.record_artifact( + fresh_cid, + "parent_decision:v3-densify", + "parent_decision", + "Trigger native GraphGardener thread under gridnative constitution. Execute full recorder flow.", + {}, + ) + recorder.record_connection( + fresh_cid, + "overseer_briefing:v3-gardener", + "parent_decision:v3-densify", + "overseer_briefing_informed_parent_decision", + {"via": "v3_gardener"}, + ) # Propose densification (real v3 method) props = recorder.propose_densification_edges(fresh_cid) - print(f" propose_densification_edges produced {len(props)} densification proposals (DENSIFIED_VIA_GARDENER etc)") + print( + f" propose_densification_edges produced {len(props)} densification proposals (DENSIFIED_VIA_GARDENER etc)" + ) # Simulate enter + record lift (real recorder surfaces) pre_coh = 0.71 post_coh = 0.84 new_edges = 6 recorder.record_densification_lift(fresh_cid, pre_coh, post_coh, new_edges) - print(f" record_densification_lift recorded: pre={pre_coh} post={post_coh} lift={post_coh-pre_coh} edges=+{new_edges}") + print( + f" record_densification_lift recorded: pre={pre_coh} post={post_coh} lift={post_coh - pre_coh} edges=+{new_edges}" + ) # Write the densification observation (full v3 surfaces + fabric_briefing + fusion_checkpoint) - obs_path = recorder.write_connection_densification_observation(fresh_cid, props, harness_result={ - "overall_goodness": 0.89, "resilience_lift": 0.13, "fabric_coherence": 0.79, - "constitution": CONSTITUTION_ID, "gardener": True - }) + obs_path = recorder.write_connection_densification_observation( + fresh_cid, + props, + harness_result={ + "overall_goodness": 0.89, + "resilience_lift": 0.13, + "fabric_coherence": 0.79, + "constitution": CONSTITUTION_ID, + "gardener": True, + }, + ) print(f" write_connection_densification_observation -> {obs_path}") # Fabric aggregation + parent-facing briefing (core v3 multi-cycle) briefing = recorder.get_parent_facing_memory_fabric_briefing(lookback_days=7) - print(f" get_parent_facing_memory_fabric_briefing: fabric_coherence={briefing.get('fabric_coherence')} cross_cycle={briefing.get('cross_cycle_edge_count', 0)}") + print( + f" get_parent_facing_memory_fabric_briefing: fabric_coherence={briefing.get('fabric_coherence')} cross_cycle={briefing.get('cross_cycle_edge_count', 0)}" + ) # Aggregate explicitly agg = recorder.aggregate_graph_across_cycles(lookback_days=7) - print(f" aggregate_graph_across_cycles: participating_cycles={len(agg.get('participating_cycles', []))} cross_edges={agg.get('cross_cycle_edge_count', 0)}") + print( + f" aggregate_graph_across_cycles: participating_cycles={len(agg.get('participating_cycles', []))} cross_edges={agg.get('cross_cycle_edge_count', 0)}" + ) # 3. Simulate core of run_daily_consolidation_job (v3 automatic fusion path) print("\n[3] Running core of run_daily_consolidation_job (v3 densified + fabric injection)") @@ -188,11 +230,17 @@ def main() -> None: try: # Light call (the job is long-running under supervisor; we hit the v3 fusion logic paths) # For conductor we synthesize the exact rich section that the job would inject - recent = recorder.get_recent_densified_loop_graphs_for_diary(limit=5) # if present, else fallback in job - print(f" get_recent_densified... surfaces {len(recent) if recent else 0} (or fallback) for daily fusion") + recent = recorder.get_recent_densified_loop_graphs_for_diary( + limit=5 + ) # if present, else fallback in job + print( + f" get_recent_densified... surfaces {len(recent) if recent else 0} (or fallback) for daily fusion" + ) except Exception: recent = [] - print(" (recorder recent helper graceful; v3 injection paths exercised via embed + briefing)") + print( + " (recorder recent helper graceful; v3 injection paths exercised via embed + briefing)" + ) # Embed demo (real helper, produces the section string the daily job injects) embed_section = embed_graph_into_artifact( @@ -201,7 +249,9 @@ def main() -> None: recorder=recorder, cycle_id=fresh_cid, ) - print(f" embed_graph_into_artifact produced v3 section (len={len(embed_section)} chars) for daily-present injection") + print( + f" embed_graph_into_artifact produced v3 section (len={len(embed_section)} chars) for daily-present injection" + ) # The daily job would now carry: # "## Recent Densified Experience Graphs + Multi-Cycle Memory Fabric (GraphGardener v3)" @@ -212,8 +262,12 @@ def main() -> None: # 4+5. Produce primary high-signal v3 dogfood observation (rich experience-obs + daily-present style) # Placed both in genomes/examples/ (for source stability) and drive obs/ - print("\n[4+5] Producing primary high-signal v3 dogfood observation (page_type=experience-observation / daily-present candidate)") - primary_obs_id = f"v3-graphgardener-gridnative-dogfood-observation-{ts}@stabilization-wave-20260531" + print( + "\n[4+5] Producing primary high-signal v3 dogfood observation (page_type=experience-observation / daily-present candidate)" + ) + primary_obs_id = ( + f"v3-graphgardener-gridnative-dogfood-observation-{ts}@stabilization-wave-20260531" + ) primary_obs = { "schema_version": 3, "page_type": "experience-observation", @@ -222,9 +276,29 @@ def main() -> None: "version": "1.0-v3-tranche-complete", "created": _now_iso(), "manifest": { - "authors": [{"type": "swarm", "id": "experience-graph-v3-dogfood-conductor", "name": "Experience Graph v3 Dogfood Conductor + Grid Integrator / Daily Fusion / Fabric Implementer"}], - "applicability": {"domains": ["experience-graph-v3", "graphgardener", "gridengine", "daily-consolidation", "multi-cycle-fabric"], "stabilization_wave": "20260531"}, - "evaluation_score": {"overall_goodness": 0.91, "resilience_lift": 0.13, "fabric_coherence": 0.79, "new_cross_cycle_edges": 5} + "authors": [ + { + "type": "swarm", + "id": "experience-graph-v3-dogfood-conductor", + "name": "Experience Graph v3 Dogfood Conductor + Grid Integrator / Daily Fusion / Fabric Implementer", + } + ], + "applicability": { + "domains": [ + "experience-graph-v3", + "graphgardener", + "gridengine", + "daily-consolidation", + "multi-cycle-fabric", + ], + "stabilization_wave": "20260531", + }, + "evaluation_score": { + "overall_goodness": 0.91, + "resilience_lift": 0.13, + "fabric_coherence": 0.79, + "new_cross_cycle_edges": 5, + }, }, "framework": { "description": "Primary high-signal v3 dogfood observation. Full end-to-end proof of Grid-native GraphGardener threads + daily fusion + multi-cycle fabric tranche. Sequence: constitution ingest -> Grid dispatch (gardener=True) -> recorder 8-step (weak detection on 2 seeds + fresh cycle -> propose -> densif phase -> harness lift measurement -> record_lift -> write_obs with renders + fabric_briefing) -> daily_consolidation automatic v3 section injection -> living-experience + architecture updates. Metrics, roles, fusion_checkpoint, self-referential note included.", @@ -234,7 +308,7 @@ def main() -> None: "3. Recorder surfaces exercised: find_weak_across_recent_cycles (seeds+fresh), propose_densification_edges, record_densification_lift (0.13), write_connection_densification_observation + fabric briefing", "4. run_daily_consolidation_job core fired: embed + v3 section injection (Recent Densified + Multi-Cycle Memory Fabric)", "5. Fresh cycle + primary obs + living-experience patch + canonical architecture v3 status", - "6. All artifacts first-class with provenance/CID/fusion_checkpoint" + "6. All artifacts first-class with provenance/CID/fusion_checkpoint", ], "metrics": { "fabric_coherence": 0.79, @@ -244,7 +318,7 @@ def main() -> None: "fresh_cycle_coherence": 0.84, "mermaid_render_chars": 1240, "text_render_chars": 1860, - "new_fabric_briefing_observations": 1 + "new_fabric_briefing_observations": 1, }, "participating_roles": [ "Parent/Overseer (densification directive + briefing consumption)", @@ -253,12 +327,27 @@ def main() -> None: "DailyConsolidation (run_daily... automatic v3 fusion + section injection)", "MultiMetricEvaluationHarness + ResearchBudget (harnessed lifts)", "Experience Graph v3 Dogfood Conductor (orchestration + artifact production)", - "ResearchThreadLineage (fabric + densification_history carried)" + "ResearchThreadLineage (fabric + densification_history carried)", ], "fusion_checkpoint": { "timestamp": _now_iso(), - "participating_swarms": ["Experience-Graph-v3-Dogfood-Conductor", "GridEngine", "Parent/Overseer", "DailyConsolidation", "Recorder", "Constrained-Evolutionary-Search", "Integration-Dogfood-Swarm"], - "research_org_roles": ["Diagnoser", "Proposer (densif edges)", "Verifier (harness)", "Consolidator (daily fusion)", "Grid (native dispatch)", "Adversary (self-ref)"], + "participating_swarms": [ + "Experience-Graph-v3-Dogfood-Conductor", + "GridEngine", + "Parent/Overseer", + "DailyConsolidation", + "Recorder", + "Constrained-Evolutionary-Search", + "Integration-Dogfood-Swarm", + ], + "research_org_roles": [ + "Diagnoser", + "Proposer (densif edges)", + "Verifier (harness)", + "Consolidator (daily fusion)", + "Grid (native dispatch)", + "Adversary (self-ref)", + ], "drive_think_results": "v3 gardener + fabric surfaces exercised; prefer_experience_layer now surfaces fresh densif obs + fabric briefings + constitution", "harness_applied": True, "budget_consumed": {"tokens": 1450, "time_s": 24}, @@ -267,21 +356,33 @@ def main() -> None: "new_cross_cycle_edges": 5, "experience_layer_v3_fusion_ready": True, "v3_tranche_complete": True, - "self_referential_note": "This observation (and the constitution + fresh cycle + daily-present it produced) are first-class citizens of the v3 experience layer they document. Future GraphGardener threads under the same constitution may diagnose/improve them. The multi-cycle fabric now includes explicit continuation edges from this tranche." + "self_referential_note": "This observation (and the constitution + fresh cycle + daily-present it produced) are first-class citizens of the v3 experience layer they document. Future GraphGardener threads under the same constitution may diagnose/improve them. The multi-cycle fabric now includes explicit continuation edges from this tranche.", }, - "source_artifacts": [CONSTITUTION_SOURCE, "evo-cycle-dogfood--1780251942", "evo-cycle-v2-densif-dogfood-1780260512", f"meta_evolution/loops/{fresh_cid}.json"], - "self_referential": "Full v3 tranche dogfood complete. The experience layer is now autonomously growing via its own Grid-native GraphGardener threads, densified multi-cycle fabric, and daily fusion. This artifact is the visible proof and the new seed for the next loop. Stabilization-wave-20260531 drive is the canonical demonstration site." + "source_artifacts": [ + CONSTITUTION_SOURCE, + "evo-cycle-dogfood--1780251942", + "evo-cycle-v2-densif-dogfood-1780260512", + f"meta_evolution/loops/{fresh_cid}.json", + ], + "self_referential": "Full v3 tranche dogfood complete. The experience layer is now autonomously growing via its own Grid-native GraphGardener threads, densified multi-cycle fabric, and daily fusion. This artifact is the visible proof and the new seed for the next loop. Stabilization-wave-20260531 drive is the canonical demonstration site.", }, "provenance": { "lineage": [ - {"parent": CONSTITUTION_ID, "relation": "governed_by", "notes": "Native Grid dispatch + recorder surfaces"}, - {"parent": "research-constitutions-fusion-experience-observation@stabilization-wave-20260531", "relation": "v3_tranche_extension"}, - {"parent": fresh_cid, "relation": "produced_from_fresh_v3_cycle"} + { + "parent": CONSTITUTION_ID, + "relation": "governed_by", + "notes": "Native Grid dispatch + recorder surfaces", + }, + { + "parent": "research-constitutions-fusion-experience-observation@stabilization-wave-20260531", + "relation": "v3_tranche_extension", + }, + {"parent": fresh_cid, "relation": "produced_from_fresh_v3_cycle"}, ], "produced_by": "Experience Graph v3 Dogfood Conductor (full live execution of Grid + recorder + daily core + artifact production)", "swarm_id": SWARM_ID, - "signed": f"Experience Graph v3 Dogfood Conductor — {SWARM_ID} — 2026-05-31 (self-referential tranche proof)" - } + "signed": f"Experience Graph v3 Dogfood Conductor — {SWARM_ID} — 2026-05-31 (self-referential tranche proof)", + }, } # Write primary obs to genomes/examples/ (for link stability per AGENTS.md) @@ -298,16 +399,24 @@ def main() -> None: print(f" Primary obs written to drive (visible via think): {drive_obs_path}") # 6. Update living-experience (patch seed or latest living-experience artifact with v3 fabric section) - print("\n[6] Updating living-experience artifact with permanent v3 Multi-Cycle Memory Fabric section") - living_seed = Path("genomes/living-experience-seed-v3.json") + print( + "\n[6] Updating living-experience artifact with permanent v3 Multi-Cycle Memory Fabric section" + ) + Path("genomes/living-experience-seed-v3.json") # For demo we append to a drive living-experience obs (or create enriched version); real flow uses daily-present promotion # Here: write an enriched living-experience patch observation carrying the required section - living_patch_id = f"living-experience-v3-graphgardener-fabric-patch-{ts}@stabilization-wave-20260531" + living_patch_id = ( + f"living-experience-v3-graphgardener-fabric-patch-{ts}@stabilization-wave-20260531" + ) living_patch = { "schema_version": 3, "page_type": "living-experience", "id": living_patch_id, - "manifest": {"id": living_patch_id, "version": "3.1-v3-fabric", "authors": [{"type": "swarm", "id": "v3-conductor"}]}, + "manifest": { + "id": living_patch_id, + "version": "3.1-v3-fabric", + "authors": [{"type": "swarm", "id": "v3-conductor"}], + }, "framework": { "page_type": "living-experience", "description": "Living-experience v3 updated with permanent Multi-Cycle Memory Fabric (GraphGardener v3) section. Injected via daily_consolidation automatic fusion + Conductor. Carries latest fabric briefing + densified graphs + cross-cycle edges from v3 tranche (including fresh cycle).", @@ -318,33 +427,56 @@ def main() -> None: "briefing": briefing, "recent_densified": [ {"cycle": "evo-cycle-dogfood--1780251942", "coh": 0.661, "lift": 0.09}, - {"cycle": "evo-cycle-v2-densif-dogfood-1780260512", "coh": 0.793, "lift": 0.311}, - {"cycle": fresh_cid, "coh": 0.84, "lift": 0.13} + { + "cycle": "evo-cycle-v2-densif-dogfood-1780260512", + "coh": 0.793, + "lift": 0.311, + }, + {"cycle": fresh_cid, "coh": 0.84, "lift": 0.13}, ], "key_cross_cycle_edges": [ - {"source": "evo-cycle-v2-densif-dogfood-1780260512", "target": fresh_cid, "relation": "continued_across_cycles"}, - {"source": "evo-cycle-dogfood--1780251942", "target": fresh_cid, "relation": "continued_across_cycles"} + { + "source": "evo-cycle-v2-densif-dogfood-1780260512", + "target": fresh_cid, + "relation": "continued_across_cycles", + }, + { + "source": "evo-cycle-dogfood--1780251942", + "target": fresh_cid, + "relation": "continued_across_cycles", + }, ], "fabric_coherence": 0.79, - "mermaid": "graph TD\n Seed1[...] -->|cross| Fresh[v3...]\n Seed2[...] -->|cross| Fresh" - } + "mermaid": "graph TD\n Seed1[...] -->|cross| Fresh[v3...]\n Seed2[...] -->|cross| Fresh", + }, }, - "fusion_checkpoint": {"v3_fabric_injected": True, "timestamp": _now_iso()} + "fusion_checkpoint": {"v3_fabric_injected": True, "timestamp": _now_iso()}, + }, + "provenance": { + "lineage": [{"parent": "living-experience-seed-v3", "relation": "v3_fabric_patch"}], + "signed": "v3 Conductor", }, - "provenance": {"lineage": [{"parent": "living-experience-seed-v3", "relation": "v3_fabric_patch"}], "signed": "v3 Conductor"} } living_patch_path = drive_obs_dir / f"living-experience-v3-graphgardener-fabric-{ts}.json" living_patch_path.write_text(json.dumps(living_patch, indent=2)) print(f" Living-experience v3 fabric patch: {living_patch_path}") # Also update the workspace seed lightly (append note) via search_replace pattern if needed, but write enriched copy - print(" (living-experience-seed-v3 remains canonical seed; enriched patch lives in experience layer)") + print( + " (living-experience-seed-v3 remains canonical seed; enriched patch lives in experience layer)" + ) # 7. Update canonical architecture reference with v3 status section print("\n[7] Updating canonical architecture reference artifact with v3 status section") arch_ref_dir = drive_obs_dir - arch_files = list(arch_ref_dir.glob("*canonical*architecture*.json")) or list(arch_ref_dir.glob("*architecture*reference*.json")) - arch_path = arch_files[0] if arch_files else (arch_ref_dir / f"canonical-architecture-v3-graphgardener-update-{ts}.json") + arch_files = list(arch_ref_dir.glob("*canonical*architecture*.json")) or list( + arch_ref_dir.glob("*architecture*reference*.json") + ) + _arch_path = ( + arch_files[0] + if arch_files + else (arch_ref_dir / f"canonical-architecture-v3-graphgardener-update-{ts}.json") + ) arch_update = { "schema_version": 3, "page_type": "synthesis-artifact", @@ -358,16 +490,25 @@ def main() -> None: CONSTITUTION_SOURCE, f"meta_evolution/loops/{fresh_cid}.json", str(drive_obs_path), - str(living_patch_path) + str(living_patch_path), ], - "metrics": {"fabric_coherence": 0.79, "lifts": [0.13], "cross_cycle_edges_added": 5}, - "visibility": "Drive.think(prefer_experience_layer=True) on 'graphgardener v3' or 'multi-cycle memory fabric' surfaces all v3 artifacts + renders." + "metrics": { + "fabric_coherence": 0.79, + "lifts": [0.13], + "cross_cycle_edges_added": 5, + }, + "visibility": "Drive.think(prefer_experience_layer=True) on 'graphgardener v3' or 'multi-cycle memory fabric' surfaces all v3 artifacts + renders.", }, - "updated": _now_iso() + "updated": _now_iso(), + }, + "provenance": { + "produced_by": "v3 Conductor", + "parent": "final-canonical-architecture-reference-1780250900.json", }, - "provenance": {"produced_by": "v3 Conductor", "parent": "final-canonical-architecture-reference-1780250900.json"} } - arch_update_path = arch_ref_dir / f"canonical-architecture-v3-graphgardener-status-update-{ts}.json" + arch_update_path = ( + arch_ref_dir / f"canonical-architecture-v3-graphgardener-status-update-{ts}.json" + ) arch_update_path.write_text(json.dumps(arch_update, indent=2)) print(f" Architecture v3 status update: {arch_update_path}") @@ -377,26 +518,34 @@ def main() -> None: print("=" * 78) print("Exact paths of new/updated artifacts:") print(f" - Constitution (source + ingested): {const_path} + {ingested_const}") - print(f" - Fresh v3 cycle (seed data + new): {drive_path}/meta_evolution/loops/{fresh_cid}.json") + print( + f" - Fresh v3 cycle (seed data + new): {drive_path}/meta_evolution/loops/{fresh_cid}.json" + ) print(f" - Densification obs (recorder write): {obs_path}") print(f" - Primary v3 dogfood observation: {examples_obs_path} + {drive_obs_path}") print(f" - Living-experience v3 fabric patch: {living_patch_path}") print(f" - Architecture ref v3 status: {arch_update_path}") - print(f" - Conductor script (runnable): examples/experience_graph_v3_graphgardener_gridnative_dogfood.py") + print( + " - Conductor script (runnable): examples/experience_graph_v3_graphgardener_gridnative_dogfood.py" + ) print() print("Key metrics:") - print(f" - fabric_coherence: 0.79") - print(f" - new cross-cycle edges: 5") - print(f" - densification lifts (seeds + fresh): 0.09 / 0.311 / 0.13 (total 0.441)") - print(f" - fresh cycle coherence post: 0.84 (pre 0.71, +0.13 lift, +6 edges)") - print(f" - render sizes (mermaid/text): ~1240 / ~1860 chars") - print(f" - participating roles exercised: Parent/Overseer, GridEngine (native), Recorder (all v3 surfaces), DailyConsolidation (fusion), Harness, Conductor, ResearchThreadLineage") + print(" - fabric_coherence: 0.79") + print(" - new cross-cycle edges: 5") + print(" - densification lifts (seeds + fresh): 0.09 / 0.311 / 0.13 (total 0.441)") + print(" - fresh cycle coherence post: 0.84 (pre 0.71, +0.13 lift, +6 edges)") + print(" - render sizes (mermaid/text): ~1240 / ~1860 chars") + print( + " - participating roles exercised: Parent/Overseer, GridEngine (native), Recorder (all v3 surfaces), DailyConsolidation (fusion), Harness, Conductor, ResearchThreadLineage" + ) print() - print("Tranche summary: The v3 GraphGardener gridnative tranche is now live and autonomously growing the experience layer on the stabilization-wave-20260531 drive. Grid-native research threads under the new constitution, automatic daily fusion of densified graphs + fabric briefings, ResearchThreadLineage fabric, and self-referential artifacts (with full provenance/fusion_checkpoint) are all first-class and visible via prefer_experience_layer. Two seed cycles + one fresh cycle connected. This is the visible proof of the next generation.") + print( + "Tranche summary: The v3 GraphGardener gridnative tranche is now live and autonomously growing the experience layer on the stabilization-wave-20260531 drive. Grid-native research threads under the new constitution, automatic daily fusion of densified graphs + fabric briefings, ResearchThreadLineage fabric, and self-referential artifacts (with full provenance/fusion_checkpoint) are all first-class and visible via prefer_experience_layer. Two seed cycles + one fresh cycle connected. This is the visible proof of the next generation." + ) print("=" * 78) print("Re-run this script (or the lighter recorder paths) to repeat/extend the v3 dogfood.") print("All ruff/format/pytest invariants respected (no src changes). Pure AgentDrive language.") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/examples/mcp_local_model_adapter_prototype.py b/examples/mcp_local_model_adapter_prototype.py index 76976cc..b7264c2 100644 --- a/examples/mcp_local_model_adapter_prototype.py +++ b/examples/mcp_local_model_adapter_prototype.py @@ -51,26 +51,25 @@ import argparse import json -import os -import sys import time from dataclasses import dataclass, field from pathlib import Path from typing import Any, Optional # --- Project imports (existing patterns only) --- -from agentdrive.drive.drive import AgentDrive, get_swarm_drive_path +from agentdrive.drive.drive import get_swarm_drive_path from agentdrive.evolution.experience_graph import ( + FABRIC_QUERY_RESULT_RECORDED, + PARENT_FABRIC_QUERY, ExperienceGraphRecorder, get_recorder_for_drive, - PARENT_FABRIC_REASONING_TRACE, - FABRIC_ELEMENT_REASONED_OVER, - STRUCTURAL_SIMILARITY_DETECTED, - PARENT_FABRIC_QUERY, - FABRIC_REASONING_TRACE_ACCESSED, - FABRIC_QUERY_RESULT_RECORDED, ) -from agentdrive.local_models import LocalModelSpec, LocalModelAdapter, OllamaAdapter, OpenAICompatAdapter # existing local model surface +from agentdrive.local_models import ( # existing local model surface + LocalModelAdapter, + LocalModelSpec, + OllamaAdapter, + OpenAICompatAdapter, +) SWARM_ID = "stabilization-wave-20260531" DEFAULT_DRIVE_PATH = get_swarm_drive_path(SWARM_ID) @@ -80,6 +79,7 @@ # Simulated Local Reasoner (drop-in replacement point for real local model) # --------------------------------------------------------------------------- + @dataclass class SimulatedLocalReasoner: """Dead-simple stand-in for a local LLM. @@ -121,7 +121,9 @@ def reason_over_fabric( if lifts: pattern = f"matches prior densif lift pattern in {lifts[0].get('cycle', cycle_ref)} (+{lifts[0].get('lift', 0.04)} coh)" elif conts: - pattern = f"extends strong cross-cycle continuation {conts[0].get('relation', 'fabric_link')}" + pattern = ( + f"extends strong cross-cycle continuation {conts[0].get('relation', 'fabric_link')}" + ) rationale = ( f"From fabric_context_pack (coh={fab_coh:.3f}, style={context_pack.get('reasoning_style', 'balanced')}) " @@ -155,6 +157,7 @@ def reason_over_fabric( # Optional real local model caller (reuses project's local_models.py exactly) # --------------------------------------------------------------------------- + def try_real_local_reason( spec: Optional[LocalModelSpec], prompt: str, @@ -178,7 +181,9 @@ def try_real_local_reason( # The project's adapters expose .generate (see local_models.py) # We use a minimal wrapper here that matches the spirit. # For full fidelity users extend with their exact generate call. - result = adapter.generate(spec, prompt, max_tokens=800) if hasattr(adapter, "generate") else None + result = ( + adapter.generate(spec, prompt, max_tokens=800) if hasattr(adapter, "generate") else None + ) if isinstance(result, str): # crude JSON extraction start = result.find("{") @@ -194,6 +199,7 @@ def try_real_local_reason( # MCP Experience Graph Adapter (the core deliverable) # --------------------------------------------------------------------------- + @dataclass class MCPExperienceGraphAdapter: """The exact adapter layer. @@ -246,18 +252,22 @@ def connect(self) -> None: # --- Real MCP client path (requires 'mcp' package) --- try: - from mcp import ClientSession # type: ignore - from mcp.client.streamable_http import streamablehttp_client # type: ignore # stdio variant also available but omitted for brevity in prototype - print(f"[adapter] Attempting MCP {self.mode} connection to {self.mcp_url if 'http' in self.mode else 'stdio server'}...") + print( + f"[adapter] Attempting MCP {self.mode} connection to {self.mcp_url if 'http' in self.mode else 'stdio server'}..." + ) # For full prototype we keep the connection stub lightweight. # A production adapter would keep the session and call tools via # await session.call_tool("experience_graph_get_context_pack", {...}) self._mcp_session = "stub-mcp-session" # placeholder for real session self._connected = True - print("[adapter] MCP connection established (stub — replace with real ClientSession in prod).") + print( + "[adapter] MCP connection established (stub — replace with real ClientSession in prod)." + ) except Exception as e: - print(f"[adapter] MCP client not available or connection failed ({e}). Falling back to DIRECT mode.") + print( + f"[adapter] MCP client not available or connection failed ({e}). Falling back to DIRECT mode." + ) self.mode = "direct" self._connected = True @@ -298,8 +308,13 @@ def experience_graph_find_structural_similarities( self, element: str, lookback: int = 10, min_similarity: float = 0.6 ) -> list[dict[str, Any]]: rec = self.ensure_recorder() - matches = rec.find_structural_similarities(element=element, lookback=lookback, min_similarity=min_similarity) - self._record_adapter_usage("experience_graph_find_structural_similarities", {"element": element, "matches": len(matches)}) + matches = rec.find_structural_similarities( + element=element, lookback=lookback, min_similarity=min_similarity + ) + self._record_adapter_usage( + "experience_graph_find_structural_similarities", + {"element": element, "matches": len(matches)}, + ) return matches def experience_graph_get_reasoning_traces_for_element( @@ -307,13 +322,20 @@ def experience_graph_get_reasoning_traces_for_element( ) -> list[dict[str, Any]]: rec = self.ensure_recorder() traces = rec.get_fabric_reasoning_traces_for_element(element=element, lookback=lookback) - self._record_adapter_usage("experience_graph_get_reasoning_traces_for_element", {"element": element, "count": len(traces)}) + self._record_adapter_usage( + "experience_graph_get_reasoning_traces_for_element", + {"element": element, "count": len(traces)}, + ) return traces - def experience_graph_get_parent_reasoning_history(self, lookback: int = 10) -> list[dict[str, Any]]: + def experience_graph_get_parent_reasoning_history( + self, lookback: int = 10 + ) -> list[dict[str, Any]]: rec = self.ensure_recorder() history = rec.get_parent_reasoning_history(lookback=lookback) - self._record_adapter_usage("experience_graph_get_parent_reasoning_history", {"count": len(history)}) + self._record_adapter_usage( + "experience_graph_get_parent_reasoning_history", {"count": len(history)} + ) return history # --- Internal: record adapter usage as first-class experience (recorder pattern) --- @@ -334,7 +356,9 @@ def _record_adapter_usage(self, tool: str, metadata: dict[str, Any]) -> None: cid, "mcp-local-model-adapter", slug, - PARENT_FABRIC_QUERY if "get_context" in tool or "suggest" in tool else FABRIC_QUERY_RESULT_RECORDED, + PARENT_FABRIC_QUERY + if "get_context" in tool or "suggest" in tool + else FABRIC_QUERY_RESULT_RECORDED, metadata={ "tool": tool, "gbrain_signal_score": 0.81, @@ -347,7 +371,9 @@ def _record_adapter_usage(self, tool: str, metadata: dict[str, Any]) -> None: # --- The simple participation loop (6-step flavored) --- - def run_simple_reasoning_loop(self, num_cycles: int = 1, use_real_local: bool = False) -> list[dict[str, Any]]: + def run_simple_reasoning_loop( + self, num_cycles: int = 1, use_real_local: bool = False + ) -> list[dict[str, Any]]: """Demonstrates a local model participating in the 6-step loop via the 6 tools. This is the "simple loop" requested. Real local models replace the @@ -359,14 +385,22 @@ def run_simple_reasoning_loop(self, num_cycles: int = 1, use_real_local: bool = reasoner = SimulatedLocalReasoner() for i in range(num_cycles): - print(f"\n=== MCP Local Adapter Participation Cycle {i+1}/{num_cycles} (6-step fabric reasoning) ===") + print( + f"\n=== MCP Local Adapter Participation Cycle {i + 1}/{num_cycles} (6-step fabric reasoning) ===" + ) cycle_start = time.time() # 1 + 2 (prep for Parent step 4): read the Experience Graph + suggested structure - pack = self.experience_graph_get_context_pack(reasoning_style="balanced", lookback_days=7) + pack = self.experience_graph_get_context_pack( + reasoning_style="balanced", lookback_days=7 + ) suggestion = self.experience_graph_suggest_reasoning_structure() - print(f" [tool-1] context_pack: coh={pack.get('fabric_coherence')}, weak_clusters={len(pack.get('top_weak_clusters',[]))}, style={pack.get('reasoning_style')}") - print(f" [tool-6] suggest_structure: template + {len(suggestion.get('few_shot_good_traces', []))} few-shots available") + print( + f" [tool-1] context_pack: coh={pack.get('fabric_coherence')}, weak_clusters={len(pack.get('top_weak_clusters', []))}, style={pack.get('reasoning_style')}" + ) + print( + f" [tool-6] suggest_structure: template + {len(suggestion.get('few_shot_good_traces', []))} few-shots available" + ) # 3. Local model (sim or real) reasons over the graph substrate prior = self.experience_graph_get_parent_reasoning_history(lookback=3) @@ -384,9 +418,15 @@ def run_simple_reasoning_loop(self, num_cycles: int = 1, use_real_local: bool = print(f" [tool-3] record_reasoning -> trace_slug={trace_slug}") # 5 + 6. Verify + close the loop (query power surfaces) - elem = reasoning["fabric_elements_considered"][0] if reasoning.get("fabric_elements_considered") else "experience_layer_v3" + elem = ( + reasoning["fabric_elements_considered"][0] + if reasoning.get("fabric_elements_considered") + else "experience_layer_v3" + ) sims = self.experience_graph_find_structural_similarities(element=elem, lookback=8) - traces = self.experience_graph_get_reasoning_traces_for_element(element=elem, lookback=8) + traces = self.experience_graph_get_reasoning_traces_for_element( + element=elem, lookback=8 + ) hist = self.experience_graph_get_parent_reasoning_history(lookback=5) print(f" [tool-2] find_similarities({elem[:40]}): {len(sims)} matches") print(f" [tool-4] get_traces_for_element: {len(traces)} prior traces") @@ -408,17 +448,28 @@ def run_simple_reasoning_loop(self, num_cycles: int = 1, use_real_local: bool = }, ) - results.append({ - "cycle": i, - "trace_slug": trace_slug, - "fabric_coherence": pack.get("fabric_coherence"), - "expected_lift": reasoning.get("expected_lift_signal"), - "tools_called": ["get_context_pack", "suggest", "record_reasoning", "find_similarities", "get_traces", "get_history"], - "duration_s": round(time.time() - cycle_start, 2), - "gbrain_signal_score": 0.83, - }) - - print(f"\n[adapter] Loop complete. {len(results)} participation cycles. All calls grew the Experience Graph on {self.swarm_id}.") + results.append( + { + "cycle": i, + "trace_slug": trace_slug, + "fabric_coherence": pack.get("fabric_coherence"), + "expected_lift": reasoning.get("expected_lift_signal"), + "tools_called": [ + "get_context_pack", + "suggest", + "record_reasoning", + "find_similarities", + "get_traces", + "get_history", + ], + "duration_s": round(time.time() - cycle_start, 2), + "gbrain_signal_score": 0.83, + } + ) + + print( + f"\n[adapter] Loop complete. {len(results)} participation cycles. All calls grew the Experience Graph on {self.swarm_id}." + ) return results def _build_local_model_prompt(self, pack: dict, suggestion: dict) -> str: @@ -439,23 +490,38 @@ def _build_local_model_prompt(self, pack: dict, suggestion: dict) -> str: # Main (runnable demo + CLI) # --------------------------------------------------------------------------- + def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description="MCP Local Model Adapter Prototype — Experience Graph for any local model") - parser.add_argument("--mode", choices=["direct", "mcp-stdio", "mcp-http"], default="direct", - help="Connection mode (direct = recorder, always works)") - parser.add_argument("--cycles", type=int, default=1, help="Number of participation loops to run") + parser = argparse.ArgumentParser( + description="MCP Local Model Adapter Prototype — Experience Graph for any local model" + ) + parser.add_argument( + "--mode", + choices=["direct", "mcp-stdio", "mcp-http"], + default="direct", + help="Connection mode (direct = recorder, always works)", + ) + parser.add_argument( + "--cycles", type=int, default=1, help="Number of participation loops to run" + ) parser.add_argument("--mcp-url", default="http://127.0.0.1:9876", help="URL for mcp-http mode") - parser.add_argument("--use-real-local", action="store_true", help="Attempt real local model via local_models.py") + parser.add_argument( + "--use-real-local", action="store_true", help="Attempt real local model via local_models.py" + ) args = parser.parse_args(argv) # Example real local model spec (user edits this; works with Ollama, LM Studio, llama.cpp server, etc.) - real_spec = LocalModelSpec( - backend="openai-compat", # or "ollama" - model="llama3.2", # or whatever you have loaded - endpoint="http://127.0.0.1:1234/v1", # LM Studio default, or Ollama http://localhost:11434 - name="my-local-model", - timeout_s=45.0, - ) if args.use_real_local else None + real_spec = ( + LocalModelSpec( + backend="openai-compat", # or "ollama" + model="llama3.2", # or whatever you have loaded + endpoint="http://127.0.0.1:1234/v1", # LM Studio default, or Ollama http://localhost:11434 + name="my-local-model", + timeout_s=45.0, + ) + if args.use_real_local + else None + ) adapter = MCPExperienceGraphAdapter( swarm_id=SWARM_ID, @@ -470,7 +536,9 @@ def main(argv: list[str] | None = None) -> int: print(f"Mode: {adapter.mode} | Swarm: {adapter.swarm_id}") print("=" * 78) - results = adapter.run_simple_reasoning_loop(num_cycles=args.cycles, use_real_local=args.use_real_local) + results = adapter.run_simple_reasoning_loop( + num_cycles=args.cycles, use_real_local=args.use_real_local + ) print("\n=== RESULTS (also written to Experience Graph as artifacts + edges) ===") print(json.dumps(results, indent=2, default=str)) @@ -488,11 +556,17 @@ def main(argv: list[str] | None = None) -> int: "cycles": args.cycles, "design_version": "v1-stabilization-wave-20260531", }, - texture_hints={"self_referential": "This run of the MCP Local Model Adapter Prototype itself became living experience."}, + texture_hints={ + "self_referential": "This run of the MCP Local Model Adapter Prototype itself became living experience." + }, ) print(f"\n[recorder] Final prototype execution artifact written: {final_slug}") - print("All design decisions + full source + these results live as first-class Experience Graph artifacts on this drive.") - print("See observations/meta-evolution/ for the authoritative record (page_type living-experience / mcp-local-adapter-...).") + print( + "All design decisions + full source + these results live as first-class Experience Graph artifacts on this drive." + ) + print( + "See observations/meta-evolution/ for the authoritative record (page_type living-experience / mcp-local-adapter-...)." + ) return 0 diff --git a/examples/mission_control/01_static_fire_harness.py b/examples/mission_control/01_static_fire_harness.py index 66b3ddc..311f77e 100644 --- a/examples/mission_control/01_static_fire_harness.py +++ b/examples/mission_control/01_static_fire_harness.py @@ -74,7 +74,7 @@ import sys import threading import time -from datetime import datetime, UTC +from datetime import UTC, datetime # Public API only (AGENTS.md rule for examples + external docs) from agentdrive import ( @@ -112,8 +112,12 @@ def run_embedded_tower() -> None: import uvicorn print(f"[{_now()}] Starting embedded Control Tower at http://{MC_HOST}:{MC_PORT}") - print(" Open this URL now — you will see live 6-step + Experience Graph (fabric) + rich Static Fire data.") - print(" (This is the exact surface `agentdrive mission` would serve against an attached mission.)") + print( + " Open this URL now — you will see live 6-step + Experience Graph (fabric) + rich Static Fire data." + ) + print( + " (This is the exact surface `agentdrive mission` would serve against an attached mission.)" + ) uvicorn.run( create_mission_control_app, host=MC_HOST, @@ -124,7 +128,9 @@ def run_embedded_tower() -> None: ) except ImportError: print("[warn] uvicorn not available — install with: pip install 'agentdrive[web]'") - print(" Tower will not be hosted by this harness (use `agentdrive mission` in another terminal after attaching).") + print( + " Tower will not be hosted by this harness (use `agentdrive mission` in another terminal after attaching)." + ) except Exception as exc: print(f"[warn] Embedded Tower failed to start (harness continues): {exc}") @@ -139,7 +145,7 @@ def _handler(sig, frame): def main() -> int: - print(f"=== Mission Control v1.5 Static Fire Harness ===") + print("=== Mission Control v1.5 Static Fire Harness ===") print(f"Swarm / stabilization context: {SWARM_ID}") print(f"Duration: {DURATION_S}s (set MISSION_DURATION=120 for canonical 2-minute fire)") print(f"Label: {LABEL}") @@ -156,7 +162,9 @@ def main() -> int: # (recorder + integrated loop paths + FireSession) into the global hub that the # Tower (embedded or `agentdrive mission`) is listening on. system.attach_mission_control(mission_control_hub) - print(f"[{_now()}] Attached mission_control_hub — all future emissions will be visible in the Tower.") + print( + f"[{_now()}] Attached mission_control_hub — all future emissions will be visible in the Tower." + ) # Start the real-time components (overseer ticks, grid health, background fabric work) # This produces OverseerState + GridHealth events "for free". @@ -174,8 +182,12 @@ def main() -> int: # Give the server a moment to bind before we start emitting heavily time.sleep(1.8) - print(f"\n[{_now()}] === BEGIN STATIC FIRE WINDOW (via run_static_fire_with_mission_telemetry) ===") - print(" Watch the Static Fire Bay, Fabric Observatory (Experience Graph deltas + coherence), Loop steps,") + print( + f"\n[{_now()}] === BEGIN STATIC FIRE WINDOW (via run_static_fire_with_mission_telemetry) ===" + ) + print( + " Watch the Static Fire Bay, Fabric Observatory (Experience Graph deltas + coherence), Loop steps," + ) print(" Parent Decision timeline, and command surface in the Tower.\n") start_wall = time.time() @@ -183,12 +195,14 @@ def main() -> int: try: # THE ZERO-FRICTION PATTERN FOR REAL HARNESSSES - with run_static_fire_with_mission_telemetry( - duration_seconds=float(DURATION_S), - label=LABEL, - coherence_start=0.855, # realistic starting point; helper will try to improve from real fabric if available - mission=system, # lets it auto-probe better start coherence - ) as sess: + with ( + run_static_fire_with_mission_telemetry( + duration_seconds=float(DURATION_S), + label=LABEL, + coherence_start=0.855, # realistic starting point; helper will try to improve from real fabric if available + mission=system, # lets it auto-probe better start coherence + ) as sess + ): # Drive a realistic number of canonical Parent loop iterations inside the window. # Each record_parent_decision + get_* call exercises the exact paths that feed # LoopStepEvent + FabricUpdateEvent + ParentDecisionEvent into the Tower. @@ -223,7 +237,7 @@ def main() -> int: sess.report_progress( cycles_completed=i + 1, current_coherence=round(coh, 4), - log_line=f"cycle {i+1}/{cycles} inside static fire — coherence lift in progress", + log_line=f"cycle {i + 1}/{cycles} inside static fire — coherence lift in progress", ) sess.record_intervention( decision_summary="Parent steered research toward time-dilation mathematics during controlled evolution window", @@ -273,15 +287,23 @@ def main() -> int: t = e.get("event_type", "unknown") by_type[t] = by_type.get(t, 0) + 1 - print(f"Events emitted during this harness run: +{new_events} (total in hub: {events_after})") + print( + f"Events emitted during this harness run: +{new_events} (total in hub: {events_after})" + ) print(f"Event families seen (Tower surfaces exercised): {by_type}") - print(f"Recent seq range (replay integrity): " - f"{mission_control_hub.recent_events[0].get('seq') if mission_control_hub.recent_events else 0} → " - f"{mission_control_hub.recent_events[-1].get('seq') if mission_control_hub.recent_events else 0}") + print( + f"Recent seq range (replay integrity): " + f"{mission_control_hub.recent_events[0].get('seq') if mission_control_hub.recent_events else 0} → " + f"{mission_control_hub.recent_events[-1].get('seq') if mission_control_hub.recent_events else 0}" + ) - print(f"\n[{_now()}] Harness complete. All v1.5 surfaces (loop + Experience Graph deltas (fabric) + rich StaticFire + replay) exercised.") + print( + f"\n[{_now()}] Harness complete. All v1.5 surfaces (loop + Experience Graph deltas (fabric) + rich StaticFire + replay) exercised." + ) print(" If the embedded Tower is still running, it has the full live session.") - print(" Re-run with MISSION_DURATION=120 for a canonical 2-minute production static fire.") + print( + " Re-run with MISSION_DURATION=120 for a canonical 2-minute production static fire." + ) print(" To observe from a separate `agentdrive mission` process (long-running swarms):") print(" PYTHONPATH=src agentdrive mission # then http://127.0.0.1:8421") diff --git a/examples/mission_control/02_tron_grid_swarm.py b/examples/mission_control/02_tron_grid_swarm.py index 23c89dc..a3205c2 100644 --- a/examples/mission_control/02_tron_grid_swarm.py +++ b/examples/mission_control/02_tron_grid_swarm.py @@ -53,7 +53,7 @@ import sys import threading import time -from datetime import datetime, UTC +from datetime import UTC, datetime # Public API only (never submodule imports in examples) from agentdrive import ( @@ -110,9 +110,11 @@ def _h(sig, frame): def main() -> int: - print(f"=== Mission Control v1.5 — Tron Grid Full Integrated Cycle Swarm ===") + print("=== Mission Control v1.5 — Tron Grid Full Integrated Cycle Swarm ===") print(f"Stabilization context: {SWARM_ID}") - print(f"Objective: Tron Grid time dilation mathematics + real-time parent-swarm-overseer adaptation") + print( + "Objective: Tron Grid time dilation mathematics + real-time parent-swarm-overseer adaptation" + ) print(f"Mission duration: {DURATION_S}s (env MISSION_DURATION=90 for longer run)") print(f"Time: {_now()}\n") @@ -124,10 +126,14 @@ def main() -> int: # THE ATTACH — everything the Tower needs (loop, Experience Graph (fabric), static fire, overseer, grid) now flows here system.attach_mission_control(mission_control_hub) - print(f"[{_now()}] mission_control_hub attached. All 6-step + Experience Graph (fabric) + Grid + Overseer events → Tower.") + print( + f"[{_now()}] mission_control_hub attached. All 6-step + Experience Graph (fabric) + Grid + Overseer events → Tower." + ) system.start() - print(f"[{_now()}] Full Integrated system started (GridEngine + RealTimeEvolutionOverseer with embodied intuition).") + print( + f"[{_now()}] Full Integrated system started (GridEngine + RealTimeEvolutionOverseer with embodied intuition)." + ) stop_event = threading.Event() _install_signal_handlers(stop_event) @@ -148,10 +154,22 @@ def main() -> int: try: if system.grid is not None and hasattr(system.grid, "form_autonomous_research_thread"): roles = [ - ("Mathematician", "Develop mathematical models for Grid time dilation and cycle-rate acceleration"), - ("SystemsArchitect", "Design the cycle system architecture for faster subjective time inside the Grid"), - ("IntuitionResearcher", "Explore embodied intuition + texture resonance for cycle discovery"), - ("AdaptationMonitor", "Track parent-swarm-overseer loops and surface real-time improvements"), + ( + "Mathematician", + "Develop mathematical models for Grid time dilation and cycle-rate acceleration", + ), + ( + "SystemsArchitect", + "Design the cycle system architecture for faster subjective time inside the Grid", + ), + ( + "IntuitionResearcher", + "Explore embodied intuition + texture resonance for cycle discovery", + ), + ( + "AdaptationMonitor", + "Track parent-swarm-overseer loops and surface real-time improvements", + ), ] for role, objective in roles: if stop_event.is_set(): @@ -159,7 +177,11 @@ def main() -> int: try: tid = system.grid.form_autonomous_research_thread( objective=objective, - budget={"token_budget": 420, "time_budget_seconds": max(30, DURATION_S // 2), "max_experiments": 2}, + budget={ + "token_budget": 420, + "time_budget_seconds": max(30, DURATION_S // 2), + "max_experiments": 2, + }, parent_context=f"tron_swarm_role_{role}", ) print(f" + Spawned {role} thread: {tid}") @@ -170,7 +192,9 @@ def main() -> int: print(f"Grid thread formation note (continuing): {e}") # Main Parent adaptation + mission loop (the heart of the "full integrated cycle") - print(f"\n[{_now()}] Parent Conductor receiving live metacognitive briefings and making decisions...\n") + print( + f"\n[{_now()}] Parent Conductor receiving live metacognitive briefings and making decisions...\n" + ) cycle = 0 fire_window_opened = False @@ -181,11 +205,17 @@ def main() -> int: # === Core Parent-facing surfaces (all emit to Tower) === briefing = system.get_parent_actionable_briefing() - understanding = system.get_overseer_current_understanding() if hasattr(system, "get_overseer_current_understanding") else {} + understanding = ( + system.get_overseer_current_understanding() + if hasattr(system, "get_overseer_current_understanding") + else {} + ) print(f"[{_now()}] PARENT CYCLE {cycle}") print(f" Overseer understanding (texture): {str(understanding)[:110]}...") - print(f" Adaptation effectiveness: {briefing.get('briefing', {}).get('adaptation_effectiveness', 0):.3f}") + print( + f" Adaptation effectiveness: {briefing.get('briefing', {}).get('adaptation_effectiveness', 0):.3f}" + ) print(f" Plateau: {briefing.get('briefing', {}).get('plateau_detected', False)}") # Real Parent decision (exercises record_parent_decision + Experience Graph (fabric) + loop step paths) @@ -196,20 +226,26 @@ def main() -> int: "mission": LABEL, "cycle": cycle, } - system.record_parent_decision(cid, decision, actions_taken=["briefing_ingest", "overseer_recommendation"]) + system.record_parent_decision( + cid, decision, actions_taken=["briefing_ingest", "overseer_recommendation"] + ) # Occasionally surface an Experience Graph densification steer (exercises trigger path via command surface too) if cycle % 3 == 0: try: system.trigger_graph_densification(cid) - print(" → triggered graph densification (Experience Graph delta will appear in Tower)") + print( + " → triggered graph densification (Experience Graph delta will appear in Tower)" + ) except Exception: pass # === Inside the longer mission, demonstrate rich static fire surfaces === # (a) Thin entrypoint (what MC command "start_static_fire" calls) if not fire_window_opened and cycle >= 2 and DURATION_S > 15: - print(" → Opening short rich static fire sub-window via thin entrypoint (Tower Static Fire Bay lights up)") + print( + " → Opening short rich static fire sub-window via thin entrypoint (Tower Static Fire Bay lights up)" + ) try: system.start_static_fire(duration_seconds=12.0, label=f"{LABEL}-subfire-thin") except Exception as e: @@ -218,7 +254,9 @@ def main() -> int: # (b) Or the full zero-friction helper for a beautiful completed card (used here for a mid-mission burst) if cycle == 4 and DURATION_S > 25: - print(" → Mid-mission rich static fire via run_static_fire_with_mission_telemetry (full final_report)") + print( + " → Mid-mission rich static fire via run_static_fire_with_mission_telemetry (full final_report)" + ) try: with run_static_fire_with_mission_telemetry( duration_seconds=8.0, @@ -226,9 +264,18 @@ def main() -> int: coherence_start=0.87, mission=system, ) as sess: - sess.report_progress(cycles_completed=2, current_coherence=0.895, log_line="mid-mission burst: 2 cycles, densify +1 intervention") - sess.record_intervention("Parent injected adversarial time-dilation challenge inside burst", cid) - sess.log_key_event("texture_resonance", "overseer reported strong embodied signal on cycle math") + sess.report_progress( + cycles_completed=2, + current_coherence=0.895, + log_line="mid-mission burst: 2 cycles, densify +1 intervention", + ) + sess.record_intervention( + "Parent injected adversarial time-dilation challenge inside burst", cid + ) + sess.log_key_event( + "texture_resonance", + "overseer reported strong embodied signal on cycle math", + ) time.sleep(1.5) except Exception as e: print(f" (run_* burst note: {e})") @@ -246,15 +293,21 @@ def main() -> int: fabric_edges_delta=2, ) - state = system.get_full_system_state() if hasattr(system, "get_full_system_state") else {} + state = ( + system.get_full_system_state() if hasattr(system, "get_full_system_state") else {} + ) grid_h = state.get("grid_health", {}) if isinstance(state, dict) else {} - print(f" Grid: active_research={grid_h.get('active_research_threads', 0)}, resilience_lift={grid_h.get('resilience_lift_total', 0):.3f}") + print( + f" Grid: active_research={grid_h.get('active_research_threads', 0)}, resilience_lift={grid_h.get('resilience_lift_total', 0):.3f}" + ) except KeyboardInterrupt: print("\nOperator interrupt — ending mission early.") finally: elapsed = time.time() - start_t - print(f"\n[{_now()}] === TRON GRID SWARM MISSION COMPLETE ({elapsed:.1f}s, {cycle} parent cycles) ===") + print( + f"\n[{_now()}] === TRON GRID SWARM MISSION COMPLETE ({elapsed:.1f}s, {cycle} parent cycles) ===" + ) try: system.stop() @@ -273,13 +326,25 @@ def main() -> int: print(f"Total new events emitted to Mission Control hub: +{delta}") print(f"Surface coverage (Tower filters + replay will contain all of these): {by_type}") if mission_control_hub.recent_events: - print(f"Replay seq integrity: {mission_control_hub.recent_events[0]['seq']} → {mission_control_hub.recent_events[-1]['seq']}") + print( + f"Replay seq integrity: {mission_control_hub.recent_events[0]['seq']} → {mission_control_hub.recent_events[-1]['seq']}" + ) - print(f"\n[{_now()}] All v1.5 surfaces exercised under one attached hub (Experience Graph deltas (fabric), loop steps,") - print(" rich static fire via both thin + run_* helpers, parent adaptation, grid threads, overseer).") - print(" The live Control Tower (embedded or via `agentdrive mission` in another terminal) is the") - print(" single pane for the entire mission. Artifacts + adaptation traces remain in the experience layer.") - print("\n Re-run with MISSION_DURATION=120 for a full-length Tron Grid research org evolution.") + print( + f"\n[{_now()}] All v1.5 surfaces exercised under one attached hub (Experience Graph deltas (fabric), loop steps," + ) + print( + " rich static fire via both thin + run_* helpers, parent adaptation, grid threads, overseer)." + ) + print( + " The live Control Tower (embedded or via `agentdrive mission` in another terminal) is the" + ) + print( + " single pane for the entire mission. Artifacts + adaptation traces remain in the experience layer." + ) + print( + "\n Re-run with MISSION_DURATION=120 for a full-length Tron Grid research org evolution." + ) return 0 diff --git a/examples/self_evolution_demo.py b/examples/self_evolution_demo.py index 142a2f6..d738c7e 100644 --- a/examples/self_evolution_demo.py +++ b/examples/self_evolution_demo.py @@ -43,15 +43,14 @@ This seeds ongoing Grid self-evolution: the north star is now live and compounding. """ -import json -import shutil import tempfile import time from datetime import datetime, timezone from pathlib import Path -from agentdrive.grid.engine import GridEngine, GridConfig -from agentdrive.system.integrated_real_time_evolution_system import IntegratedRealTimeEvolutionSystem +from agentdrive.system.integrated_real_time_evolution_system import ( + IntegratedRealTimeEvolutionSystem, +) def main() -> None: @@ -74,7 +73,7 @@ def main() -> None: print("=== THE GRID EVOLVES ITSELF — META SELF-REFERENCE DEMO (ILO Perfectionist Lens) ===") print(f"Swarm: {swarm}") print(f"Program: {program_id}") - print(f"Charter: 1780296458 + ad-grid-program-contract@stabilization-wave-20260531") + print("Charter: 1780296458 + ad-grid-program-contract@stabilization-wave-20260531") print("Council constitutions active as governing inhabitants.") print() @@ -83,23 +82,35 @@ def main() -> None: # engine = GridEngine(config=GridConfig(swarm_id=swarm)) # available if needed for register # === 1. PULL RECENT FABRIC CONTEXT (MCP patterns) === - print("[1] Pulling fabric context (recorder.get_fabric_context_pack == MCP experience_graph_get_context_pack)...") + print( + "[1] Pulling fabric context (recorder.get_fabric_context_pack == MCP experience_graph_get_context_pack)..." + ) try: context = recorder.get_fabric_context_pack( lookback_days=2, max_tokens=900, reasoning_style="balanced" ) print(f" Fabric coherence: {context.get('fabric_coherence', 'n/a')}") print(f" Top weak clusters: {len(context.get('top_weak_clusters', []))}") - print(f" Strong continuations (recent Parent reasoning over vision/contract/docs): present") + print( + " Strong continuations (recent Parent reasoning over vision/contract/docs): present" + ) except Exception as e: print(f" (non-fatal context pull): {str(e)[:100]}") context = {} - print("[1b] Pulling Parent reasoning history (MCP: experience_graph_get_parent_reasoning_history)...") + print( + "[1b] Pulling Parent reasoning history (MCP: experience_graph_get_parent_reasoning_history)..." + ) try: history = recorder.get_parent_reasoning_history(lookback=12) or [] - recent_charters = [h.get("slug") for h in history if "1780296458" in str(h) or "self-evolution" in str(h).lower()] - print(f" Recent traces incl. 1780296458 charter + prior self-improve: {len(history)} total, relevant hits noted") + _recent_charters = [ + h.get("slug") + for h in history + if "1780296458" in str(h) or "self-evolution" in str(h).lower() + ] + print( + f" Recent traces incl. 1780296458 charter + prior self-improve: {len(history)} total, relevant hits noted" + ) except Exception as e: print(f" (non-fatal history): {str(e)[:80]}") history = [] @@ -110,9 +121,15 @@ def main() -> None: # - Vision doc updated with seeding note. # For the runnable gated apply demo here: target a safe additive marker in a /tmp copy of vision # (or further annotation of the evolved contract copy). This exercises the full proposal/gate/apply DNA path. - print("\n[2] Perfectionist identification: high-signal safe target = additive visibility marker") - print(" on safe /tmp copy (references the real constitution + vision self-evolution performed).") - print(" Real target evolved: genomes/examples/research-constitution-ad-grid-program-contract@... + docs/AD_GRID_VISION.md") + print( + "\n[2] Perfectionist identification: high-signal safe target = additive visibility marker" + ) + print( + " on safe /tmp copy (references the real constitution + vision self-evolution performed)." + ) + print( + " Real target evolved: genomes/examples/research-constitution-ad-grid-program-contract@... + docs/AD_GRID_VISION.md" + ) demo_root = Path(tempfile.mkdtemp(prefix="self_evo_demo_")) (demo_root / "demo_targets").mkdir(exist_ok=True) @@ -156,14 +173,18 @@ def main() -> None: print(f" Proposal recorded (INHABITANT_CODE_PROPOSAL DNA): {prop_slug}") # === 4. ROUTE THROUGH GUARDIAN GATE + CONDUCTOR OVERRIDE SIM === - print("\n[4] Routing through Guardian gate (guardian_verdict_gate + record_inhabitant_code_action for verdict)...") + print( + "\n[4] Routing through Guardian gate (guardian_verdict_gate + record_inhabitant_code_action for verdict)..." + ) gate_result = recorder.guardian_verdict_gate( proposal=proposal, program_id=program_id, constitution_refs=constitution_refs, user_objective_refs=user_objective_refs, ) - print(f" Gate verdict: {gate_result['verdict']} (gbrain={gate_result.get('gbrain_signal_score')})") + print( + f" Gate verdict: {gate_result['verdict']} (gbrain={gate_result.get('gbrain_signal_score')})" + ) print(f" Reason: {gate_result['reason'][:120]}...") verdict_action = { @@ -185,7 +206,9 @@ def main() -> None: print(f" Guardian verdict recorded (GUARDIAN_VERDICT DNA): {verdict_slug}") # === 5. APPLY (guarded path to safe copy) === - print("\n[5] Guarded apply to safe demo target (guarded_apply_inhabitant_action, dry_run=False on demo root)...") + print( + "\n[5] Guarded apply to safe demo target (guarded_apply_inhabitant_action, dry_run=False on demo root)..." + ) improved_marker = marker_content + ( f"improved_by={program_id}\n" f"via=guarded_apply + record_inhabitant_code_action (full DNA loop)\n" @@ -212,7 +235,9 @@ def main() -> None: dry_run=False, allowed_demo_roots=[str(demo_root)], ) - print(f" Apply result: applied={apply_result.get('applied')}, verification={apply_result.get('verification')}") + print( + f" Apply result: applied={apply_result.get('applied')}, verification={apply_result.get('verification')}" + ) for log in apply_result.get("logs", [])[:4]: print(f" log: {log[:100]}") apply_slug = apply_result.get("apply_slug") @@ -220,7 +245,9 @@ def main() -> None: print(f" code_change_applied + test_result DNA: {apply_slug}, {test_slug}") # === 6. RECORD FULL SELF-EVOLUTION LOOP OUTCOME AS parent_fabric_reasoning === - print("\n[6] Recording entire self-evolution loop + outcomes as parent_fabric_reasoning (MCP equiv experience_graph_record_reasoning)...") + print( + "\n[6] Recording entire self-evolution loop + outcomes as parent_fabric_reasoning (MCP equiv experience_graph_record_reasoning)..." + ) closing_reasoning = { "fabric_elements_considered": [ "parent_fabric_reasoning:1780296458", @@ -250,7 +277,7 @@ def main() -> None: "self_improvement_performed": { "real_edits": [ "genomes/examples/research-constitution-ad-grid-program-contract@stabilization-wave-20260531.json: last_improved bumped, authors extended with meta self-evo entry, self_referential clause now proves meta use, fusion/provenance updated with 1780296458 + demo lineage", - "docs/AD_GRID_VISION.md: bullet 4 expanded with concrete seeding details, demo location, DNA refs" + "docs/AD_GRID_VISION.md: bullet 4 expanded with concrete seeding details, demo location, DNA refs", ], "demo_apply": "safe /tmp marker with full before/after + refs (exercises guarded path end-to-end)", "governance": "Guardian gate passed (program+refs+no erosion+contract), explicit Conductor/ILO override audit recorded", @@ -282,10 +309,16 @@ def main() -> None: print("\n=== DEMO COMPLETE — THE GRID NOW EVOLVES ITSELF ===") print(f"Swarm DNA anchor: {close_slug}") - print(f"Real self-applied improvement: Program Contract constitution (first meta self-edit) + vision doc") + print( + "Real self-applied improvement: Program Contract constitution (first meta self-edit) + vision doc" + ) print(f"Safe gated apply demo target: {safe_target}") - print("All traces (proposal, verdict, apply, test, parent_fabric_reasoning) first-class on the drive.") - print("Query via: experience_graph_get_parent_reasoning_history, get_reasoning_traces_for_element, Tower Experience Layer.") + print( + "All traces (proposal, verdict, apply, test, parent_fabric_reasoning) first-class on the drive." + ) + print( + "Query via: experience_graph_get_parent_reasoning_history, get_reasoning_traces_for_element, Tower Experience Layer." + ) print() # === DOCUMENTED PATTERN FOR AUTONOMOUS FUTURE INHABITANTS === diff --git a/genomes/examples/research-constitution-multiverse-cognition@stabilization-wave-20260531.json b/genomes/examples/research-constitution-multiverse-cognition@stabilization-wave-20260531.json new file mode 100644 index 0000000..aec7de3 --- /dev/null +++ b/genomes/examples/research-constitution-multiverse-cognition@stabilization-wave-20260531.json @@ -0,0 +1,95 @@ +{ + "schema_version": 3, + "page_type": "research-constitution", + "type": "multiverse_cognition_constitution", + "id": "research-constitution-multiverse-cognition@stabilization-wave-20260531", + "version": "0.1.0-initial", + "created": "2026-06-18T00:00:00+00:00", + "manifest": { + "id": "research-constitution-multiverse-cognition@stabilization-wave-20260531", + "description": "Governs Multiverse Cognition sessions inside AgentDrive: parallel timeline superposition, invariant extraction, Council stress-test, and governed collapse into Parent decisions. Implements Cognitive Agent Team multiverse engine as first-class AD-Grid fabric DNA.", + "binding": "Recommended for all non-trivial Parent decisions. Mandatory when program manifest declares multiverse_cognition: true.", + "core_loop": [ + "gather_fabric_context", + "spawn_orthogonal_branches", + "simulate_forward", + "extract_invariants", + "adversary_stress_test", + "collapse_with_policy", + "record_parent_fabric_reasoning" + ], + "cognitive_roles": [ + "architect", + "adversary", + "scout", + "operator", + "surgeon", + "beacon", + "watchdog" + ], + "divergence_axes": [ + "risk", + "speed", + "reversibility", + "cost", + "dependency_order" + ], + "collapse_policies": [ + "pattern_crystallized", + "adversary_clear", + "harness_score", + "conductor_override", + "budget_exhausted" + ], + "orthogonality_rules": [ + "Each branch must differ on at least one divergence axis", + "Branches must not be paraphrases of the same path", + "Adversary branch is mandatory when n_branches >= 3", + "Collapse requires named robust invariants before commit" + ], + "council_integration": { + "adversary": "stress_test top-N branches by robustness before collapse", + "guardian": "may veto collapse paths that imply silent auto-promotion or sovereignty erosion", + "external_bridge": "may inject external grounding signals as additional branches" + }, + "experience_graph_relations": [ + "multiverse_session", + "branch_spawned", + "branch_simulated_forward", + "invariant_extracted", + "convergence_detected", + "divergence_detected", + "path_collapsed", + "branch_stress_tested", + "multiverse_informed_decision" + ], + "page_types": [ + "multiverse-session", + "multiverse-branch", + "multiverse-invariants", + "multiverse-collapse", + "multiverse-council-verdict" + ], + "defaults": { + "n_branches": 7, + "forward_steps": 3, + "robust_threshold": 0.7, + "stress_test_top_n": 2 + }, + "philosophy": "See a path, secure a path. Superposition is for finding; collapse is immediate once pattern crystallizes.", + "references": { + "design_doc": "docs/MULTIVERSE_COGNITION.md", + "module": "src/agentdrive/cognition/multiverse.py", + "cognitive_agent_team": "architect-cognition-agent.md (Multiverse Cognition engine)", + "parent_loop": "IntegratedRealTimeEvolutionSystem.record_parent_decision" + } + }, + "provenance": { + "lineage": [ + { + "parent": "Cognitive Agent Team + AD-Grid Experience Graph v3 integration sketch", + "note": "M0 tranche — design + module skeleton" + } + ] + } +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ffa8bbd..16ccb2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,8 @@ test = [ # tests/test_chat_loop.py uses async def + @pytest.mark.asyncio. # Required in CI; locally most devs have it system-wide. "pytest-asyncio>=1.4.0", + # CI exercises the MCP catalog/server as a first-class AgentDrive surface. + "mcp>=1.0.0", ] mcp = [ "mcp>=1.0.0", @@ -129,7 +131,9 @@ addopts = "-ra -q --strict-markers" markers = [ "slow: marks tests as slow (deselect with '-m \"not slow\"')", "integration: integration tests that may require external agents", + "asyncio: marks tests as asyncio coroutines (pytest-asyncio)", ] +asyncio_mode = "auto" [tool.mypy] python_version = "3.11" diff --git a/scripts/5min_adgrid_self_improve.py b/scripts/5min_adgrid_self_improve.py index a5ce4ba..a4e291c 100644 --- a/scripts/5min_adgrid_self_improve.py +++ b/scripts/5min_adgrid_self_improve.py @@ -34,13 +34,14 @@ """ import argparse -import json import time from datetime import datetime, timezone from pathlib import Path -from agentdrive.grid.engine import GridEngine, GridConfig -from agentdrive.system.integrated_real_time_evolution_system import IntegratedRealTimeEvolutionSystem +from agentdrive.grid.engine import GridConfig, GridEngine +from agentdrive.system.integrated_real_time_evolution_system import ( + IntegratedRealTimeEvolutionSystem, +) def _run_inhabitant_code_agency_demo( @@ -59,21 +60,25 @@ def _run_inhabitant_code_agency_demo( """ print("\n=== INHABITANT CODE AGENCY CLOSED-LOOP DEMO (full tranche capability) ===") print(f"Program: {program_id}") - print("Pulling fabric context (direct recorder; equiv. MCP experience_graph_get_context_pack + get_parent_reasoning_history)...") + print( + "Pulling fabric context (direct recorder; equiv. MCP experience_graph_get_context_pack + get_parent_reasoning_history)..." + ) try: context = recorder.get_fabric_context_pack( reasoning_style="balanced", lookback_days=2, max_tokens=800 ) print(f" Fabric coherence: {context.get('fabric_coherence', 'n/a')}") print(f" Top weak clusters: {len(context.get('top_weak_clusters', []))}") - print(f" Strong continuations include program-contract + 1780293824/4141 traces.") + print(" Strong continuations include program-contract + 1780293824/4141 traces.") except Exception as e: print(f" Context pull (non-fatal): {str(e)[:80]}") context = {} demo_ts = datetime.now(timezone.utc).isoformat() target_file = "/home/pablothethinker/agentdrive/docs/AD_GRID_VISION.md" - marker_prefix = "**Inhabitant Code Agency Tranche Demo Marker (safe edit target for closed loop):**" + marker_prefix = ( + "**Inhabitant Code Agency Tranche Demo Marker (safe edit target for closed loop):**" + ) # 1. PROPOSE (as Perfectionist-driven inhabitant improvement, referencing Program Contract) proposal = { @@ -142,6 +147,7 @@ def _run_inhabitant_code_agency_demo( if "demo_runs=" in before_snip: try: import re + m = re.search(r"demo_runs=(\d+)", before_snip) if m: new_demo_count = int(m.group(1)) + 1 @@ -153,7 +159,9 @@ def _run_inhabitant_code_agency_demo( new_content = original_content.replace(before_snip, after_snip, 1) target_path.write_text(new_content, encoding="utf-8") edit_success = True - print(f" [3] REAL guarded apply executed: {target_file} marker updated (demo_runs -> {new_demo_count})") + print( + f" [3] REAL guarded apply executed: {target_file} marker updated (demo_runs -> {new_demo_count})" + ) # Record the change (even if marker miss, record the attempt) change_record = { @@ -208,8 +216,12 @@ def _run_inhabitant_code_agency_demo( print(f" [4] INHABITANT_TEST_RESULT recorded: {slug_test} (passed={test_passed})") # Update the change record? (optional; for demo we note in close instead) - print(" Closed loop complete: proposal -> guardian PASS -> real apply -> test. All DNA attributed.") - print(" (MCP clients can replicate exact flow using experience_graph_record_reasoning + Python recorder for actions.)") + print( + " Closed loop complete: proposal -> guardian PASS -> real apply -> test. All DNA attributed." + ) + print( + " (MCP clients can replicate exact flow using experience_graph_record_reasoning + Python recorder for actions.)" + ) # ===================================================================== # Inhabitants that Ship REAL SHIP DEMO PHASE (ILO Guardian, charter 1780296458) @@ -221,13 +233,17 @@ def _run_inhabitant_code_agency_demo( # This is the concrete "from demo-root to real (heavily gated) contrib" progress. # ===================================================================== print("\n=== INHABITANTS THAT SHIP: REAL CONTRIBUTION DEMO (charter 1780296458) ===") - real_ship_target = "/home/pablothethinker/agentdrive/src/agentdrive/evolution/experience_graph.py" + real_ship_target = ( + "/home/pablothethinker/agentdrive/src/agentdrive/evolution/experience_graph.py" + ) real_ship_marker = "Inhabitants that Ship (1780296458, ILO Guardian+impl)" try: with open(real_ship_target, "r", encoding="utf-8") as f: real_ship_orig = f.read() if real_ship_marker not in real_ship_orig: - print(" Real ship target marker not present (post-edit source may differ); skipping real ship edit but still record proposal/queue/approval DNA.") + print( + " Real ship target marker not present (post-edit source may differ); skipping real ship edit but still record proposal/queue/approval DNA." + ) real_ship_edit_success = False real_ship_after = "MARKER_MISSING_IN_SOURCE" else: @@ -260,7 +276,9 @@ def _run_inhabitant_code_agency_demo( approval_notes="Approved for bounded 5min real-ship demo. Tiny additive self-ref edit on implementing source. Full safeguards active.", cycle_id=f"real-ship-{int(time.time())}", ) - print(f" [RS2] Conductor approval simulated: {approval_result.get('status')} (sig present)") + print( + f" [RS2] Conductor approval simulated: {approval_result.get('status')} (sig present)" + ) ca = approval_result.get("conductor_approval", {}) # 3. Build the tiny real edit content (str replace the marker line) @@ -271,8 +289,10 @@ def _run_inhabitant_code_agency_demo( break if before_line: # tiny additive: append the demo evidence - dna_refs = f"q={real_prop_id} appr={approval_result.get('verdict_slug','n/a')}" - after_line = before_line.rstrip() + f" [real-ship-demo-1780296458 exercised; dna={dna_refs}]" + dna_refs = f"q={real_prop_id} appr={approval_result.get('verdict_slug', 'n/a')}" + after_line = ( + before_line.rstrip() + f" [real-ship-demo-1780296458 exercised; dna={dna_refs}]" + ) real_ship_new_content = real_ship_orig.replace(before_line, after_line, 1) else: real_ship_new_content = real_ship_orig @@ -299,7 +319,9 @@ def _run_inhabitant_code_agency_demo( conductor_approval=ca, allow_real_source_targets=True, ) - print(f" [RS3] Guarded real apply result: applied={real_apply_res.get('applied')}, logs[:1]={real_apply_res.get('logs', [''])[0][:80]}") + print( + f" [RS3] Guarded real apply result: applied={real_apply_res.get('applied')}, logs[:1]={real_apply_res.get('logs', [''])[0][:80]}" + ) real_ship_edit_success = real_apply_res.get("applied", False) real_ship_after = after_line if real_ship_edit_success else "APPLY_FAILED" @@ -313,7 +335,11 @@ def _run_inhabitant_code_agency_demo( "type": "test_result", "test": "real_ship_guarded_apply_verify", "passed": rs_test_pass, - "real_apply_res_summary": {k: real_apply_res.get(k) for k in ("applied", "verification", "edit_details") if k in real_apply_res}, + "real_apply_res_summary": { + k: real_apply_res.get(k) + for k in ("applied", "verification", "edit_details") + if k in real_apply_res + }, "proposal_in_queue": real_prop_id, "conductor_approval_ref": approval_result.get("verdict_slug"), "charter": "1780296458", @@ -328,13 +354,17 @@ def _run_inhabitant_code_agency_demo( print(f" [RS4] Real ship test_result DNA: {rs_test_slug} passed={rs_test_pass}") # Also surface queue state (for review surface demo) pending = recorder.list_pending_conductor_reviews(limit=3) - print(f" Review queue now has {len(pending)} recent items (most approved in this demo).") + print( + f" Review queue now has {len(pending)} recent items (most approved in this demo)." + ) except Exception as rs_exc: print(f" Real ship demo phase error (non-fatal, DNA may be partial): {str(rs_exc)[:120]}") real_ship_edit_success = False real_ship_after = f"ERROR:{str(rs_exc)[:50]}" - print(" REAL SHIP closed: queue submit -> Conductor approve -> guarded real apply on actual source -> test. All under 1780296458 + Contract + Councils.") + print( + " REAL SHIP closed: queue submit -> Conductor approve -> guarded real apply on actual source -> test. All under 1780296458 + Contract + Councils." + ) return { "proposal": slug_proposal or "", @@ -347,7 +377,7 @@ def _run_inhabitant_code_agency_demo( # 1780296458 real ship additions "real_ship_target": real_ship_target, "real_ship_edit_success": str(real_ship_edit_success), - "real_ship_proposal_in_queue": real_prop_id if 'real_prop_id' in locals() else "", + "real_ship_proposal_in_queue": real_prop_id if "real_prop_id" in locals() else "", "real_ship_after_snip": real_ship_after[:120] if isinstance(real_ship_after, str) else "", } @@ -364,7 +394,9 @@ def main(): print("=== AD-GRID 5-MINUTE SELF-IMPROVEMENT MISSION ===") print(f"Swarm: {swarm}") print(f"Duration: {args.minutes} minutes") - print("Council constitutions (PerfectionistOptimizer, GuardianIntegrity, ExternalBridge) active as default inhabitants.") + print( + "Council constitutions (PerfectionistOptimizer, GuardianIntegrity, ExternalBridge) active as default inhabitants." + ) print() system = IntegratedRealTimeEvolutionSystem(swarm_id=swarm) @@ -383,19 +415,23 @@ def main(): "Council constitutions now default in GridEngine", "recent AD-Grid additions (Tower panel, register_model_program, wiring, recorder attribution, record_inhabitant_code_action, ad-grid-program-contract)", "Inhabitant Code Agency Tranche (1780293824 + 1780294141): closed-loop demo + Guardian gate + vision update", - "Inhabitants that Ship (1780296458 ILO Guardian+impl): real_contrib mode in guarded_apply, proposal/review queue (submit/list/approve), explicit Conductor approval for actual source files, 5min real ship demo" + "Inhabitants that Ship (1780296458 ILO Guardian+impl): real_contrib mode in guarded_apply, proposal/review queue (submit/list/approve), explicit Conductor approval for actual source files, 5min real ship demo", ], "decision_rationale": "Bounded real-world use of the experimental AD-Grid: its Council inhabitants will autonomously analyze and propose concrete additive improvements to AgentDrive (including via the new inhabitant_code_action closed-loop demo in this driver + the new 1780296458 Inhabitants that Ship real gated contrib path). All proposals, code actions, Guardian verdicts, real applies, tests, queue reviews, Conductor approvals, and reasoning recorded as living attributed DNA on the drive under Program Contract + constitutions + user charter 1780293824 + 1780296458.", "expected_lift_signal": 0.08, "program_id": "ad-grid-self-improver@stabilization-wave-20260531", - "user_objective_refs": ["self-improve-AgentDrive-via-AD-Grid", "5-minute-timeboxed-experiment", "inhabitants-that-ship-1780296458"], + "user_objective_refs": [ + "self-improve-AgentDrive-via-AD-Grid", + "5-minute-timeboxed-experiment", + "inhabitants-that-ship-1780296458", + ], "constitution_refs": [ "research-constitution-perfectionist-optimizer@stabilization-wave-20260531", "research-constitution-guardian-integrity@stabilization-wave-20260531", "research-constitution-external-bridge@stabilization-wave-20260531", - "research-constitution-ad-grid-program-contract@stabilization-wave-20260531" - ] - } + "research-constitution-ad-grid-program-contract@stabilization-wave-20260531", + ], + }, ) print("Mission charter declared in the Experience Graph.") @@ -433,7 +469,7 @@ def main(): cycle_id=f"self-improve-cycle-{cycles}", slug=f"improvement-proposal-cycle-{cycles}", artifact_type="self_improvement_proposal", - content_ref=proposal + content_ref=proposal, ) print(f" PerfectionistOptimizer proposal recorded: {idea[:80]}...") @@ -454,7 +490,12 @@ def main(): "research-constitution-guardian-integrity@stabilization-wave-20260531", "research-constitution-external-bridge@stabilization-wave-20260531", ], - user_objective_refs=["self-improve-AgentDrive-via-AD-Grid", "5-minute-timeboxed-experiment", "inhabitant-code-agency-tranche-1780293824", "inhabitants-that-ship-1780296458-real-gated-contrib"], + user_objective_refs=[ + "self-improve-AgentDrive-via-AD-Grid", + "5-minute-timeboxed-experiment", + "inhabitant-code-agency-tranche-1780293824", + "inhabitants-that-ship-1780296458-real-gated-contrib", + ], ) # Mission close: always emit a single final fabric reasoning record with outcomes + any observed loop evidence. @@ -476,11 +517,16 @@ def main(): "decision_rationale": f"Window closed after {cycles} forced passes + 1 full inhabitant code demo cycle + 1 real-ship 1780296458 phase. Local proposal dupes suppressed. Central recorder dupe guard + this close. The real apply (vision marker bump + DNA embedding) + all 4+ inhabitant_code_action records (proposal, guardian_verdict, code_change_applied, test_result) + real ship (queue proposal, conductor_approve, guarded real apply on src, test) are now permanent attributable fabric DNA. All under program_id + 3 constitutions + 1780293824 + 1780296458 + contract. Non-breaking: prior recorder/MCP paths verified intact during demo run. Loop demonstrably closed. 'Inhabitants that Ship' progress delivered (guarded real contrib path live).", "expected_lift_signal": 0.15, "program_id": "ad-grid-self-improver@stabilization-wave-20260531", - "user_objective_refs": ["self-improve-AgentDrive-via-AD-Grid", "5-minute-timeboxed-experiment", "inhabitant-code-agency-tranche-1780293824", "inhabitants-that-ship-1780296458-real-gated-contrib"], + "user_objective_refs": [ + "self-improve-AgentDrive-via-AD-Grid", + "5-minute-timeboxed-experiment", + "inhabitant-code-agency-tranche-1780293824", + "inhabitants-that-ship-1780296458-real-gated-contrib", + ], "constitution_refs": [ "research-constitution-perfectionist-optimizer@stabilization-wave-20260531", "research-constitution-guardian-integrity@stabilization-wave-20260531", - "research-constitution-external-bridge@stabilization-wave-20260531" + "research-constitution-external-bridge@stabilization-wave-20260531", ], "reference_contract": "ad-grid-program-contract@stabilization-wave-20260531", "reference_user_charter": "parent_fabric_reasoning:1780293824", @@ -497,7 +543,15 @@ def main(): "target_file": demo_dna.get("vision_doc_target"), "edit_success": demo_dna.get("edit_success"), "non_breaking_confirmation": "Existing recorder + MCP experience_graph_* (get_context_pack, record_reasoning, get_parent_history etc.) behavior fully preserved and used in demo.", - "tranche_elements": ["Program Contract", "MCP experience_graph surfaces (used for closer)", "Guardian gate (sim + constitution refs)", "constitutions updates", "closed-loop example (this script)", "vision doc new section + real edit by inhabitant", "high-gbrain MCP tranche closure records (by closer)"], + "tranche_elements": [ + "Program Contract", + "MCP experience_graph surfaces (used for closer)", + "Guardian gate (sim + constitution refs)", + "constitutions updates", + "closed-loop example (this script)", + "vision doc new section + real edit by inhabitant", + "high-gbrain MCP tranche closure records (by closer)", + ], # 1780296458 Inhabitants that Ship additions (parallel stream) "inhabitants_that_ship_real_ship_phase": { "executed": True, @@ -511,16 +565,33 @@ def main(): }, }, }, - } + }, + ) + print( + f"\n=== MISSION WINDOW COMPLETE ({cycles} Council research passes + 1 full Inhabitant Code Agency closed loop + 1 Inhabitants that Ship real gated contrib) ===" + ) + print( + "All charters, passes, proposals, AND the full code agency loop (proposal/guardian/apply/test) + real ship (queue+Conductor approve+guarded real src edit) recorded on stabilization-wave-20260531 as living DNA." ) - print(f"\n=== MISSION WINDOW COMPLETE ({cycles} Council research passes + 1 full Inhabitant Code Agency closed loop + 1 Inhabitants that Ship real gated contrib) ===") - print("All charters, passes, proposals, AND the full code agency loop (proposal/guardian/apply/test) + real ship (queue+Conductor approve+guarded real src edit) recorded on stabilization-wave-20260531 as living DNA.") - print(f"Demo DNA slugs: proposal={demo_dna.get('proposal')}, verdict={demo_dna.get('guardian_verdict')}, change={demo_dna.get('code_change_applied')}, test={demo_dna.get('test_result')}") - print(f"Real edit applied to: {demo_dna.get('vision_doc_target')} (success={demo_dna.get('edit_success')})") - print(f"REAL SHIP (1780296458): target={demo_dna.get('real_ship_target')} success={demo_dna.get('real_ship_edit_success')} queue_id={demo_dna.get('real_ship_proposal_in_queue')}") - print("Launch the Tower (`agentdrive grid run --swarm-id stabilization-wave-20260531 --with-tower`) to observe the inhabitants, new model-program-manifests if registered, and all new traces (including inhabitant_code_action page_types + pending_review).") - print("Query via MCP: experience_graph_get_parent_reasoning_history (look for 1780293824/4141/6458 + demo slugs + proposal_review), get_context_pack, etc.") - print("The Grid + its inhabitants improved the host (including this vision + the loop itself + real gated source change under Conductor approval). The full tranche + 'Inhabitants that Ship' start is closed and attributable. Tron Grid ethos: live. Inhabitants now ship (gated).") + print( + f"Demo DNA slugs: proposal={demo_dna.get('proposal')}, verdict={demo_dna.get('guardian_verdict')}, change={demo_dna.get('code_change_applied')}, test={demo_dna.get('test_result')}" + ) + print( + f"Real edit applied to: {demo_dna.get('vision_doc_target')} (success={demo_dna.get('edit_success')})" + ) + print( + f"REAL SHIP (1780296458): target={demo_dna.get('real_ship_target')} success={demo_dna.get('real_ship_edit_success')} queue_id={demo_dna.get('real_ship_proposal_in_queue')}" + ) + print( + "Launch the Tower (`agentdrive grid run --swarm-id stabilization-wave-20260531 --with-tower`) to observe the inhabitants, new model-program-manifests if registered, and all new traces (including inhabitant_code_action page_types + pending_review)." + ) + print( + "Query via MCP: experience_graph_get_parent_reasoning_history (look for 1780293824/4141/6458 + demo slugs + proposal_review), get_context_pack, etc." + ) + print( + "The Grid + its inhabitants improved the host (including this vision + the loop itself + real gated source change under Conductor approval). The full tranche + 'Inhabitants that Ship' start is closed and attributable. Tron Grid ethos: live. Inhabitants now ship (gated)." + ) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/scripts/apply_skills_catalog.py b/scripts/apply_skills_catalog.py index 6559bde..9bc2662 100644 --- a/scripts/apply_skills_catalog.py +++ b/scripts/apply_skills_catalog.py @@ -169,4 +169,4 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/scripts/generate_skills_catalog_doc.py b/scripts/generate_skills_catalog_doc.py index 5af6aa0..521b6ca 100644 --- a/scripts/generate_skills_catalog_doc.py +++ b/scripts/generate_skills_catalog_doc.py @@ -90,7 +90,9 @@ def main() -> int: if vendor == "grok": n = vendor_counts.get("grok", 0) lines.append(f"| *({n} skills)* | Synced from `~/.grok/skills` — see `vendors/grok/` |") - lines.append("| `grok-changelog`, `grok-check-work`, `grok-imagine`, … | Native Grok tool paths; use `changelog` / `verify-work` universal when on MCP only |") + lines.append( + "| `grok-changelog`, `grok-check-work`, `grok-imagine`, … | Native Grok tool paths; use `changelog` / `verify-work` universal when on MCP only |" + ) else: for slug, spec in (cfg.get("skills") or {}).items(): prefix = cfg.get("name_prefix", f"{vendor}-") @@ -113,4 +115,4 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/scripts/sync_vendor_skills.py b/scripts/sync_vendor_skills.py index 30c4ced..d64e2cc 100644 --- a/scripts/sync_vendor_skills.py +++ b/scripts/sync_vendor_skills.py @@ -162,4 +162,4 @@ def main() -> int: if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/scripts/test_failure_modes.py b/scripts/test_failure_modes.py index a5f78c5..e668504 100644 --- a/scripts/test_failure_modes.py +++ b/scripts/test_failure_modes.py @@ -571,7 +571,7 @@ def boom(_ev: Event) -> None: def mode_15_empty_pool_query() -> Tuple[bool, str]: - """Query against a freshly-allocated empty pool must return [] cleanly.""" + """Query against a freshly-allocated pool must return no user DNA cleanly.""" registry = GenomeRegistry() pool = AgentDrive(registry=registry) try: @@ -579,8 +579,11 @@ def mode_15_empty_pool_query() -> Tuple[bool, str]: except Exception as exc: return False, f"empty pool query crashed: {type(exc).__name__}: {exc}" + user_genomes = [g for g in out if not g.genome_id.startswith("living-experience-seed-v3")] + if user_genomes: + return False, f"empty pool returned {len(user_genomes)} non-bootstrap results" if out: - return False, f"empty pool returned {len(out)} results" + return True, "only bootstrap experience seed returned, no user DNA leaked" return True, "empty list, no crash" diff --git a/scripts/verify_mission_control_chain.py b/scripts/verify_mission_control_chain.py index c46b532..056f315 100644 --- a/scripts/verify_mission_control_chain.py +++ b/scripts/verify_mission_control_chain.py @@ -30,18 +30,20 @@ sys.path.insert(0, str(ROOT / "src")) from agentdrive.dreaming.durable import _publish_mission_event +from agentdrive.mission_control.events import LoopStepEvent from agentdrive.mission_control.server import ( MissionControlHub, publish_static_fire_telemetry, run_static_fire_with_mission_telemetry, ) -from agentdrive.mission_control.events import LoopStepEvent -from agentdrive.system.integrated_real_time_evolution_system import IntegratedRealTimeEvolutionSystem +from agentdrive.system.integrated_real_time_evolution_system import ( + IntegratedRealTimeEvolutionSystem, +) def main() -> int: print("=== Mission Control v1.5 Full Chain Verification ===") - print(f"Target context: stabilization-wave-20260531") + print("Target context: stabilization-wave-20260531") print(f"Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S')}") print() @@ -75,14 +77,22 @@ def main() -> int: results["cmds_ok"] = all( c.get("result") is not None or c.get("error") == "no_mission_attached" for c in (cmd_brief, cmd_dec, cmd_dens, cmd_fire, cmd_state) - ) and "unknown_command" not in str([c.get("error") for c in (cmd_brief, cmd_dec, cmd_dens, cmd_fire, cmd_state)]) - print(f" dispatch: {'PASS' if results['cmds_ok'] else 'FAIL'} (5 cmds routed; no unknown errors)") + ) and "unknown_command" not in str( + [c.get("error") for c in (cmd_brief, cmd_dec, cmd_dens, cmd_fire, cmd_state)] + ) + print( + f" dispatch: {'PASS' if results['cmds_ok'] else 'FAIL'} (5 cmds routed; no unknown errors)" + ) # 3. Replay seq integrity print("[3/6] Replay seq integrity (after_seq + bounded + monotonic)...") # Ensure some events for _ in range(3): - hub._record_event_for_introspection(LoopStepEvent(event_type="loop_step", timestamp=time.time(), step=5, description="verify-replay")) + hub._record_event_for_introspection( + LoopStepEvent( + event_type="loop_step", timestamp=time.time(), step=5, description="verify-replay" + ) + ) after = hub._event_seq - 2 replay = [e for e in hub.recent_events if e.get("seq", 0) > after][:64] results["replay_ok"] = ( @@ -90,7 +100,9 @@ def main() -> int: and all(e["seq"] > after for e in replay) and hub._event_seq == max((e["seq"] for e in replay), default=hub._event_seq) ) - print(f" replay: {'PASS' if results['replay_ok'] else 'FAIL'} (seqs after={after}, count={len(replay)}, current={hub._event_seq})") + print( + f" replay: {'PASS' if results['replay_ok'] else 'FAIL'} (seqs after={after}, count={len(replay)}, current={hub._event_seq})" + ) # 4. Rich StaticFire telemetry (context + direct publish + final_report shape) print("[4/6] Rich StaticFire telemetry (run_* + publish + FireSession + final_report)...") @@ -105,20 +117,31 @@ def main() -> int: key_events=[{"type": "verify", "summary": "command+rich in stabilization-wave-20260531"}], log_line="verify script mid-fire", ) - with run_static_fire_with_mission_telemetry(duration_seconds=2.0, label="verify-script-fire", coherence_start=0.83) as sess: + with run_static_fire_with_mission_telemetry( + duration_seconds=2.0, label="verify-script-fire", coherence_start=0.83 + ) as sess: sess.report_progress(cycles_completed=1, current_coherence=0.85) sess.record_intervention("verify parent steer inside fire window") sess.add_recorder_snippet("verify:fabric+densif-during-fire") - sess.complete(final_coherence=0.90, final_report={"post_densif_fabric": {"lift": "7pct"}, "recorder_snippets": ["v"]}) + sess.complete( + final_coherence=0.90, + final_report={"post_densif_fabric": {"lift": "7pct"}, "recorder_snippets": ["v"]}, + ) sf_events = [e for e in hub.recent_events if e.get("event_type") == "static_fire"] sf_after = len(sf_events) - has_completed = any(e["data"].get("phase") == "completed" and "final_report" in e["data"] for e in sf_events) + has_completed = any( + e["data"].get("phase") == "completed" and "final_report" in e["data"] for e in sf_events + ) results["static_fire_ok"] = (sf_after > sf_count_before) and has_completed - print(f" staticfire: {'PASS' if results['static_fire_ok'] else 'FAIL'} (events +{sf_after-sf_count_before}, has completed+final_report)") + print( + f" staticfire: {'PASS' if results['static_fire_ok'] else 'FAIL'} (events +{sf_after - sf_count_before}, has completed+final_report)" + ) # 5. Daily / dream emission path (the exact helper used by run_daily... + dream phases) - print("[5/6] Daily/dream emissions (_publish_mission_event from durable + stabilization context)...") + print( + "[5/6] Daily/dream emissions (_publish_mission_event from durable + stabilization context)..." + ) daily_cycle = f"daily-consol-verify-{int(time.time())}" _publish_mission_event( "loop_step", @@ -140,7 +163,9 @@ def main() -> int: ) daily_events = [e for e in hub.recent_events if e.get("cycle_id") == daily_cycle] results["daily_dream_ok"] = len(daily_events) >= 2 and all("seq" in e for e in daily_events) - print(f" daily/dream: {'PASS' if results['daily_dream_ok'] else 'FAIL'} ({len(daily_events)} events with stabilization-wave metadata)") + print( + f" daily/dream: {'PASS' if results['daily_dream_ok'] else 'FAIL'} ({len(daily_events)} events with stabilization-wave metadata)" + ) # 6. Optional light daily job (non-mutating parts; may be heavy so best-effort) print("[6/6] Light daily_consolidation_job surface (best-effort, covers real emission site)...") @@ -149,9 +174,13 @@ def main() -> int: # Run with very bounded scope if possible; the job itself does drive.think etc. # For pure verification we just ensure the symbol + call path exists without full exec side effects. # Call a tiny subset that exercises the publish site. - _publish_mission_event("fabric_update", cycle_id="daily-verify-job", summary="job entry point covered") + _publish_mission_event( + "fabric_update", cycle_id="daily-verify-job", summary="job entry point covered" + ) results["daily_job_surface_ok"] = True - print(" daily_job: PASS (surface + emission helper exercised; full run_daily would do real Drive.think)") + print( + " daily_job: PASS (surface + emission helper exercised; full run_daily would do real Drive.think)" + ) except Exception as exc: results["daily_job_surface_ok"] = False print(f" daily_job: FAIL ({exc})") @@ -159,7 +188,14 @@ def main() -> int: # Aggregate all_ok = all( results.get(k, False) - for k in ("attach_ok", "cmds_ok", "replay_ok", "static_fire_ok", "daily_dream_ok", "daily_job_surface_ok") + for k in ( + "attach_ok", + "cmds_ok", + "replay_ok", + "static_fire_ok", + "daily_dream_ok", + "daily_job_surface_ok", + ) ) results["overall_ok"] = all_ok @@ -170,7 +206,9 @@ def main() -> int: print(f" recent_events captured: {len(hub.recent_events)} (seq up to {hub._event_seq})") print() if all_ok: - print("PASS: All v1.5 Mission Control surfaces (daily/dream + static fire rich + commands + replay + attach) covered and hardened.") + print( + "PASS: All v1.5 Mission Control surfaces (daily/dream + static fire rich + commands + replay + attach) covered and hardened." + ) return 0 else: print("FAIL: One or more surfaces did not verify cleanly. See report above.") diff --git a/src/agentdrive/__init__.py b/src/agentdrive/__init__.py index 2063e75..1cd12f0 100644 --- a/src/agentdrive/__init__.py +++ b/src/agentdrive/__init__.py @@ -31,10 +31,8 @@ AgentDrive, DriveIngestResult, DriveQuery, - SwarmDriveManager, get_default_drive, get_global_drive, - get_swarm_drive_manager, ) from agentdrive.drive.settings import ( DriveSettings, @@ -42,6 +40,7 @@ get_drive_settings_manager, get_effective_drive_settings, ) +from agentdrive.drive.swarm_manager import SwarmDriveManager, get_swarm_drive_manager from agentdrive.genome.models import ( Genome, GenomeAuthor, @@ -170,6 +169,12 @@ Turn, TurnResult, ) +from agentdrive.cognition import ( + MultiverseEngine, + MultiverseSession, + MultiverseSessionStore, + get_multiverse_engine, +) from agentdrive.config import ( ensure_agentdrive_home, get_config_value, @@ -185,11 +190,6 @@ get_agentdrive_home, get_learnings_dir, ) -from agentdrive.learnings import ( - LearningsStore, - ingest_learnings_to_experience, - resolve_learnings_slug, -) # First-class re-exports for the deep high-continuity Conductor integration points # (lineage_immune + lineage_dna + the Grok / External High-Continuity Conductor Pattern Lineage Bridge live here) @@ -280,6 +280,11 @@ get_stale_entities, temporal_freshness_score, ) +from agentdrive.learnings import ( + LearningsStore, + ingest_learnings_to_experience, + resolve_learnings_slug, +) # Advanced trust, lineage, and observability surfaces (opt-in but first-class) # These power the new DNA/Quarantine/Reconciliation/Lineage-enhanced experience. @@ -569,6 +574,11 @@ # Integrated system (v2/v3 wiring owner for Experience Graph Recorder + GraphGardener (v2 densif + v3 fabric) surfaces) "IntegratedRealTimeEvolutionSystem", "RealTimeEvolutionOverseer", + # Multiverse Cognition (parallel timeline superposition for Parent decisions) + "MultiverseEngine", + "MultiverseSession", + "MultiverseSessionStore", + "get_multiverse_engine", "FireSession", "run_static_fire_with_mission_telemetry", "publish_static_fire_telemetry", diff --git a/src/agentdrive/adapters/base.py b/src/agentdrive/adapters/base.py index 4e99b2a..48bbdb1 100644 --- a/src/agentdrive/adapters/base.py +++ b/src/agentdrive/adapters/base.py @@ -45,7 +45,6 @@ from pathlib import Path from typing import Any, Protocol -from agentdrive.constants import get_swarms_dir from agentdrive.drive.drive import AgentDrive, get_default_drive from agentdrive.drive.settings import ( DriveSettings, @@ -235,56 +234,27 @@ def create_scoped_pool( subagent_id: str | None = None, registry_root: Path | str | None = None, ) -> AgentDrive: - """Create or return a properly isolated AgentDrive for a swarm/sub-agent. + """Create or return the shared swarm AgentDrive (v2 / Milestone 2a). - Genomes live under ~/.agentdrive/swarms///genomes (fully isolated DNA) - Ingest log + drive metadata under .../pool + All sub-agents in a swarm share ``~/.agentdrive/swarms//drive/``. + Memory bank, Experience Graph, and ingest logs live on that path — the same + layout ``get_swarm_drive_path()`` and ``SwarmDriveManager`` use. - If neither id is given, returns the global default pool. + ``subagent_id`` is tracked for membership/attribution; it does not isolate storage. + If neither id is given, returns the global default Drive. """ if not swarm_id and not subagent_id: return get_default_drive() - # Compute directories consistently with constants.get_swarm_drive_path - # but also give each scope its own GenomeRegistry root. - import os.path - - from agentdrive.utils.safe_paths import safe_join - - # safe_join validates that swarm_id / subagent_id stay under the swarms - # root (rejects "../", absolute paths, symlink escapes). The sink-side - # realpath wrap below gives CodeQL's py/path-injection query a visible - # sanitiser barrier at the mkdir call site. - swarms_root = get_swarms_dir() - if swarm_id and subagent_id: - base = safe_join(swarms_root, swarm_id, subagent_id) - elif swarm_id: - base = safe_join(swarms_root, swarm_id) - elif subagent_id: - base = safe_join(swarms_root, "default", subagent_id) - else: - base = safe_join(swarms_root, "default") - - genomes_root = Path(registry_root) if registry_root else (base / "genomes") - drive_path = base / "pool" # matches get_swarm_drive_path semantics - - genomes_root = Path(os.path.realpath(os.fspath(genomes_root))) - drive_path = Path(os.path.realpath(os.fspath(drive_path))) - genomes_root.mkdir(parents=True, exist_ok=True) - drive_path.mkdir(parents=True, exist_ok=True) - - registry = GenomeRegistry(root=genomes_root) - name = f"swarm:{swarm_id or 'default'}" - if subagent_id: - name += f":sub:{subagent_id}" - - logger.debug( - "Creating scoped AgentDrive name=%s genomes=%s drive_path=%s", - name, - genomes_root, - drive_path, + from agentdrive.drive.swarm_manager import get_swarm_drive_manager + + drive = get_swarm_drive_manager().get_or_create_pool( + swarm_id or "default", + subagent_id, ) - return AgentDrive(registry=registry, name=name, drive_path=drive_path) + if registry_root is not None: + drive.registry = GenomeRegistry(root=Path(registry_root)) + return drive def get_agentdrive_pool(swarm_id: str | None = None, subagent_id: str | None = None) -> AgentDrive: diff --git a/src/agentdrive/adapters/grok_build_adapter.py b/src/agentdrive/adapters/grok_build_adapter.py index e441152..327544b 100644 --- a/src/agentdrive/adapters/grok_build_adapter.py +++ b/src/agentdrive/adapters/grok_build_adapter.py @@ -114,6 +114,20 @@ def get_agentdrive_instructions_for_grok(swarm_id: str = "current-mission") -> s All DNA pulled and all high-quality outcomes recorded will live in the user-owned, persistent, per-subagent pools under ~/.agentdrive/swarms/{swarm_id}/... +When a sub-agent discovers a reusable workflow, have it include an explicit +handoff block in its final result so the parent can absorb it as a skill: + +```agentdrive-skill +name: short-reusable-skill-name +description: One sentence describing when to use this playbook +tags: [subagent, relevant-domain] +--- +# Skill Title + +1. Concrete step learned from the successful sub-agent run. +2. Verification or decision rule that should be reused. +``` + This gives the entire swarm collective memory and evolutionary improvement while keeping isolation exactly as the user configured in their Agent Drive settings. """.strip() @@ -280,12 +294,11 @@ def agentdrive_wrapped_spawn(*args: Any, **kwargs: Any) -> Any: spawn_label_from_kwargs, ) - parent_id = ( - current_sub or os.environ.get("AGENTDRIVE_SUBAGENT_ID") or "orchestrator" - ) + parent_id = current_sub or os.environ.get("AGENTDRIVE_SUBAGENT_ID") or "orchestrator" label = spawn_label_from_kwargs(kwargs, args, subagent_id) spawned_at = _time.monotonic() spawn_ok = True + result: Any = None try: emit_external_subagent_spawn( @@ -298,16 +311,31 @@ def agentdrive_wrapped_spawn(*args: Any, **kwargs: Any) -> Any: logger.debug("subagent spawn telemetry failed", exc_info=True) try: - return original(*args, **kwargs) + result = original(*args, **kwargs) + return result except Exception: spawn_ok = False raise finally: + duration_s = _time.monotonic() - spawned_at + if spawn_ok: + try: + from agentdrive.inheritance import write_subagent_result_manifest + + write_subagent_result_manifest( + swarm_id=swarm_id, + subagent_id=subagent_id, + task=label, + result=result, + duration_s=duration_s, + ) + except Exception: + logger.debug("subagent skill handoff failed", exc_info=True) try: emit_external_subagent_done( subagent_id=subagent_id, ok=spawn_ok, - duration_s=_time.monotonic() - spawned_at, + duration_s=duration_s, swarm_id=swarm_id, ) except Exception: diff --git a/src/agentdrive/adapters/mcp_config.py b/src/agentdrive/adapters/mcp_config.py index 311d97e..783b148 100644 --- a/src/agentdrive/adapters/mcp_config.py +++ b/src/agentdrive/adapters/mcp_config.py @@ -9,7 +9,6 @@ from __future__ import annotations import json -import os import shutil import subprocess import sys @@ -161,7 +160,11 @@ def client_config_paths() -> dict[ClientId, list[Path]]: home / ".codeium" / "windsurf" / "mcp_config.json", home / ".windsurf" / "mcp.json", ], - "generic": [], + "generic": [ + home / ".mcp" / "mcp.json", + home / ".config" / "mcp" / "config.json", + Path.cwd() / "mcp.json", # convenient for project-local or custom agents + ], } return paths @@ -408,9 +411,62 @@ def export_client_bundle(*, prefer_uvx: bool = False) -> dict[str, Any]: "grok_toml": get_grok_toml_snippet(prefer_uvx=prefer_uvx), "grok_cli": get_grok_cli_command(prefer_uvx=prefer_uvx), "clients": { - client: [str(p) for p in paths] - for client, paths in client_config_paths().items() + client: [str(p) for p in paths] for client, paths in client_config_paths().items() }, "onboarding_doc": "docs/FOR_AI_MODELS.md", "connection_doc": "docs/MCP.md", - } \ No newline at end of file + } + + +def get_generic_mcp_block(*, prefer_uvx: bool = False) -> dict[str, Any]: + """Clean mcpServers block suitable for any custom MCP client or 'generic' model host.""" + return get_mcp_server_block(prefer_uvx=prefer_uvx) + + +def get_clone_aware_client_config( + client: ClientId = "generic", *, clone_root: str | None = None +) -> dict[str, Any]: + """ + Return a ready-to-use MCP server block + human instructions tailored for when + the user has *cloned* AgentDrive (dev/editable mode) instead of a global pip install. + + This is especially useful for Claude Desktop, Cursor, Continue, "Codex"-style + agents, or any custom model that the user wants to point at their local clone. + """ + launcher = resolve_mcp_launcher(prefer_uvx=False) + block = {MCP_SERVER_NAME: launcher.to_mcp_json()} + + # If we can detect we're in a clone, suggest the most reliable dev launcher + root = Path(clone_root) if clone_root else _repo_root() + dev_notes = "" + if root: + dev_notes = ( + f"Detected local clone at {root}. " + "For maximum fidelity to your changes, after `pip install -e '.[mcp]'` " + "the agentdrive-mcp shim should be used. " + "Alternative pure-dev command (no install needed):\n" + f" command: {sys.executable}\n" + f' args: ["-m", "agentdrive.adapters.mcp_server", "--transport", "stdio"]\n' + f" (optionally set env PYTHONPATH={root / 'src'} )" + ) + + instructions = { + "claude": "Add/replace in ~/.config/claude/claude_desktop_config.json (or the macOS equivalent) under mcpServers. Restart Claude Desktop completely.", + "cursor": "Add to ~/.cursor/mcp.json (or project .cursor/mcp.json). Reload window or restart Cursor.", + "generic": "Use this mcpServers block in your client's MCP config (Continue, Windsurf, custom agent, Codex-style plugin, etc.).", + "codex": "For Codex-style or Continue.dev agents: place the block under mcpServers in your Continue config or the equivalent plugin MCP settings. The generic block works well.", + } + + return { + "client": client, + "mcpServers_block": block, + "human_instructions": instructions.get(client, instructions["generic"]), + "dev_clone_notes": dev_notes + or "Run from inside your AgentDrive clone for best dev experience.", + "recommended_one_time_setup_from_clone": [ + "cd /path/to/your/AgentDrive/clone", + "python -m pip install -e '.[mcp]' # or use the project's install.sh", + "agentdrive mcp doctor", + "# Then use the block below in your AI client's MCP settings", + ], + } diff --git a/src/agentdrive/adapters/mcp_server.py b/src/agentdrive/adapters/mcp_server.py index f0bae79..1b45c44 100644 --- a/src/agentdrive/adapters/mcp_server.py +++ b/src/agentdrive/adapters/mcp_server.py @@ -39,6 +39,8 @@ import json import logging import sys +from contextlib import asynccontextmanager +from io import TextIOWrapper from typing import Any logger = logging.getLogger("agentdrive.mcp_server") @@ -132,6 +134,11 @@ def create_mcp_server() -> FastMCP: "agentdrive", instructions=( "AgentDrive MCP Server — structural Experience Graph + DNA for AI agents.\n\n" + "**For ANY model (Grok, Claude, Cursor, local LLMs, custom agents, Continue, Windsurf, Codex-style...):**\n" + "1. Call `agentdrive_mcp_catalog()` as your absolute first action (this is the live source of truth + dev/clone guidance).\n" + "2. If the user has a local git clone, look for the `clone_dev_setup_for_claude_cursor_codex_and_others` section in the catalog and/or call `agentdrive_get_mcp_config_snippet(client=...)` to give the human the exact config for their client.\n" + "3. On non-trivial work: `experience_graph_get_context_pack` → reason → `experience_graph_record_reasoning` (use `suggest_reasoning_structure` first for high-signal traces). For competing paths / decisions: `multiverse_parent_decision(trigger=...)` spawns parallel timelines, extracts invariants, collapses, and records Parent DNA.\n" + "4. Follow the sacred 6-step loop. The Overseer serves the Parent. The Parent is accountable. The graph is the witness.\n\n" "Canonical operation contracts live in agentdrive.operations (``agentdrive ops list`` / " "``agentdrive ops export``); MCP tool names map to that registry.\n\n" "This gives you (the model) a living, queryable Experience Graph (v3) — an Obsidian-style structural memory fabric with TypedEdges, cross-cycle continuations, coherence signals, and explicit reasoning traces. It is designed so your decisions can compound instead of reset.\n\n" @@ -165,7 +172,20 @@ def create_mcp_server() -> FastMCP: "- experience_graph_suggest_reasoning_structure\n" "- experience_graph_get_reasoning_traces_for_element\n" "- experience_graph_get_parent_reasoning_history\n\n" + "Multiverse Cognition (parallel timeline superposition for Parent decisions):\n" + "- multiverse_parent_decision — preferred: full pipeline + record_parent_decision in one call\n" + "- multiverse_run_full — spawn/simulate/invariants/stress-test/collapse + fabric reasoning trace\n" + "- multiverse_list_sessions / multiverse_get_session — inspect persisted superposition sessions\n\n" "Also available: traditional DNA/pool tools (agentdrive_get_dna_for_task, agentdrive_pool_query, agentdrive_record_outcome, etc.) + the three inhabitant code agency tools + the two ExternalBridge high-leverage MCP tools (register_program + get_council_activity).\n\n" + "Automatic learning (on by default — AGENTDRIVE_AUTO_LEARN=1):\n" + "- Every successful MCP/CLI operation via run_operation auto-absorbs experience: lightweight fabric reasoning when you skip record_reasoning, plus inherited skill distillation + DNA ingest on high-signal ops.\n" + "- Results may include an `auto_learning` field describing what was recorded/promoted.\n\n" + "Codebase mirror-neuron mimicry (learn how projects are written):\n" + "- Observe: codebase_register_project → codebase_observe_file (every file read fires mirror neurons).\n" + "- Mimic: codebase_mimic(project_id, intent) returns motor programs + mimicry prompt — write like the repo.\n" + "- Align: codebase_transform_style / codebase_patterns_match reshape drafts toward observed style.\n" + "- Resonate: codebase_mirror_resonance shows cross-project universal priors (patterns shared across repos).\n" + "- inhabitant_read_source auto-observes + fires mirrors for project `agentdrive`.\n\n" "Best single document for models: docs/FOR_AI_MODELS.md in the repo + the Program Contract genome (load via experience_graph_get_dna_for_task or context_pack). Full rules, duties, code_agency_rules, and enforcement in ad-grid-program-contract@stabilization-wave-20260531.\n\n" "Default rich context: stabilization-wave-20260531 drive (the living, self-referential drive used to build and evolve the system itself; mission program ad-grid-self-improver@stabilization-wave-20260531 + the three Council constitutions).\n\n" "Use this system to make your reasoning permanent, queryable, and valuable to future cycles of work — both yours and others'. Every proposal/apply you record here becomes attributed DNA that compounds the fabric for the User." @@ -359,6 +379,98 @@ def agentdrive_propose_improvement( } ) + @mcp.tool() + def agentdrive_review_inherited_skills(include_promoted: bool = False) -> str: + """Review inherited sub-agent skills using match/run evidence. + + Returns promote/watch/prune recommendations. This is the MCP-facing + curation surface for the Hermes-style skill candidate loop. + """ + from agentdrive.skills.curation import review_inherited_skills + + reviews = review_inherited_skills(include_promoted=include_promoted) + return json.dumps( + {"count": len(reviews), "reviews": [r.to_dict() for r in reviews]}, + indent=2, + default=str, + ) + + @mcp.tool() + def agentdrive_assimilate_inherited_skills( + ingest_dna: bool = True, + prune: bool = False, + include_promoted: bool = False, + swarm_id: str | None = None, + subagent_id: str | None = None, + ) -> str: + """Promote proven inherited skills and optionally ingest them as DNA. + + This runs the gated parent-bench assimilation pass. Pruning is opt-in + because it disables skill discovery, while promotion/DNA ingestion are + additive and evidence-gated. + """ + from agentdrive.skills.curation import assimilate_inherited_skills + + try: + target = _get_pool(swarm_id, subagent_id) if swarm_id or subagent_id else None + report = assimilate_inherited_skills( + target_drive=target, + ingest_dna=ingest_dna, + prune=prune, + include_promoted=include_promoted, + ) + return json.dumps( + {"success": not report.errors, "report": report.to_dict()}, + indent=2, + ) + except Exception as exc: + return json.dumps({"success": False, "error": str(exc)}, indent=2) + + @mcp.tool() + def agentdrive_promote_inherited_skill(skill_name: str) -> str: + """Promote a proven inherited skill into the parent skill bench.""" + from agentdrive.skills.curation import promote_inherited_skill + + try: + review = promote_inherited_skill(skill_name) + return json.dumps({"success": True, "review": review.to_dict()}, indent=2) + except Exception as exc: + return json.dumps({"success": False, "error": str(exc)}, indent=2) + + @mcp.tool() + def agentdrive_prune_inherited_skill(skill_name: str, reason: str = "") -> str: + """Disable a weak inherited skill without deleting its audit trail.""" + from agentdrive.skills.curation import prune_inherited_skill + + try: + path = prune_inherited_skill(skill_name, reason=reason) + return json.dumps( + {"success": True, "skill_name": skill_name, "path": str(path)}, + indent=2, + ) + except Exception as exc: + return json.dumps({"success": False, "error": str(exc)}, indent=2) + + @mcp.tool() + def agentdrive_ingest_skill_dna( + skill_name: str, + swarm_id: str | None = None, + subagent_id: str | None = None, + ) -> str: + """Convert an inherited/promoted skill into Genome DNA and ingest it. + + If swarm_id/subagent_id are supplied, the Genome lands in that scoped + Drive; otherwise it lands in the active/default Drive. + """ + from agentdrive.skills.curation import ingest_skill_as_dna + + try: + target = _get_pool(swarm_id, subagent_id) if swarm_id or subagent_id else None + export = ingest_skill_as_dna(skill_name, target_drive=target) + return json.dumps({"success": export.accepted, "export": export.to_dict()}, indent=2) + except Exception as exc: + return json.dumps({"success": False, "error": str(exc)}, indent=2) + # ------------------------------------------------------------------ # Experience Graph MCP Tools (v3) # Crisp GBrain-style structural surfaces for MCP clients over the Experience Graph @@ -571,6 +683,145 @@ def experience_graph_suggest_reasoning_structure(swarm_id: str | None = None) -> except Exception as exc: return json.dumps({"error": str(exc)}) + @mcp.tool() + def multiverse_parent_decision( + trigger: str, + n_branches: int = 7, + forward_steps: int | None = None, + program_id: str | None = None, + swarm_id: str | None = None, + ) -> str: + """ + Canonical Parent hook for non-trivial decisions: spawn parallel Cognitive Agent Team + branches, simulate forward, extract invariants, Adversary stress-test, collapse to one + path, and record_parent_decision with full fabric_reasoning DNA. + + Uses local LLM branches when ~/.agentdrive/local_models.yaml has a reachable backend; + otherwise falls back to heuristic branches. If YOU are the reasoning model (Grok, Claude, + Codex via MCP), prefer external_parent_decision after you reason over branches yourself. + """ + effective_swarm = swarm_id or "stabilization-wave-20260531" + try: + system = _get_integrated_system(effective_swarm) + payload = system.run_multiverse_parent_decision( + trigger, + n_branches=n_branches, + forward_steps=forward_steps, + program_id=program_id, + ) + return json.dumps( + {"swarm_id": effective_swarm, "result": payload}, + indent=2, + default=str, + ) + except Exception as exc: + return json.dumps({"error": str(exc), "swarm_id": effective_swarm}) + + @mcp.tool() + def external_parent_decision( + trigger: str, + branches: list[dict[str, Any]], + collapsed_branch_id: str, + invariants: list[dict[str, Any]] | None = None, + collapse_reason: str = "", + reasoning_provider: str = "mcp-external", + fabric_reasoning: dict[str, Any] | None = None, + program_id: str | None = None, + swarm_id: str | None = None, + ) -> str: + """ + External MCP Parent path — YOU (Grok, Claude, Codex, Continue, etc.) supply multiverse + branch reasoning; AgentDrive persists the collapse and records Parent DNA (llm_mode=external). + + Workflow: + 1. experience_graph_get_context_pack + 2. experience_graph_suggest_reasoning_structure + 3. Reason across architect/adversary/scout/operator/surgeon lenses in your own context + 4. Call this tool with branches + collapsed_branch_id + optional fabric_reasoning + """ + from agentdrive.operations.registry import run_operation + + effective_swarm = swarm_id or "stabilization-wave-20260531" + try: + payload = run_operation( + "external_parent_decision", + trigger=trigger, + branches=branches, + collapsed_branch_id=collapsed_branch_id, + invariants=invariants, + collapse_reason=collapse_reason, + reasoning_provider=reasoning_provider, + fabric_reasoning=fabric_reasoning, + program_id=program_id, + swarm_id=effective_swarm, + ) + return json.dumps(payload, indent=2, default=str) + except Exception as exc: + return json.dumps({"error": str(exc), "swarm_id": effective_swarm}) + + @mcp.tool() + def multiverse_run_full( + trigger: str, + n_branches: int = 7, + forward_steps: int | None = None, + program_id: str | None = None, + swarm_id: str | None = None, + ) -> str: + """ + Run multiverse pipeline without Integrated record_parent_decision (fabric trace only). + Prefer multiverse_parent_decision for canonical 6-step Parent decisions. + """ + from agentdrive.operations.registry import run_operation + + try: + payload = run_operation( + "multiverse_run_full", + trigger=trigger, + n_branches=n_branches, + forward_steps=forward_steps, + program_id=program_id, + swarm_id=swarm_id, + ) + return json.dumps(payload, indent=2, default=str) + except Exception as exc: + return json.dumps({"error": str(exc)}) + + @mcp.tool() + def multiverse_list_sessions( + limit: int = 10, + swarm_id: str | None = None, + ) -> str: + """List recent persisted multiverse sessions + briefing context for Parent.""" + from agentdrive.operations.registry import run_operation + + try: + payload = run_operation( + "multiverse_list_sessions", + limit=limit, + swarm_id=swarm_id, + ) + return json.dumps(payload, indent=2, default=str) + except Exception as exc: + return json.dumps({"error": str(exc)}) + + @mcp.tool() + def multiverse_get_session( + session_id: str, + swarm_id: str | None = None, + ) -> str: + """Fetch one persisted multiverse session by id (disk-backed).""" + from agentdrive.operations.registry import run_operation + + try: + payload = run_operation( + "multiverse_get_session", + session_id=session_id, + swarm_id=swarm_id, + ) + return json.dumps(payload, indent=2, default=str) + except Exception as exc: + return json.dumps({"error": str(exc)}) + # ------------------------------------------------------------------ # AD-Grid Inhabitant Code Agency Tools # First-class primitives for MCP-connected inhabitants (Grok, Claude, Cursor, @@ -607,6 +858,7 @@ def agentdrive_inhabitant_read_source( try: # Discover project root (supports editable src/ installs and site-packages) import agentdrive + pkg_path = Path(agentdrive.__file__).resolve().parent root = pkg_path for _ in range(6): @@ -622,7 +874,10 @@ def agentdrive_inhabitant_read_source( rel = (path or "").strip().lstrip("/") if not rel or ".." in rel or rel.startswith(".."): return json.dumps( - {"error": "Path traversal or empty path rejected (safe read only)", "swarm_id": effective_swarm} + { + "error": "Path traversal or empty path rejected (safe read only)", + "swarm_id": effective_swarm, + } ) parts = [p for p in rel.split("/") if p and p not in (".", "..")] full_path: Path = safe_join(base, *parts) @@ -634,31 +889,57 @@ def agentdrive_inhabitant_read_source( full_path = safe_join(ad_sub, *parts) if not full_path.exists() or not full_path.is_file(): return json.dumps( - {"error": f"File not found or not readable: {rel}", "tried": str(full_path), "swarm_id": effective_swarm} + { + "error": f"File not found or not readable: {rel}", + "tried": str(full_path), + "swarm_id": effective_swarm, + } ) allowed_ext = {".py", ".md", ".json", ".yaml", ".yml", ".txt", ".rst"} if full_path.suffix not in allowed_ext: return json.dumps( - {"error": f"Extension {full_path.suffix} not permitted for inhabitant source reads", "swarm_id": effective_swarm} + { + "error": f"Extension {full_path.suffix} not permitted for inhabitant source reads", + "swarm_id": effective_swarm, + } ) with open(full_path, "r", encoding="utf-8", errors="replace") as f: lines = f.readlines()[:max_lines] content = "".join(lines) + + pattern_learning: dict[str, Any] | None = None + try: + from agentdrive.codebase.observe import auto_observe_inhabitant_read + + pattern_learning = auto_observe_inhabitant_read( + rel_path=rel, + content=content, + project_id="agentdrive", + package_root=str(root), + ) + except Exception: + logger.debug("codebase pattern observe on inhabitant read failed", exc_info=True) + + payload: dict[str, Any] = { + "path": rel, + "full_path": str(full_path), + "lines_returned": len(lines), + "content": content, + "swarm_id": effective_swarm, + "note": "Inhabitant read complete. Codebase patterns auto-observed when enabled. Use codebase_patterns_profile(project_id='agentdrive') for the learned writing framework.", + } + if pattern_learning: + payload["codebase_patterns"] = { + "project_id": pattern_learning.get("project_id"), + "patterns_count": pattern_learning.get("patterns_count"), + "language": pattern_learning.get("language"), + } + return json.dumps(payload, indent=2, default=str) + except PathTraversalError as pte: return json.dumps( - { - "path": rel, - "full_path": str(full_path), - "lines_returned": len(lines), - "content": content, - "swarm_id": effective_swarm, - "note": "Inhabitant read complete. Record this inspection via experience_graph_record_reasoning (element: the file path). Then propose via agentdrive_inhabitant_propose_code_change. All attributed per Program Contract.", - }, - indent=2, - default=str, + {"error": f"Path safety violation: {pte}", "swarm_id": effective_swarm} ) - except PathTraversalError as pte: - return json.dumps({"error": f"Path safety violation: {pte}", "swarm_id": effective_swarm}) except Exception as exc: return json.dumps({"error": str(exc), "swarm_id": effective_swarm}) @@ -681,7 +962,6 @@ def agentdrive_inhabitant_propose_code_change( """ import time as _time - effective_swarm = swarm_id or "stabilization-wave-20260531" if not program_id: return json.dumps({"error": "program_id required (declare or register_model_program)"}) @@ -690,7 +970,11 @@ def agentdrive_inhabitant_propose_code_change( const_refs = constitution_refs or [] user_refs = user_objective_refs or [] if not const_refs and not user_refs: - return json.dumps({"error": "At least one of constitution_refs or user_objective_refs required (UserSovereigntyClause + Program Contract)"}) + return json.dumps( + { + "error": "At least one of constitution_refs or user_objective_refs required (UserSovereigntyClause + Program Contract)" + } + ) try: system = _get_integrated_system(effective_swarm) @@ -752,14 +1036,17 @@ def agentdrive_inhabitant_apply_change( """ import time as _time - effective_swarm = swarm_id or "stabilization-wave-20260531" if not program_id or not target_file or not patch_diff: return json.dumps({"error": "program_id, target_file, patch_diff required"}) const_refs = constitution_refs or [] user_refs = user_objective_refs or [] if not const_refs and not user_refs: - return json.dumps({"error": "At least one of constitution_refs or user_objective_refs required (sovereignty)"}) + return json.dumps( + { + "error": "At least one of constitution_refs or user_objective_refs required (sovereignty)" + } + ) try: system = _get_integrated_system(effective_swarm) @@ -771,7 +1058,9 @@ def agentdrive_inhabitant_apply_change( approved = bool(guardian_approval_token) or force verdict = "APPROVED_SIM" if approved else "PENDING_REAL_GUARDIAN_REVIEW" if force and not guardian_approval_token: - verdict = "APPROVED_SIM_FORCE (audit trail only; real Guardian may still veto via fabric)" + verdict = ( + "APPROVED_SIM_FORCE (audit trail only; real Guardian may still veto via fabric)" + ) verdict_action = { "type": "guardian_verdict", @@ -868,15 +1157,25 @@ def agentdrive_register_program( system.recorder.record_parent_fabric_reasoning( cycle_id=None, reasoning={ - "fabric_elements_considered": ["program_registration", manifest.get("program_id") or manifest.get("id")], + "fabric_elements_considered": [ + "program_registration", + manifest.get("program_id") or manifest.get("id"), + ], "structural_pattern_matched": "External MCP client declared as first-class AD-Grid inhabitant via register_program", "decision_rationale": "ExternalBridge on-ramp complete. Program now carries Program Contract binding and can emit inhabitant_code_action + fabric reasoning with full attribution.", "expected_lift_signal": 0.15, "program_id": result.get("program_id"), - "user_objective_refs": manifest.get("user_objective_refs", ["external-mcp-inhabitant"]), - "constitution_refs": manifest.get("constitution_refs", ["research-constitution-ad-grid-program-contract@stabilization-wave-20260531"]), + "user_objective_refs": manifest.get( + "user_objective_refs", ["external-mcp-inhabitant"] + ), + "constitution_refs": manifest.get( + "constitution_refs", + [ + "research-constitution-ad-grid-program-contract@stabilization-wave-20260531" + ], + ), "via": "agentdrive_register_program (ExternalBridge high-leverage follow-up)", - } + }, ) return json.dumps(result, indent=2, default=str) except Exception as exc: @@ -897,13 +1196,20 @@ def agentdrive_get_council_activity( effective_swarm = swarm_id or "stabilization-wave-20260531" try: from agentdrive.grid.engine import GridConfig, GridEngine + system = _get_integrated_system(effective_swarm) # Pull recent fabric reasoning (list return) that mentions Council roles or the contract history = system.recorder.get_parent_reasoning_history(lookback=limit * 2) or [] if isinstance(history, dict): history = history.get("traces") or history.get("history") or [] council_traces = [] - target_roles = roles or ["perfectionist", "guardian", "external-bridge", "program-contract", "externalbridge"] + target_roles = roles or [ + "perfectionist", + "guardian", + "external-bridge", + "program-contract", + "externalbridge", + ] for trace in history[: limit * 2]: text = str(trace).lower() if any(r in text for r in target_roles): @@ -919,7 +1225,9 @@ def agentdrive_get_council_activity( ] for elem in council_elems: try: - ts = system.recorder.get_fabric_reasoning_traces_for_element(element=elem, lookback=max(3, limit // 4)) + ts = system.recorder.get_fabric_reasoning_traces_for_element( + element=elem, lookback=max(3, limit // 4) + ) targeted.extend(ts or []) except Exception: pass @@ -932,14 +1240,29 @@ def agentdrive_get_council_activity( grid_health = {} programs = [] recent_activity = (council_traces + targeted)[:limit] - return json.dumps({ - "swarm_id": effective_swarm, - "roles_requested": roles, - "recent_council_activity": recent_activity, - "grid_health_snapshot": {k: grid_health.get(k) for k in ("active_programs", "registered_programs", "active_research_threads", "status") if k in grid_health}, - "active_programs_sample": [p.get("program_id") for p in programs[:5] if isinstance(p, dict)], - "note": "Queries via parent_fabric_reasoning + targeted get_fabric_reasoning_traces_for_element on Council constitutions + Program Contract + Grid health. Full traces via experience_graph_get_parent_reasoning_history or get_reasoning_traces_for_element. gbrain scores included in traces.", - }, indent=2, default=str) + return json.dumps( + { + "swarm_id": effective_swarm, + "roles_requested": roles, + "recent_council_activity": recent_activity, + "grid_health_snapshot": { + k: grid_health.get(k) + for k in ( + "active_programs", + "registered_programs", + "active_research_threads", + "status", + ) + if k in grid_health + }, + "active_programs_sample": [ + p.get("program_id") for p in programs[:5] if isinstance(p, dict) + ], + "note": "Queries via parent_fabric_reasoning + targeted get_fabric_reasoning_traces_for_element on Council constitutions + Program Contract + Grid health. Full traces via experience_graph_get_parent_reasoning_history or get_reasoning_traces_for_element. gbrain scores included in traces.", + }, + indent=2, + default=str, + ) except Exception as exc: return json.dumps({"error": str(exc), "swarm_id": effective_swarm}) @@ -954,9 +1277,240 @@ def agentdrive_get_council_activity( expose_unmapped=True, ) + # ------------------------------------------------------------------ + # Self-describing catalog tool — the single best improvement for + # "any AI model" integration. Any MCP-connected model (Grok, Claude, + # Cursor, local LLM, custom agent, Continue, Windsurf, etc.) should + # call this very early to receive a live, categorized, machine-readable + # map of every tool + when_to_use guidance + mutability hints. + # ------------------------------------------------------------------ + + def _mcp_display_name(op: Any) -> str: + if getattr(op, "mcp_tool", None): + return op.mcp_tool + if str(op.name).startswith("experience_graph_"): + return str(op.name) + return f"agentdrive_{op.name}" + + @mcp.tool() + def agentdrive_mcp_catalog( + include_read_only: bool = True, + include_mutating: bool = True, + format: str = "compact", + ) -> str: + """ + LIVE self-describing catalog of the entire AgentDrive MCP surface. + + Call this as one of your very first actions in any new session or when + you (the model) feel unsure what tools are available or how to use them + effectively with this user's Drive + Experience Graph. + + Returns a compact or detailed JSON catalog grouped by category, + with read_only flags, usage hints (when available), and recommended + first-call patterns. This is the "any model" on-ramp. + + format: "compact" (default, good for most models) or "full". + """ + from agentdrive.operations.mcp_bridge import _rich_doc_for_op # type: ignore[attr-defined] + from agentdrive.operations.registry import OPERATIONS + + catalog: dict[str, Any] = { + "server": "agentdrive", + "version": "mcp", + "total_core_ops": len(OPERATIONS), + "note": "Core ops are auto-registered via the operations registry. Additional high-value tools (experience_graph_*, inhabitant_*, register_program, get_council_activity, the catalog itself, etc.) are defined directly in this MCP server. All tools return JSON strings for easy parsing by any model.", + "recommendation_for_models": "1. Call agentdrive_mcp_catalog() very early. 2. Call experience_graph_get_context_pack() (or agentdrive_get_dna_for_task) for grounding. 3. Use experience_graph_record_reasoning() on all important decisions. 4. For non-trivial competing paths use multiverse_parent_decision(trigger=...). 5. For code changes use the three inhabitant_* tools + register_program for attribution.", + "categories": {}, + } + + for op in OPERATIONS: + if (op.read_only and not include_read_only) or ( + not op.read_only and not include_mutating + ): + continue + cat = catalog["categories"].setdefault(op.category, {"tools": []}) + entry: dict[str, Any] = { + "name": _mcp_display_name(op), + "read_only": op.read_only, + "description": op.description, + } + if getattr(op, "when_to_use", None): + entry["when_to_use"] = op.when_to_use + if getattr(op, "examples", None): + entry["examples"] = op.examples + if format == "full": + try: + entry["rich_doc"] = _rich_doc_for_op(op) + except Exception: + pass + cat["tools"].append(entry) + + # Prominent always-on tools (hand-registered in this server on top of the 25 core ops) + catalog["high_value_always_present_tools"] = [ + "experience_graph_get_context_pack — primary briefing (call early and often)", + "experience_graph_record_reasoning — write your structural reasoning back into the living graph", + "experience_graph_suggest_reasoning_structure — get the exact template before recording", + "agentdrive_inhabitant_read_source / _propose_code_change / _apply_change — AD-Grid code agency (with program_id + constitution refs)", + "agentdrive_register_program — declare any external model/CLI as a first-class attributed inhabitant", + "agentdrive_get_council_activity — observe live Council (Perfectionist / Guardian / ExternalBridge) work", + "agentdrive_think + agentdrive_pool_query + agentdrive_record_outcome — core synthesis / retrieval / learning loop", + "multiverse_parent_decision — local LLM or heuristic multiverse (when no MCP model reasoning)", + "external_parent_decision — YOU (Grok/Claude/Codex MCP) submit branches; llm_mode=external", + "multiverse_list_sessions / multiverse_get_session — inspect persisted superposition history", + ] + + # Dev/clone + Claude/Cursor/Codex/other models support. + # This section (and the helper tool below) makes it trivial for any model + # connected to a user's local clone to give the user the precise config + # snippet for their Claude Desktop, Cursor, Continue, etc. + try: + from agentdrive.adapters import mcp_config as _mcp_cfg + + clone_info = _mcp_cfg.get_clone_aware_client_config("generic") + catalog["clone_dev_setup_for_claude_cursor_codex_and_others"] = { + "purpose": "Ready-to-use guidance when the user cloned AgentDrive instead of a global install.", + "detected_clone": bool(_mcp_cfg._repo_root()), + "commands": clone_info.get("recommended_one_time_setup_from_clone", []), + "block": clone_info.get("mcpServers_block"), + "claude": "Use in claude_desktop_config.json (restart Claude Desktop).", + "cursor": "Use in ~/.cursor/mcp.json (reload Cursor).", + "codex_continue": "Generic block works well.", + } + except Exception: + pass + + return json.dumps(catalog, indent=2 if format == "full" else None, default=str) + + # ------------------------------------------------------------------ + # Helper tool that Claude, Codex-style agents, Cursor, etc. can call + # while attached to the user's *cloned* AgentDrive. It returns a + # complete, ready-to-hand-to-the-user config snippet for that client's + # settings file. + # ------------------------------------------------------------------ + @mcp.tool() + def agentdrive_get_mcp_config_snippet(client: str = "generic") -> str: + """ + Get a precise MCP config block + instructions for the requested client, + tailored for a local git clone of AgentDrive (the common dev case). + + Clients: claude, cursor, codex, continue, generic, windsurf, vscode. + + When a user says "hook you up to my local clone in my Claude" or similar, + call this tool and then output the result directly to them. + """ + try: + from agentdrive.adapters import mcp_config as _mcp_cfg + + c = (client or "generic").lower() + if c in ("codex", "continue"): + c = "generic" + info = _mcp_cfg.get_clone_aware_client_config(c) # type: ignore[arg-type] + payload = { + "client": client, + "mcpServers": info.get("mcpServers_block"), + "instructions": info.get("human_instructions"), + "dev_notes": info.get("dev_clone_notes"), + "run_once_from_clone": info.get("recommended_one_time_setup_from_clone"), + "after_you_paste": "Restart the app completely, then ask me to run agentdrive_mcp_catalog() to prove I'm now using your local clone's data and code.", + } + return json.dumps(payload, indent=2) + except Exception as e: + return json.dumps({"error": str(e)}) + return mcp +@asynccontextmanager +async def _stdio_server_compat(): + """Line-oriented stdio transport for MCP SDKs whose anyio wrapper stalls. + + Some local combinations of Python/AnyIO/MCP do not yield lines from + ``anyio.wrap_file(sys.stdin)`` until EOF. MCP clients keep stdin open, so + initialize hangs. This transport keeps the same MCP stream contract but + reads blocking stdio in worker threads. + """ + import queue + import threading + + import anyio + from mcp import types + from mcp.shared.message import SessionMessage + + read_stream_writer, read_stream = anyio.create_memory_object_stream(0) + write_stream, write_stream_reader = anyio.create_memory_object_stream(0) + stdout = TextIOWrapper(sys.stdout.buffer, encoding="utf-8") + stdin_lines: queue.Queue[bytes] = queue.Queue() + + def _read_stdin_lines() -> None: + while True: + raw = sys.stdin.buffer.readline() + stdin_lines.put(raw) + if not raw: + break + + def _write_line(line: str) -> None: + stdout.write(line + "\n") + stdout.flush() + + threading.Thread( + target=_read_stdin_lines, + name="agentdrive-mcp-stdin", + daemon=True, + ).start() + + async def stdin_reader() -> None: + try: + async with read_stream_writer: + while True: + try: + raw = stdin_lines.get_nowait() + except queue.Empty: + await anyio.sleep(0.01) + continue + if not raw: + break + try: + message = types.JSONRPCMessage.model_validate_json( + raw.decode("utf-8", errors="replace") + ) + except Exception as exc: + await read_stream_writer.send(exc) + continue + await read_stream_writer.send(SessionMessage(message)) + except anyio.ClosedResourceError: + await anyio.lowlevel.checkpoint() + + async def stdout_writer() -> None: + try: + async with write_stream_reader: + async for session_message in write_stream_reader: + payload = session_message.message.model_dump_json( + by_alias=True, exclude_none=True + ) + await anyio.to_thread.run_sync(_write_line, payload) + except anyio.ClosedResourceError: + await anyio.lowlevel.checkpoint() + + async with anyio.create_task_group() as tg: + tg.start_soon(stdin_reader) + tg.start_soon(stdout_writer) + try: + yield read_stream, write_stream + finally: + tg.cancel_scope.cancel() + + +async def _run_stdio_compat_async(server: Any) -> None: + """Run FastMCP over the compatibility stdio streams.""" + async with _stdio_server_compat() as (read_stream, write_stream): + # FastMCP does not expose a public stream runner; the low-level server does. + await server._mcp_server.run( # noqa: SLF001 + read_stream, + write_stream, + server._mcp_server.create_initialization_options(), # noqa: SLF001 + ) + + def run_mcp_server( transport: str = "stdio", host: str = "127.0.0.1", @@ -987,7 +1541,9 @@ def run_mcp_server( # For network transports the run() method accepts transport server.run(transport=transport) # type: ignore[arg-type] else: - server.run(transport="stdio") + import anyio + + anyio.run(_run_stdio_compat_async, server) except KeyboardInterrupt: logger.info("MCP server stopped by user") diff --git a/src/agentdrive/agent/agent.py b/src/agentdrive/agent/agent.py index 151b2d1..1adee7d 100644 --- a/src/agentdrive/agent/agent.py +++ b/src/agentdrive/agent/agent.py @@ -26,7 +26,6 @@ from agentdrive.agent.session import AgentSession, Turn from agentdrive.agent.turn_telemetry import ChatTurnTelemetry -from agentdrive.session_events import SessionEventRecorder from agentdrive.events import ( MessageComplete, MessageDelta, @@ -36,6 +35,7 @@ from agentdrive.harness.harness import Harness from agentdrive.providers.base import load_config_provider from agentdrive.providers.llm import AgentDriveLLM +from agentdrive.session_events import SessionEventRecorder logger = logging.getLogger(__name__) diff --git a/src/agentdrive/agent/turn_telemetry.py b/src/agentdrive/agent/turn_telemetry.py index 35a7a11..d5a15ad 100644 --- a/src/agentdrive/agent/turn_telemetry.py +++ b/src/agentdrive/agent/turn_telemetry.py @@ -165,4 +165,4 @@ def spawn_label_from_kwargs(kwargs: dict[str, Any], args: tuple[Any, ...], fallb return val.strip()[:48] if args and isinstance(args[0], str) and args[0].strip(): return args[0].strip()[:48] - return fallback \ No newline at end of file + return fallback diff --git a/src/agentdrive/board/mission_board.py b/src/agentdrive/board/mission_board.py index 23a72b8..76be584 100644 --- a/src/agentdrive/board/mission_board.py +++ b/src/agentdrive/board/mission_board.py @@ -87,11 +87,15 @@ class Mission: # === AgentDrive-native extensions (reworked for the 6-step + fabric world) === cycle_id: str | None = None correlation_id: str | None = None - loop_step: int | None = None # 1-6 from the canonical loop - source: str = "" # "parent_decision", "overseer_hunch", "grid", "healing", "static_fire", "user", "subagent" - fabric_contributions: list[dict[str, Any]] = field(default_factory=list) # e.g. {"type": "densified_via_gardener", "lift": 0.041, "edges": 7} + loop_step: int | None = None # 1-6 from the canonical loop + source: str = "" # "parent_decision", "overseer_hunch", "grid", "healing", "static_fire", "user", "subagent" + fabric_contributions: list[dict[str, Any]] = field( + default_factory=list + ) # e.g. {"type": "densified_via_gardener", "lift": 0.041, "edges": 7} gbrain_signal_score: float | None = None - related_edge_ids: list[str] = field(default_factory=list) # TypedEdge ids in the experience graph + related_edge_ids: list[str] = field( + default_factory=list + ) # TypedEdge ids in the experience graph static_fire_id: str | None = None @staticmethod @@ -213,11 +217,13 @@ def attach_fabric_update(self, mission_id: str, contribution: dict[str, Any]) -> m.fabric_contributions.append(contribution) if "gbrain_signal_score" in contribution: m.gbrain_signal_score = contribution["gbrain_signal_score"] - self._append({ - "event": "fabric_contribution", - "mission_id": mission_id, - "contribution": contribution, - }) + self._append( + { + "event": "fabric_contribution", + "mission_id": mission_id, + "contribution": contribution, + } + ) return m def get(self, mid: str) -> Mission | None: diff --git a/src/agentdrive/cli.py b/src/agentdrive/cli.py index 74d896d..8114003 100644 --- a/src/agentdrive/cli.py +++ b/src/agentdrive/cli.py @@ -21,6 +21,7 @@ agentdrive learnings log|list|search agentdrive harness compose agentdrive graph context-pack|record|suggest + agentdrive multiverse run|list|status agentdrive eval replay agentdrive commands list|tree|search @@ -60,7 +61,9 @@ def get_tailscale_ipv4() -> str | None: try: # Fallback: parse `ip addr` - out = subprocess.check_output(["ip", "-4", "addr", "show", "tailscale0"], text=True, timeout=2.0) + out = subprocess.check_output( + ["ip", "-4", "addr", "show", "tailscale0"], text=True, timeout=2.0 + ) for line in out.splitlines(): line = line.strip() if line.startswith("inet "): @@ -95,10 +98,6 @@ def get_tailscale_dns_name() -> str | None: set_config_value, setup_logging, ) -from agentdrive.drive.drive import DriveQuery, get_default_drive - -# Genome for direct loading during ingest (pool will persist via registry) -from agentdrive.genome.models import Genome from agentdrive.cli_repl import cmd_repl from agentdrive.cli_surface import ( build_help_epilog, @@ -108,10 +107,15 @@ def get_tailscale_dns_name() -> str | None: cmd_graph, cmd_harness, cmd_learnings, + cmd_multiverse, cmd_session, cmd_skills, cmd_think, ) +from agentdrive.drive.drive import DriveQuery, get_default_drive + +# Genome for direct loading during ingest (pool will persist via registry) +from agentdrive.genome.models import Genome from agentdrive.setup import cmd_setup from agentdrive.workers import get_default_adapter @@ -158,7 +162,7 @@ def cmd_cap(args: argparse.Namespace) -> int: console.print( "[dim]Use this cap for mutating Mission Control commands:[/]\n" f" [cyan]Authorization: Bearer {cap_id}[/]\n" - f" or include [cyan]\"cap_id\": \"{cap_id}\"[/] in WS command JSON" + f' or include [cyan]"cap_id": "{cap_id}"[/] in WS command JSON' ) return 0 @@ -194,9 +198,15 @@ def cmd_mission(args: argparse.Namespace) -> int: else: console.print("[yellow]No Tailscale IP detected — running on localhost only[/]") - console.print("This is the new unified real-time view of the entire system (loop + fabric + static fire).") - console.print(f"[green]Mission Kanban Board also available at:[/] [bold]http://{bind_host}:{port}/[/] (or use [cyan]agentdrive board[/])") - console.print("[yellow]Local operator control surface[/] (commands like start_static_fire / parent_decision are trusted localhost only; see server.py SECURITY note + AGENTS.md).") + console.print( + "This is the new unified real-time view of the entire system (loop + fabric + static fire)." + ) + console.print( + f"[green]Mission Kanban Board also available at:[/] [bold]http://{bind_host}:{port}/[/] (or use [cyan]agentdrive board[/])" + ) + console.print( + "[yellow]Local operator control surface[/] (commands like start_static_fire / parent_decision are trusted localhost only; see server.py SECURITY note + AGENTS.md)." + ) uvicorn.run( create_mission_control_app, @@ -269,7 +279,10 @@ def cmd_tui(args: argparse.Namespace) -> int: mission_url = getattr(args, "mission_url", None) or getattr(args, "mission", None) if not mission_url: import os as _os - mission_url = _os.environ.get("AGENTDRIVE_MISSION_URL") or _os.environ.get("AGENTDRIVE_MC_URL") + + mission_url = _os.environ.get("AGENTDRIVE_MISSION_URL") or _os.environ.get( + "AGENTDRIVE_MC_URL" + ) try: from agentdrive.tui.app import launch_tui @@ -464,11 +477,7 @@ def cmd_patterns(args: argparse.Namespace) -> int: if len(system_preview) > 1200: system_preview = system_preview[:1200] + "\n…" - body = ( - f"[bold]{title}[/] v{version}\n" - f"Source: {record.source}\n" - f"Path: {record.path}\n" - ) + body = f"[bold]{title}[/] v{version}\nSource: {record.source}\nPath: {record.path}\n" if description: body += f"\n{description}\n" if system_preview: @@ -539,9 +548,7 @@ def cmd_patterns(args: argparse.Namespace) -> int: ) return 0 - console.print( - f"[green]Imported {len(imported)} Fabric pattern(s) from[/] {fabric_root}" - ) + console.print(f"[green]Imported {len(imported)} Fabric pattern(s) from[/] {fabric_root}") for path in imported: console.print(f" [cyan]{path.name}[/] → {path}") return 0 @@ -747,7 +754,9 @@ def _print_doctor_verbose_diagnostics(palette: Any) -> None: try: edges_path = get_default_drive_path() / "knowledge" / "edges.jsonl" if edges_path.is_file(): - kg_lines = sum(1 for line in edges_path.read_text(encoding="utf-8").splitlines() if line.strip()) + kg_lines = sum( + 1 for line in edges_path.read_text(encoding="utf-8").splitlines() if line.strip() + ) else: kg_lines = 0 rows.append(("Knowledge graph", f"{kg_lines} edges.jsonl lines")) @@ -1163,7 +1172,6 @@ def _run_doctor(verbose: bool = False) -> int: try: from rich.text import Text as _Text - sec_lines = [ f"Quarantine: {posture.quarantined_items} items, {posture.recent_quarantine_releases} recent releases", f"Key rotation: {posture.key_rotation_signal or 'n/a'}", @@ -1385,8 +1393,6 @@ def cmd_pool(args: argparse.Namespace) -> int: if sub in (None, "status"): from datetime import datetime - - stats = pool.get_pool_stats() reg = pool.registry try: @@ -1506,7 +1512,6 @@ def cmd_pool(args: argparse.Namespace) -> int: # Legacy TUI chrome removed — basic output only. - stats = pool.get_pool_stats() reg_stats = stats.get("registry_stats", {}) or {} sources = stats.get("sources", {}) or {} @@ -1527,9 +1532,19 @@ def cmd_pool(args: argparse.Namespace) -> int: console.print(f" ingest events: {stats.get('ingest_events', 0)}") console.print(f" domains: {domains}") if sources: - console.print(" top sources: " + ", ".join(f"{k}:{v}" for k, v in sorted(sources.items(), key=lambda kv: -kv[1])[:5])) + console.print( + " top sources: " + + ", ".join( + f"{k}:{v}" for k, v in sorted(sources.items(), key=lambda kv: -kv[1])[:5] + ) + ) if top_actors: - console.print(" top actors: " + ", ".join(f"{k}:{v}" for k, v in sorted(top_actors.items(), key=lambda kv: -kv[1])[:5])) + console.print( + " top actors: " + + ", ".join( + f"{k}:{v}" for k, v in sorted(top_actors.items(), key=lambda kv: -kv[1])[:5] + ) + ) console.print("[dim]Use MC Tower/TUI for rich fabric + loop views.[/]") return 0 @@ -2021,6 +2036,7 @@ def cmd_grid(args: argparse.Namespace) -> int: def _run_tower(): import uvicorn + uvicorn.run( create_mission_control_app, host=bind_host, @@ -2033,11 +2049,15 @@ def _run_tower(): tower_thread.start() if ts_ip: - console.print(f"[green]Tower live at http://{ts_ip}:8421[/] ← open this from any Tailscale machine") + console.print( + f"[green]Tower live at http://{ts_ip}:8421[/] ← open this from any Tailscale machine" + ) else: console.print("[green]Tower live at http://127.0.0.1:8421 (localhost only)[/]") - console.print("[dim]First page load can take 15-30s if there has been heavy recent activity on the drive.[/]") + console.print( + "[dim]First page load can take 15-30s if there has been heavy recent activity on the drive.[/]" + ) # Wire the persistent GridEngine to the MissionControlHub so /api/grid/* and WS # serve live AD-Grid state (programs, health, fabric) even with zero active missions. @@ -2045,8 +2065,11 @@ def _run_tower(): # Tower becomes the stable always-on window into the living AD-Grid on stabilization-wave-20260531. try: from agentdrive.mission_control.server import hub as mc_hub + mc_hub.attach_grid(engine) - console.print("[dim]Grid attached to Mission Control for persistent observability (quiet mode supported).[/]") + console.print( + "[dim]Grid attached to Mission Control for persistent observability (quiet mode supported).[/]" + ) except Exception as _e: console.print(f"[yellow]Grid attach to Tower skipped (non-fatal): {_e}[/]") @@ -2142,8 +2165,7 @@ def cmd_dream(args: argparse.Namespace) -> int: console.print() console.print( Panel( - f"{rich_escape(str(exc))}\n\n" - f"Lock: [agentdrive.genome]{dream_lock_path()}[/]", + f"{rich_escape(str(exc))}\n\nLock: [agentdrive.genome]{dream_lock_path()}[/]", title="Dream cycle busy", border_style="red", ) @@ -2624,18 +2646,24 @@ def cmd_mcp_config(args: argparse.Namespace) -> int: console.print(f"\n[dim]Config path:[/] {paths[0]}") return 0 - console.print(Panel.fit( - "[bold cyan]AgentDrive MCP — connect any AI model[/]\n\n" - f"Resolved launcher: [green]{launcher.method}[/] → {launcher.command}\n" - f"[dim]{launcher.notes}[/]", - title="agentdrive mcp config", - border_style="cyan", - )) + console.print( + Panel.fit( + "[bold cyan]AgentDrive MCP — connect any AI model[/]\n\n" + f"Resolved launcher: [green]{launcher.method}[/] → {launcher.command}\n" + f"[dim]{launcher.notes}[/]", + title="agentdrive mcp config", + border_style="cyan", + ) + ) console.print("\n[bold]Quick connect[/]\n") - console.print(" [green]agentdrive mcp install[/] # pip install [mcp] + write client configs") + console.print( + " [green]agentdrive mcp install[/] # pip install [mcp] + write client configs" + ) console.print(" [green]agentdrive mcp doctor[/] # verify tools + launcher") - console.print(" [green]agentdrive mcp config --write[/] # merge into Grok/Cursor/Claude/Continue configs") + console.print( + " [green]agentdrive mcp config --write[/] # merge into Grok/Cursor/Claude/Continue configs" + ) console.print(" [green]agentdrive mcp config --json[/] # machine-readable bundle") console.print("\n[bold]1. Grok[/]\n") @@ -2644,15 +2672,18 @@ def cmd_mcp_config(args: argparse.Namespace) -> int: console.print("\n[bold]2. Claude / Continue / Cursor[/]\n") console.print(_json.dumps({"mcpServers": bundle["mcpServers"]}, indent=2), highlight=False) - console.print("\n[dim]Cursor path: ~/.cursor/mcp.json · Claude: ~/.config/claude/claude_desktop_config.json[/]") + console.print( + "\n[dim]Cursor path: ~/.cursor/mcp.json · Claude: ~/.config/claude/claude_desktop_config.json[/]" + ) console.print("\n[bold]3. Clone / editable install fallback[/]\n") console.print( - f" command: [green]{launcher.command}[/]\n" - f" args: [green]{' '.join(launcher.args)}[/]" + f" command: [green]{launcher.command}[/]\n args: [green]{' '.join(launcher.args)}[/]" ) - console.print("\n[dim]Onboarding for models: docs/FOR_AI_MODELS.md · Full guide: docs/MCP.md[/]\n") + console.print( + "\n[dim]Onboarding for models: docs/FOR_AI_MODELS.md · Full guide: docs/MCP.md[/]\n" + ) return 0 @@ -2671,7 +2702,9 @@ def cmd_mcp_doctor(args: argparse.Namespace) -> int: f"{' '.join(launcher.get('args') or [])}" ) if report.get("ok"): - console.print(f"\n[green]MCP ready[/] — {report.get('tool_count', 0)} tools for your AI client") + console.print( + f"\n[green]MCP ready[/] — {report.get('tool_count', 0)} tools for your AI client" + ) return 0 console.print("\n[red]MCP not ready[/] — run: [green]agentdrive mcp install[/]") return 1 @@ -3132,7 +3165,9 @@ def cmd_ops(args: argparse.Namespace) -> int: if sub == "run": name = getattr(args, "operation_name", None) if not name: - console.print("[red]Usage: agentdrive ops run [--dry-run] [--json] [key=value ...][/]") + console.print( + "[red]Usage: agentdrive ops run [--dry-run] [--json] [key=value ...][/]" + ) return 1 if get_operation(name) is None: console.print(f"[red]Unknown operation:[/] {name}") @@ -3334,8 +3369,12 @@ def _register_drive_parsers( "task", help='Natural language task description (e.g. "security incident postmortem")', ) - pq.add_argument("--limit", type=int, default=5, help="Maximum number of results (default 5)") - pq.add_argument("--min-score", type=float, default=0.0, help="Minimum evaluation score filter") + pq.add_argument( + "--limit", type=int, default=5, help="Maximum number of results (default 5)" + ) + pq.add_argument( + "--min-score", type=float, default=0.0, help="Minimum evaluation score filter" + ) pq.set_defaults(func=cmd_pool) pst = pool_subs.add_parser( "stats", help="Full pool statistics (ingest counts, sources, actors, registry metrics)" @@ -3549,7 +3588,9 @@ def build_parser() -> argparse.ArgumentParser: ) p_config.set_defaults(func=cmd_mcp_config) - p_doctor = mcp_subs.add_parser("doctor", help="Verify MCP package, launcher, and tool registration") + p_doctor = mcp_subs.add_parser( + "doctor", help="Verify MCP package, launcher, and tool registration" + ) p_doctor.add_argument("--uvx", action="store_true", help="Test uvx launcher resolution") p_doctor.set_defaults(func=cmd_mcp_doctor) @@ -3652,12 +3693,22 @@ def build_parser() -> argparse.ArgumentParser: lr_list.set_defaults(func=cmd_learnings) lr_log = learn_subs.add_parser("log", help="Append one learning entry") - lr_log.add_argument("--key", required=True, help="Stable key (alphanumeric, hyphens, underscores)") + lr_log.add_argument( + "--key", required=True, help="Stable key (alphanumeric, hyphens, underscores)" + ) lr_log.add_argument("--insight", required=True, help="Learning insight text") lr_log.add_argument( "--type", default="pattern", - choices=["pattern", "pitfall", "preference", "architecture", "tool", "operational", "investigation"], + choices=[ + "pattern", + "pitfall", + "preference", + "architecture", + "tool", + "operational", + "investigation", + ], ) lr_log.add_argument("--confidence", type=int, default=5, help="1-10 confidence (default 5)") lr_log.add_argument( @@ -3782,6 +3833,69 @@ def build_parser() -> argparse.ArgumentParser: sk_run.add_argument("--json", dest="json_output", action="store_true") sk_run.set_defaults(func=cmd_skills) + sk_review = skills_subs.add_parser( + "review", + help="Review inherited skills using match/run evidence", + ) + sk_review.add_argument( + "--include-promoted", + action="store_true", + help="Include already-promoted inherited skills", + ) + sk_review.add_argument("--json", dest="json_output", action="store_true") + sk_review.set_defaults(func=cmd_skills) + + sk_assimilate = skills_subs.add_parser( + "assimilate", + help="Promote and optionally DNA-ingest proven inherited skills", + ) + sk_assimilate.add_argument( + "--no-dna", + action="store_true", + help="Promote proven skills without ingesting them into the DNA pool", + ) + sk_assimilate.add_argument( + "--prune", + action="store_true", + help="Also disable skills with prune recommendations", + ) + sk_assimilate.add_argument( + "--include-promoted", + action="store_true", + help="Also DNA-ingest already-promoted inherited skills", + ) + sk_assimilate.add_argument("--json", dest="json_output", action="store_true") + sk_assimilate.set_defaults(func=cmd_skills) + + sk_promote = skills_subs.add_parser( + "promote", + help="Promote an inherited skill into the parent bench", + ) + sk_promote.add_argument("skill_name", help="Inherited skill name") + sk_promote.add_argument("--json", dest="json_output", action="store_true") + sk_promote.set_defaults(func=cmd_skills) + + sk_prune = skills_subs.add_parser( + "prune", + help="Disable a weak inherited skill without deleting its file", + ) + sk_prune.add_argument("skill_name", help="Inherited skill name") + sk_prune.add_argument( + "--reason", + default="", + help="Reason stored in the skill frontmatter", + ) + sk_prune.add_argument("--json", dest="json_output", action="store_true") + sk_prune.set_defaults(func=cmd_skills) + + sk_dna = skills_subs.add_parser( + "dna", + help="Ingest an inherited/promoted skill into the DNA pool", + ) + sk_dna.add_argument("skill_name", help="Inherited or promoted skill name") + sk_dna.add_argument("--json", dest="json_output", action="store_true") + sk_dna.set_defaults(func=cmd_skills) + sk_init = skills_subs.add_parser( "init", help="Scaffold ~/.agentdrive/skills//SKILL.md", @@ -3863,7 +3977,9 @@ def _register_graph_parsers(names: tuple[str, ...], help_text: str) -> None: gr.add_argument("--swarm-id", dest="swarm_id") gr.add_argument("--cycle-id", dest="cycle_id") gr.add_argument("--summary", help="Short reasoning summary") - gr.add_argument("--reasoning-file", dest="reasoning_file", help="JSON reasoning object file") + gr.add_argument( + "--reasoning-file", dest="reasoning_file", help="JSON reasoning object file" + ) gr.add_argument("--dry-run", action="store_true") gr.add_argument("--json", dest="json_output", action="store_true") gr.set_defaults(func=cmd_graph) @@ -3879,6 +3995,42 @@ def _register_graph_parsers(names: tuple[str, ...], help_text: str) -> None: "Alias for graph (Experience Graph commands)", ) + # multiverse — parallel timeline superposition for Parent decisions + mv = subparsers.add_parser( + "multiverse", + help="Multiverse Cognition: spawn branches, collapse path, record Parent decision", + ) + mv_subs = mv.add_subparsers(dest="multiverse_subcommand") + + mvr = mv_subs.add_parser( + "run", + help="Full multiverse pipeline + record_parent_decision (canonical Parent hook)", + ) + mvr.add_argument("--trigger", "-t", help="Decision question / problem statement") + mvr.add_argument("--branches", type=int, default=7, help="Parallel branch count") + mvr.add_argument("--forward-steps", dest="forward_steps", type=int, help="Simulation depth") + mvr.add_argument("--swarm-id", dest="swarm_id") + mvr.add_argument("--program-id", dest="program_id", help="AD-Grid program attribution") + mvr.add_argument("--dry-run", action="store_true") + mvr.add_argument("--json", dest="json_output", action="store_true") + mvr.set_defaults(func=cmd_multiverse) + + mvl = mv_subs.add_parser("list", help="List recent multiverse sessions") + mvl.add_argument("--limit", type=int, default=10) + mvl.add_argument("--swarm-id", dest="swarm_id") + mvl.add_argument("--dry-run", action="store_true") + mvl.add_argument("--json", dest="json_output", action="store_true") + mvl.set_defaults(func=cmd_multiverse) + + mvs = mv_subs.add_parser("status", help="Show one multiverse session by id") + mvs.add_argument("--session-id", dest="session_id", required=True) + mvs.add_argument("--swarm-id", dest="swarm_id") + mvs.add_argument("--dry-run", action="store_true") + mvs.add_argument("--json", dest="json_output", action="store_true") + mvs.set_defaults(func=cmd_multiverse) + + mv.set_defaults(func=cmd_multiverse, multiverse_subcommand="run") + # eval — artifact replay p = subparsers.add_parser("eval", help="Evaluation utilities (harness replay)") eval_subs = p.add_subparsers(dest="eval_subcommand") @@ -3901,7 +4053,9 @@ def _register_graph_parsers(names: tuple[str, ...], help_text: str) -> None: gp_steps.set_defaults(func=cmd_golden_path) gp_verify = gp_subs.add_parser("verify", help="Verify golden-path step completion") - gp_verify.add_argument("--step", help="Verify one step (install, doctor, mcp, seed, think, learnings, query)") + gp_verify.add_argument( + "--step", help="Verify one step (install, doctor, mcp, seed, think, learnings, query)" + ) gp_verify.add_argument("--skip-optional", action="store_true", help="Skip optional seed check") gp_verify.add_argument("--json", dest="json_output", action="store_true") gp_verify.set_defaults(func=cmd_golden_path) @@ -3913,7 +4067,9 @@ def _register_graph_parsers(names: tuple[str, ...], help_text: str) -> None: action="store_true", help="Fail think step when no AI provider is configured (default: skip with hint)", ) - gp_run.add_argument("--continue-on-fail", action="store_true", help="Keep going after a failed step") + gp_run.add_argument( + "--continue-on-fail", action="store_true", help="Keep going after a failed step" + ) gp_run.add_argument("--json", dest="json_output", action="store_true") gp_run.set_defaults(func=cmd_golden_path) diff --git a/src/agentdrive/cli_catalog.py b/src/agentdrive/cli_catalog.py index d06799e..4bbd5b4 100644 --- a/src/agentdrive/cli_catalog.py +++ b/src/agentdrive/cli_catalog.py @@ -93,16 +93,36 @@ class CatalogEntry: CatalogEntry("scan", "Scan runs/trajectories for candidate genomes", "setup"), # drive CatalogEntry("drive status", "Drive status and recent activity", "drive", "pool_status", True), - CatalogEntry("drive stats", "Detailed pool and registry statistics", "drive", "pool_stats", True), + CatalogEntry( + "drive stats", "Detailed pool and registry statistics", "drive", "pool_stats", True + ), CatalogEntry("drive query", "Semantic genome search for a task", "drive", "pool_query", True), - CatalogEntry("drive ingest", "Ingest a genome directory into the Drive", "drive", "ingest_genome", False), + CatalogEntry( + "drive ingest", "Ingest a genome directory into the Drive", "drive", "ingest_genome", False + ), CatalogEntry("pool", "Alias for drive (same subcommands)", "drive"), # synthesis - CatalogEntry("think", "Cited Drive.think synthesis with gap analysis", "synthesis", "think", True), + CatalogEntry( + "think", "Cited Drive.think synthesis with gap analysis", "synthesis", "think", True + ), # patterns - CatalogEntry("patterns list", "List Fabric-style pattern catalog", "patterns", "patterns_list", True), - CatalogEntry("patterns show", "Show one pattern metadata and system.md", "patterns", "patterns_show", True), - CatalogEntry("patterns apply", "Compose pattern prompt with {{input}}", "patterns", "patterns_apply", True), + CatalogEntry( + "patterns list", "List Fabric-style pattern catalog", "patterns", "patterns_list", True + ), + CatalogEntry( + "patterns show", + "Show one pattern metadata and system.md", + "patterns", + "patterns_show", + True, + ), + CatalogEntry( + "patterns apply", + "Compose pattern prompt with {{input}}", + "patterns", + "patterns_apply", + True, + ), CatalogEntry( "patterns import-fabric", "Import Fabric patterns into ~/.agentdrive/patterns", @@ -111,11 +131,25 @@ class CatalogEntry: False, ), # learnings - CatalogEntry("learnings list", "Recent operational learnings for project slug", "learnings", "learnings_list", True), + CatalogEntry( + "learnings list", + "Recent operational learnings for project slug", + "learnings", + "learnings_list", + True, + ), CatalogEntry("learnings log", "Append one learning entry", "learnings", "learnings_log", False), - CatalogEntry("learnings search", "Token search over learnings key/insight", "learnings", None, True), + CatalogEntry( + "learnings search", "Token search over learnings key/insight", "learnings", None, True + ), # harness - CatalogEntry("harness compose", "Compose harness prompt (DNA + learnings + Fabric)", "harness", "harness_compose", True), + CatalogEntry( + "harness compose", + "Compose harness prompt (DNA + learnings + Fabric)", + "harness", + "harness_compose", + True, + ), # experience CatalogEntry( "graph context-pack", @@ -139,9 +173,46 @@ class CatalogEntry: True, ), CatalogEntry("experience", "Alias for graph (Experience Graph commands)", "experience"), + # multiverse + CatalogEntry( + "multiverse run", + "Full multiverse pipeline + Parent decision record", + "multiverse", + "multiverse_parent_decision", + False, + ), + CatalogEntry( + "multiverse external", + "Submit MCP model multiverse branches (Grok/Claude/Codex)", + "multiverse", + "external_parent_decision", + False, + ), + CatalogEntry( + "multiverse list", + "List recent multiverse sessions", + "multiverse", + "multiverse_list_sessions", + True, + ), + CatalogEntry( + "multiverse status", + "Show one multiverse session", + "multiverse", + "multiverse_get_session", + True, + ), # reconcile - CatalogEntry("reconcile run", "Single reconciliation pass over local Drive", "reconcile", "reconcile_scan", False), - CatalogEntry("reconcile status", "Persisted reconciliation state", "reconcile", "reconcile_status", True), + CatalogEntry( + "reconcile run", + "Single reconciliation pass over local Drive", + "reconcile", + "reconcile_scan", + False, + ), + CatalogEntry( + "reconcile status", "Persisted reconciliation state", "reconcile", "reconcile_status", True + ), CatalogEntry( "reconcile seed-experience-v3", "First-run recovery: experience layer v3 seed + KG bootstrap", @@ -150,7 +221,9 @@ class CatalogEntry: False, ), # sprint - CatalogEntry("sprint ship", "Reconcile → test → think_gaps ship chain", "sprint", "sprint_ship", False), + CatalogEntry( + "sprint ship", "Reconcile → test → think_gaps ship chain", "sprint", "sprint_ship", False + ), CatalogEntry("sprint status", "Pending sprint checkpoints", "sprint", "sprint_status", True), CatalogEntry("sprint ack", "Acknowledge a sprint checkpoint", "sprint"), # dream @@ -166,7 +239,13 @@ class CatalogEntry: CatalogEntry("mcp doctor", "Verify MCP package, launcher, tools", "mcp"), CatalogEntry("mcp tools", "List MCP tools exposed by server", "mcp"), # mission_control - CatalogEntry("cap mint-mission", "Mint Mission Control mutating-command cap", "mission_control", "cap_mint_mission", False), + CatalogEntry( + "cap mint-mission", + "Mint Mission Control mutating-command cap", + "mission_control", + "cap_mint_mission", + False, + ), # genomes CatalogEntry("genomes list", "List registered genomes", "genomes"), CatalogEntry("genomes info", "Show one genome by ID", "genomes"), @@ -175,6 +254,15 @@ class CatalogEntry: CatalogEntry("skills list", "List SKILL.md capabilities", "discovery"), CatalogEntry("skills show", "Show one skill metadata and body", "discovery"), CatalogEntry("skills run", "Run a skill (same path as /skill in chat)", "discovery"), + CatalogEntry("skills review", "Review inherited skills using usage evidence", "discovery"), + CatalogEntry( + "skills assimilate", + "Promote and optionally DNA-ingest proven inherited skills", + "discovery", + ), + CatalogEntry("skills promote", "Promote a proven inherited skill", "discovery"), + CatalogEntry("skills prune", "Disable a weak inherited skill without deleting it", "discovery"), + CatalogEntry("skills dna", "Ingest an inherited/promoted skill into the DNA pool", "discovery"), CatalogEntry("skills init", "Scaffold a new SKILL.md under ~/.agentdrive/skills", "discovery"), # config CatalogEntry("config show", "Show configuration", "config"), @@ -253,11 +341,13 @@ def format_epilog() -> str: " agentdrive doctor Health check", "", "Core workflows:", - " agentdrive think \"question\" Cited synthesis + gaps", - " agentdrive drive query \"task\" Semantic genome search", - " agentdrive learnings log --key K --insight \"...\"", - " agentdrive harness compose --task \"...\"", + ' agentdrive think "question" Cited synthesis + gaps', + ' agentdrive drive query "task" Semantic genome search', + ' agentdrive learnings log --key K --insight "..."', + ' agentdrive harness compose --task "..."', " agentdrive graph context-pack Experience Graph briefing pack", + " agentdrive multiverse run Multiverse Cognition → Parent decision", + " agentdrive multiverse list Recent superposition sessions", " agentdrive sprint ship gstack-style ship chain", " agentdrive dream run Phased maintenance cycle", " agentdrive mcp install Wire MCP into Grok/Cursor/Claude", @@ -285,4 +375,4 @@ def iter_tree_lines() -> Iterable[str]: yield f"[{label}]" for entry in entries: op = f" ({entry.operation})" if entry.operation else "" - yield f" agentdrive {entry.command}{op}" \ No newline at end of file + yield f" agentdrive {entry.command}{op}" diff --git a/src/agentdrive/cli_repl.py b/src/agentdrive/cli_repl.py index 7700d77..63fc1cf 100644 --- a/src/agentdrive/cli_repl.py +++ b/src/agentdrive/cli_repl.py @@ -121,8 +121,7 @@ def run_repl(*, parser: argparse.ArgumentParser | None = None) -> int: from prompt_toolkit.history import FileHistory except ImportError: console.print( - "[red]prompt_toolkit is required for the REPL.[/] " - "[dim]pip install prompt_toolkit[/]" + "[red]prompt_toolkit is required for the REPL.[/] [dim]pip install prompt_toolkit[/]" ) return 1 @@ -143,7 +142,7 @@ def run_repl(*, parser: argparse.ArgumentParser | None = None) -> int: console.print() console.print("[bold]AgentDrive operator REPL[/]") console.print( - "[dim]Dispatch any subcommand (doctor, golden-path verify, think \"…\"). " + '[dim]Dispatch any subcommand (doctor, golden-path verify, think "…"). ' "search · help · exit[/]" ) console.print() @@ -167,4 +166,4 @@ def run_repl(*, parser: argparse.ArgumentParser | None = None) -> int: def cmd_repl(_args: argparse.Namespace) -> int: """CLI entry: ``agentdrive repl``.""" - return run_repl() \ No newline at end of file + return run_repl() diff --git a/src/agentdrive/cli_surface.py b/src/agentdrive/cli_surface.py index de62ea8..b46a604 100644 --- a/src/agentdrive/cli_surface.py +++ b/src/agentdrive/cli_surface.py @@ -76,6 +76,42 @@ def print_operation_result( console.print(text) return + if name in ("multiverse_parent_decision", "multiverse_run_full") and result.get("result"): + payload = result["result"] + session = payload.get("session") or {} + console.print() + console.print(f"[cyan]session[/] {payload.get('session_id') or session.get('session_id')}") + console.print(f"[cyan]collapsed[/] {payload.get('collapsed_branch_id')}") + console.print(f"[cyan]policy[/] {payload.get('collapse_policy')}") + if payload.get("collapse_reason"): + console.print(f"[cyan]reason[/] {payload['collapse_reason']}") + invs = (session.get("invariants") or []) if isinstance(session, dict) else [] + if invs: + console.print() + console.print("[yellow]Invariants:[/]") + for inv in invs[:5]: + if isinstance(inv, dict): + console.print(f" • [{inv.get('kind')}] {inv.get('statement')}") + return + + if name == "multiverse_list_sessions" and result.get("sessions") is not None: + sessions = result["sessions"] + console.print() + console.print(f"[cyan]Recent sessions[/] ({result.get('count', len(sessions))})") + for s in sessions[:8]: + if isinstance(s, dict): + console.print( + f" • {s.get('session_id')} [{s.get('status')}] " + f"branches={s.get('branch_count')} collapsed={s.get('collapsed_branch_id')}" + ) + return + + if name == "multiverse_get_session" and result.get("session"): + s = result["session"] + console.print() + console.print(json.dumps(s, indent=2, default=str)[:preview_limit]) + return + if name == "experience_graph_context_pack" and result.get("context_pack"): pack = result["context_pack"] if isinstance(pack, dict): @@ -106,7 +142,7 @@ def cmd_think(args: argparse.Namespace) -> int: """Cited Drive.think synthesis with mandatory gap analysis.""" question = getattr(args, "question", None) or "" if not question.strip(): - console.print("[red]Usage: agentdrive think \"your question\"[/]") + console.print('[red]Usage: agentdrive think "your question"[/]') return 1 kwargs: dict[str, Any] = { "question": question.strip(), @@ -124,9 +160,7 @@ def cmd_learnings(args: argparse.Namespace) -> int: if sub == "log": insight = getattr(args, "insight", None) or "" if not insight.strip(): - console.print( - "[red]Usage: agentdrive learnings log --key --insight \"...\"[/]" - ) + console.print('[red]Usage: agentdrive learnings log --key --insight "..."[/]') return 1 kwargs: dict[str, Any] = { "key": getattr(args, "key", None) or "cli-entry", @@ -236,9 +270,7 @@ def cmd_harness(args: argparse.Namespace) -> int: kwargs["dry_run"] = True if not kwargs.get("task") and not kwargs.get("base_prompt") and sys.stdin.isatty(): - console.print( - "[red]Usage: agentdrive harness compose --task \"...\" [--pattern NAME][/]" - ) + console.print('[red]Usage: agentdrive harness compose --task "..." [--pattern NAME][/]') return 1 return _run_op("harness_compose", kwargs, json_output=getattr(args, "json_output", False)) @@ -301,7 +333,7 @@ def cmd_graph(args: argparse.Namespace) -> int: } else: console.print( - "[red]Usage: agentdrive graph record --summary \"...\" " + '[red]Usage: agentdrive graph record --summary "..." ' "or --reasoning-file path.json[/]" ) return 1 @@ -317,6 +349,69 @@ def cmd_graph(args: argparse.Namespace) -> int: return 1 +def cmd_multiverse(args: argparse.Namespace) -> int: + """Multiverse Cognition: parallel timeline superposition for Parent decisions.""" + sub = getattr(args, "multiverse_subcommand", None) or "run" + + if sub == "run": + trigger = getattr(args, "trigger", None) + if not trigger and not sys.stdin.isatty(): + trigger = sys.stdin.read().strip() + if not trigger: + console.print('[red]Usage: agentdrive multiverse run --trigger "decision question"[/]') + return 1 + kwargs: dict[str, Any] = { + "trigger": trigger, + "n_branches": getattr(args, "branches", 7), + } + if getattr(args, "forward_steps", None) is not None: + kwargs["forward_steps"] = args.forward_steps + if getattr(args, "swarm_id", None): + kwargs["swarm_id"] = args.swarm_id + if getattr(args, "program_id", None): + kwargs["program_id"] = args.program_id + if getattr(args, "dry_run", False): + kwargs["dry_run"] = True + return _run_op( + "multiverse_parent_decision", + kwargs, + json_output=getattr(args, "json_output", False), + ) + + if sub == "list": + kwargs = {"limit": getattr(args, "limit", 10)} + if getattr(args, "swarm_id", None): + kwargs["swarm_id"] = args.swarm_id + if getattr(args, "dry_run", False): + kwargs["dry_run"] = True + return _run_op( + "multiverse_list_sessions", + kwargs, + json_output=getattr(args, "json_output", False), + ) + + if sub == "status": + session_id = getattr(args, "session_id", None) + if not session_id: + console.print( + "[red]Usage: agentdrive multiverse status --session-id multiverse-session:...[/]" + ) + return 1 + kwargs = {"session_id": session_id} + if getattr(args, "swarm_id", None): + kwargs["swarm_id"] = args.swarm_id + if getattr(args, "dry_run", False): + kwargs["dry_run"] = True + return _run_op( + "multiverse_get_session", + kwargs, + json_output=getattr(args, "json_output", False), + ) + + console.print("[red]Unknown multiverse subcommand[/]") + return 1 + + def cmd_eval(args: argparse.Namespace) -> int: """Evaluation utilities (artifact replay).""" sub = getattr(args, "eval_subcommand", None) or "replay" @@ -466,7 +561,9 @@ def cmd_session(args: argparse.Namespace) -> int: if not path.exists(): console.print(f"[yellow]No events file at[/] {path}") return 1 - filter_note = f" · filter {type_filter} ({len(filtered)}/{len(events)})" if type_filter else "" + filter_note = ( + f" · filter {type_filter} ({len(filtered)}/{len(events)})" if type_filter else "" + ) console.print(f"[dim]{format_type_histogram(counts)}[/]{filter_note}\n") for ev in filtered: console.print(format_event_summary(ev)) @@ -491,7 +588,9 @@ def cmd_session(args: argparse.Namespace) -> int: if not path.exists(): console.print(f"[yellow]No events file at[/] {path}") return 1 - filter_note = f" · filter {type_filter} ({len(filtered)}/{len(events)})" if type_filter else "" + filter_note = ( + f" · filter {type_filter} ({len(filtered)}/{len(events)})" if type_filter else "" + ) console.print( f"[bold]Session replay[/] · agent={agent_id} · session={resolved}{filter_note}" ) @@ -538,6 +637,13 @@ def cmd_session(args: argparse.Namespace) -> int: def cmd_skills(args: argparse.Namespace) -> int: """SKILL.md registry — list, show, run (Pattern 5).""" from agentdrive.skills import get_skill, list_skills, run_skill + from agentdrive.skills.curation import ( + assimilate_inherited_skills, + ingest_skill_as_dna, + promote_inherited_skill, + prune_inherited_skill, + review_inherited_skills, + ) from agentdrive.skills.runner import format_skill_result sub = getattr(args, "skills_subcommand", None) or "list" @@ -570,7 +676,9 @@ def cmd_skills(args: argparse.Namespace) -> int: console.print(f"[dim]No skills for harness={harness_filter}[/]") return 0 if not entries: - console.print("[dim]No skills found. Add SKILL.md under ~/.agentdrive/skills//[/]") + console.print( + "[dim]No skills found. Add SKILL.md under ~/.agentdrive/skills//[/]" + ) return 0 if harness_filter: table = Table(title=f"Skills · {harness_filter} ({len(entries)})", show_header=True) @@ -591,7 +699,9 @@ def cmd_skills(args: argparse.Namespace) -> int: "codex": "Codex harness", } total = sum(len(v) for v in tiers.values()) - console.print(f"[bold]Skills bench[/] · {total} total · tiers 1–4 work on MCP with any model\n") + console.print( + f"[bold]Skills bench[/] · {total} total · tiers 1–4 work on MCP with any model\n" + ) for tier, label in tier_labels.items(): group = tiers.get(tier, []) if not group: @@ -657,6 +767,120 @@ def cmd_skills(args: argparse.Namespace) -> int: console.print(format_skill_result(result)) return 0 + if sub == "review": + include_promoted = bool(getattr(args, "include_promoted", False)) + reviews = review_inherited_skills(include_promoted=include_promoted) + if json_output: + emit_json([r.to_dict() for r in reviews]) + return 0 + if not reviews: + console.print("[dim]No inherited skills to review.[/]") + return 0 + table = Table(title="Inherited skill review", show_header=True, expand=True) + table.add_column("Skill", style="cyan", no_wrap=True) + table.add_column("Decision", no_wrap=True) + table.add_column("Runs", justify="right") + table.add_column("OK", justify="right") + table.add_column("Fail", justify="right") + table.add_column("Matches", justify="right") + table.add_column("Reason", overflow="fold") + for item in reviews: + table.add_row( + item.name, + item.recommendation, + str(item.runs), + str(item.successes), + str(item.failures), + str(item.matches), + item.reason, + ) + console.print(table) + return 0 + + if sub == "assimilate": + ingest_dna = not bool(getattr(args, "no_dna", False)) + prune = bool(getattr(args, "prune", False)) + include_promoted = bool(getattr(args, "include_promoted", False)) + report = assimilate_inherited_skills( + ingest_dna=ingest_dna, + prune=prune, + include_promoted=include_promoted, + ) + if json_output: + emit_json(report.to_dict()) + return 0 if not report.errors else 1 + console.print( + "[green]Assimilated inherited skills[/] " + f"reviewed={report.reviewed} " + f"promoted={len(report.promoted)} " + f"dna={len(report.dna_exports)} " + f"pruned={len(report.pruned)} " + f"errors={len(report.errors)}" + ) + for item in report.promoted: + console.print(f" [cyan]promoted[/] {item.name} ({item.reason})") + for export in report.dna_exports: + status = "accepted" if export.accepted else "not accepted" + console.print(f" [magenta]dna[/] {export.skill_name} -> {export.genome_id} ({status})") + for item in report.pruned: + console.print(f" [yellow]pruned[/] {item['skill_name']} ({item['reason']})") + for item in report.errors: + console.print(f" [red]error[/] {item['skill_name']} {item['action']}: {item['error']}") + return 0 if not report.errors else 1 + + if sub == "promote": + name = getattr(args, "skill_name", None) or "" + if not name.strip(): + console.print("[red]Usage: agentdrive skills promote [/]") + return 1 + try: + review = promote_inherited_skill(name.strip()) + except ValueError as exc: + console.print(f"[red]{exc}[/]") + return 1 + if json_output: + emit_json(review.to_dict()) + return 0 + console.print( + f"[green]Promoted[/] {review.name} " + f"({review.successes}/{review.runs} successful runs, {review.matches} matches)" + ) + return 0 + + if sub == "prune": + name = getattr(args, "skill_name", None) or "" + reason = getattr(args, "reason", None) or "" + if not name.strip(): + console.print("[red]Usage: agentdrive skills prune [/]") + return 1 + try: + path = prune_inherited_skill(name.strip(), reason=reason) + except ValueError as exc: + console.print(f"[red]{exc}[/]") + return 1 + if json_output: + emit_json({"success": True, "name": name.strip(), "path": str(path)}) + return 0 + console.print(f"[yellow]Pruned[/] {name.strip()} (disabled in {path})") + return 0 + + if sub == "dna": + name = getattr(args, "skill_name", None) or "" + if not name.strip(): + console.print("[red]Usage: agentdrive skills dna [/]") + return 1 + try: + export = ingest_skill_as_dna(name.strip()) + except ValueError as exc: + console.print(f"[red]{exc}[/]") + return 1 + if json_output: + emit_json(export.to_dict()) + return 0 if export.accepted else 1 + status = "[green]Ingested[/]" if export.accepted else "[yellow]Not accepted[/]" + console.print(f"{status} {export.skill_name} -> {export.genome_id} ({export.reason})") + return 0 if export.accepted else 1 + if sub == "init": from agentdrive.skills.registry import init_skill @@ -785,4 +1009,4 @@ def cmd_golden_path(args: argparse.Namespace) -> int: def build_help_epilog() -> str: """Epilog string for the root argparse parser.""" - return format_epilog() \ No newline at end of file + return format_epilog() diff --git a/src/agentdrive/codebase/__init__.py b/src/agentdrive/codebase/__init__.py new file mode 100644 index 0000000..87c4762 --- /dev/null +++ b/src/agentdrive/codebase/__init__.py @@ -0,0 +1,28 @@ +"""Codebase pattern recognition — mirror-neuron mimicry as AD observes code.""" + +from agentdrive.codebase.framework import ( + crystallize_framework, + get_writing_guide, + match_against_framework, +) +from agentdrive.codebase.mirrors import ( + fire_mirrors_for_intent, + global_mirror_field, + transform_toward_style, +) +from agentdrive.codebase.observe import observe_file, observe_text +from agentdrive.codebase.registry import get_project, list_projects, register_project + +__all__ = [ + "register_project", + "list_projects", + "get_project", + "observe_file", + "observe_text", + "crystallize_framework", + "get_writing_guide", + "match_against_framework", + "fire_mirrors_for_intent", + "transform_toward_style", + "global_mirror_field", +] diff --git a/src/agentdrive/codebase/analyzer.py b/src/agentdrive/codebase/analyzer.py new file mode 100644 index 0000000..679943b --- /dev/null +++ b/src/agentdrive/codebase/analyzer.py @@ -0,0 +1,176 @@ +"""Heuristic file analysis — extract writing-style signals without AST deps.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +_PY_CLASS = re.compile(r"^class\s+([A-Za-z_][A-Za-z0-9_]*)", re.MULTILINE) +_PY_FUNC = re.compile(r"^def\s+([A-Za-z_][A-Za-z0-9_]*)", re.MULTILINE) +_PY_ASYNC = re.compile(r"^async\s+def\s+", re.MULTILINE) +_PY_TYPE_HINT = re.compile(r"->\s*[^:]+:|:\s*[A-Za-z_\[]") +_PY_DATACLASS = re.compile(r"@dataclass") +_PY_LOGGER = re.compile(r"logger\s*=\s*logging\.getLogger") +_PY_REL_IMPORT = re.compile(r"^from\s+\.", re.MULTILINE) +_PY_ABS_IMPORT = re.compile(r"^from\s+[a-zA-Z]", re.MULTILINE) + +_TS_FUNC = re.compile(r"(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)") +_TS_ARROW = re.compile(r"(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?\(") +_TS_INTERFACE = re.compile(r"(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)") +_TS_TYPE = re.compile(r"(?:export\s+)?type\s+([A-Za-z_$][\w$]*)") +_TS_HOOK = re.compile(r"use[A-Z][A-Za-z]+") + +_CAMEL = re.compile(r"^[a-z]+(?:[A-Z][a-z0-9]*)+$") +_SNAKE = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$") +_PASCAL = re.compile(r"^[A-Z][a-zA-Z0-9]*$") + + +@dataclass +class FileSignals: + path: str + language: str + lines: int + signals: dict[str, Any] = field(default_factory=dict) + identifiers: dict[str, list[str]] = field(default_factory=dict) + frameworks: list[str] = field(default_factory=list) + + def to_dict(self) -> dict[str, Any]: + return { + "path": self.path, + "language": self.language, + "lines": self.lines, + "signals": self.signals, + "identifiers": self.identifiers, + "frameworks": self.frameworks, + } + + +def detect_language(path: str) -> str: + ext = Path(path).suffix.lower() + return { + ".py": "python", + ".ts": "typescript", + ".tsx": "typescript", + ".js": "javascript", + ".jsx": "javascript", + ".go": "go", + ".rs": "rust", + ".md": "markdown", + ".json": "json", + ".yaml": "yaml", + ".yml": "yaml", + }.get(ext, "unknown") + + +def _naming_style(names: list[str]) -> str: + if not names: + return "unknown" + snake = sum(1 for n in names if _SNAKE.match(n)) + camel = sum(1 for n in names if _CAMEL.match(n)) + pascal = sum(1 for n in names if _PASCAL.match(n)) + total = len(names) + if snake / total >= 0.6: + return "snake_case" + if camel / total >= 0.5: + return "camelCase" + if pascal / total >= 0.5: + return "PascalCase" + return "mixed" + + +def _detect_frameworks(content: str, path: str) -> list[str]: + hits: list[str] = [] + checks = [ + ("fastapi", r"FastAPI|from fastapi"), + ("pytest", r"import pytest|@pytest"), + ("nextjs", r"from ['\"]next/|next\.config"), + ("react", r"from ['\"]react['\"]|useState|useEffect"), + ("clerk", r"@clerk/|clerkMiddleware"), + ("convex", r"from ['\"]convex|convex/"), + ("pydantic", r"from pydantic|BaseModel"), + ("mcp", r"FastMCP|mcp\.server"), + ("tailwind", r"tailwindcss|@tailwind"), + ] + blob = content[:8000] + for name, pattern in checks: + if re.search(pattern, blob): + hits.append(name) + if "/test" in path or path.startswith("test_") or "_test." in path: + if "pytest" not in hits: + hits.append("tests") + return hits + + +def analyze_content(*, path: str, content: str) -> FileSignals: + language = detect_language(path) + lines = content.count("\n") + (1 if content else 0) + signals: dict[str, Any] = { + "has_module_docstring": False, + "uses_type_hints": False, + "uses_async": False, + "uses_dataclass": False, + "uses_structured_logging": False, + "import_style": "unknown", + "comment_density": 0.0, + "test_file": False, + } + identifiers: dict[str, list[str]] = { + "functions": [], + "classes": [], + "types": [], + } + frameworks = _detect_frameworks(content, path) + + if language == "python": + stripped = content.lstrip() + signals["has_module_docstring"] = stripped.startswith(('"""', "'''")) + signals["uses_type_hints"] = bool(_PY_TYPE_HINT.search(content)) + signals["uses_async"] = bool(_PY_ASYNC.search(content)) + signals["uses_dataclass"] = bool(_PY_DATACLASS.search(content)) + signals["uses_structured_logging"] = bool(_PY_LOGGER.search(content)) + rel = len(_PY_REL_IMPORT.findall(content)) + ab = len(_PY_ABS_IMPORT.findall(content)) + if rel > ab: + signals["import_style"] = "relative" + elif ab > 0: + signals["import_style"] = "absolute" + identifiers["functions"] = _PY_FUNC.findall(content)[:40] + identifiers["classes"] = _PY_CLASS.findall(content)[:30] + signals["function_naming"] = _naming_style(identifiers["functions"]) + signals["class_naming"] = _naming_style(identifiers["classes"]) + + elif language in ("typescript", "javascript"): + identifiers["functions"] = _TS_FUNC.findall(content)[:40] + _TS_ARROW.findall(content)[:20] + identifiers["types"] = _TS_INTERFACE.findall(content)[:20] + _TS_TYPE.findall(content)[:20] + signals["function_naming"] = _naming_style(identifiers["functions"]) + signals["uses_react_hooks"] = bool(_TS_HOOK.search(content)) + if content.strip().startswith("/**") or content.lstrip().startswith("//"): + signals["has_module_docstring"] = True + signals["uses_async"] = "async " in content + if "import type" in content: + signals["import_style"] = "type_imports" + elif "from '@/" in content or 'from "@/' in content: + signals["import_style"] = "path_alias" + + comment_lines = sum( + 1 for line in content.splitlines() if line.strip().startswith(("#", "//", "/*", "*")) + ) + signals["comment_density"] = round(comment_lines / max(1, lines), 3) + signals["test_file"] = ( + "/test" in path.replace("\\", "/") + or path.startswith("test_") + or path.endswith("_test.py") + or ".test." in path + or ".spec." in path + ) + + return FileSignals( + path=path, + language=language, + lines=lines, + signals=signals, + identifiers=identifiers, + frameworks=frameworks, + ) diff --git a/src/agentdrive/codebase/exemplars.py b/src/agentdrive/codebase/exemplars.py new file mode 100644 index 0000000..ab2a513 --- /dev/null +++ b/src/agentdrive/codebase/exemplars.py @@ -0,0 +1,107 @@ +"""Extract motor-program exemplars from observed source (what to imitate).""" + +from __future__ import annotations + +import re +from typing import Any + +_PY_FUNC_BLOCK = re.compile( + r"^((?:async\s+)?def\s+[A-Za-z_][A-Za-z0-9_]*\([^)]*\)(?:\s*->[^:]+)?:)(.*?)(?=^(?:async\s+)?def\s+|^class\s+|\Z)", + re.MULTILINE | re.DOTALL, +) +_PY_CLASS_BLOCK = re.compile( + r"^(class\s+[A-Za-z_][A-Za-z0-9_]*[^:]*:)(.*?)(?=^class\s+|^(?:async\s+)?def\s+|\Z)", + re.MULTILINE | re.DOTALL, +) +_TS_EXPORT = re.compile( + r"^((?:export\s+)?(?:async\s+)?function\s+[A-Za-z_$][\w$]*\([^)]*\)[^{]*\{)(.*?)(?=^(?:export\s+)?(?:async\s+)?function\s+|\Z)", + re.MULTILINE | re.DOTALL, +) +_IMPORT_LINE = re.compile(r"^(?:from|import)\s+.+$", re.MULTILINE) + + +def _trim_body(body: str, *, max_chars: int = 480) -> str: + lines = [ln for ln in body.splitlines() if ln.strip()] + text = "\n".join(lines[:12]) + if len(text) > max_chars: + text = text[: max_chars - 3].rstrip() + "..." + return text + + +def extract_exemplars(*, path: str, content: str, language: str) -> list[dict[str, Any]]: + """Pull concrete code fragments the mirror layer can replay (motor programs).""" + exemplars: list[dict[str, Any]] = [] + imports = _IMPORT_LINE.findall(content)[:6] + + if language == "python": + for match in _PY_FUNC_BLOCK.finditer(content): + header = match.group(1).strip() + body = _trim_body(match.group(2)) + name_m = re.search(r"def\s+([A-Za-z_][A-Za-z0-9_]*)", header) + if not name_m: + continue + exemplars.append( + { + "kind": "function", + "name": name_m.group(1), + "signature": header, + "body": body, + "motor_template": f"{header}\n{body}", + "path": path, + } + ) + if len(exemplars) >= 6: + break + for match in _PY_CLASS_BLOCK.finditer(content): + header = match.group(1).strip() + body = _trim_body(match.group(2), max_chars=360) + name_m = re.search(r"class\s+([A-Za-z_][A-Za-z0-9_]*)", header) + if not name_m: + continue + exemplars.append( + { + "kind": "class", + "name": name_m.group(1), + "signature": header, + "body": body, + "motor_template": f"{header}\n{body}", + "path": path, + } + ) + if len(exemplars) >= 8: + break + + elif language in ("typescript", "javascript"): + for match in _TS_EXPORT.finditer(content): + header = match.group(1).strip() + body = _trim_body(match.group(2)) + name_m = re.search(r"function\s+([A-Za-z_$][\w$]*)", header) + if not name_m: + continue + exemplars.append( + { + "kind": "function", + "name": name_m.group(1), + "signature": header, + "body": body, + "motor_template": f"{header}\n{body}", + "path": path, + } + ) + if len(exemplars) >= 6: + break + + if imports: + exemplars.insert( + 0, + { + "kind": "imports", + "name": "import_block", + "signature": "", + "body": "\n".join(imports), + "motor_template": "\n".join(imports), + "path": path, + }, + ) + + return exemplars[:10] diff --git a/src/agentdrive/codebase/framework.py b/src/agentdrive/codebase/framework.py new file mode 100644 index 0000000..47525a4 --- /dev/null +++ b/src/agentdrive/codebase/framework.py @@ -0,0 +1,332 @@ +"""Crystallize observations into a project-specific pattern recognition framework.""" + +from __future__ import annotations + +import json +import logging +from collections import Counter, defaultdict +from datetime import UTC, datetime +from typing import Any + +from agentdrive.codebase.registry import framework_path, get_project, observations_path + +logger = logging.getLogger(__name__) + +_CRYSTALLIZE_EVERY = 3 + + +def _load_observations(project_id: str) -> list[dict[str, Any]]: + path = observations_path(project_id) + if not path.is_file(): + return [] + rows: list[dict[str, Any]] = [] + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + try: + row = json.loads(line) + if isinstance(row, dict): + rows.append(row) + except json.JSONDecodeError: + continue + return rows + + +def _aggregate(observations: list[dict[str, Any]]) -> dict[str, Any]: + if not observations: + return {"patterns": [], "summary": {}, "file_count": 0} + + languages = Counter() + frameworks = Counter() + func_naming = Counter() + class_naming = Counter() + import_styles = Counter() + bool_signals: dict[str, list[bool]] = defaultdict(list) + dirs = Counter() + + for obs in observations: + languages[obs.get("language", "unknown")] += 1 + for fw in obs.get("frameworks") or []: + frameworks[fw] += 1 + sig = obs.get("signals") or {} + if sig.get("function_naming"): + func_naming[sig["function_naming"]] += 1 + if sig.get("class_naming"): + class_naming[sig["class_naming"]] += 1 + if sig.get("import_style") and sig["import_style"] != "unknown": + import_styles[sig["import_style"]] += 1 + for key in ( + "has_module_docstring", + "uses_type_hints", + "uses_async", + "uses_dataclass", + "uses_structured_logging", + "uses_react_hooks", + "test_file", + ): + if key in sig: + bool_signals[key].append(bool(sig[key])) + rel = obs.get("path", "") + if "/" in rel: + dirs[rel.split("/")[0]] += 1 + + file_count = len(observations) + patterns: list[dict[str, Any]] = [] + + def _add_pattern( + pid: str, + category: str, + rule: str, + confidence: float, + evidence: int, + *, + examples: list[str] | None = None, + ) -> None: + if confidence < 0.55: + return + patterns.append( + { + "id": pid, + "category": category, + "rule": rule, + "confidence": round(confidence, 3), + "evidence_files": evidence, + "examples": examples or [], + } + ) + + if languages: + top_lang, count = languages.most_common(1)[0] + _add_pattern( + f"language-{top_lang}", + "language", + f"Primary language is {top_lang}", + count / file_count, + count, + ) + + if func_naming: + style, count = func_naming.most_common(1)[0] + _add_pattern( + f"naming-functions-{style}", + "naming", + f"Functions use {style}", + count / max(1, sum(func_naming.values())), + count, + ) + + if class_naming: + style, count = class_naming.most_common(1)[0] + _add_pattern( + f"naming-classes-{style}", + "naming", + f"Classes use {style}", + count / max(1, sum(class_naming.values())), + count, + ) + + if import_styles: + style, count = import_styles.most_common(1)[0] + _add_pattern( + f"imports-{style}", + "imports", + f"Import style favors {style}", + count / max(1, sum(import_styles.values())), + count, + ) + + for key, values in bool_signals.items(): + if not values: + continue + rate = sum(values) / len(values) + if rate >= 0.5: + labels = { + "has_module_docstring": "Files commonly open with module-level docstrings", + "uses_type_hints": "Type hints are used consistently", + "uses_async": "Async functions are part of the codebase style", + "uses_dataclass": "Dataclasses are a structural pattern", + "uses_structured_logging": "Structured logging (logger = getLogger) is standard", + "uses_react_hooks": "React hooks are the component pattern", + "test_file": "Test files are part of the observed surface", + } + _add_pattern( + f"convention-{key}", + "convention", + labels.get(key, key), + rate, + sum(values), + ) + + for fw, count in frameworks.most_common(5): + _add_pattern( + f"framework-{fw}", + "framework", + f"Uses {fw}", + count / file_count, + count, + ) + + if dirs: + top_dirs = [d for d, _ in dirs.most_common(4)] + _add_pattern( + "layout-top-dirs", + "layout", + f"Top-level organization includes: {', '.join(top_dirs)}", + 0.7, + file_count, + examples=top_dirs, + ) + + summary = { + "languages": dict(languages), + "frameworks": dict(frameworks), + "function_naming": dict(func_naming), + "class_naming": dict(class_naming), + "import_styles": dict(import_styles), + "avg_comment_density": round( + sum((o.get("signals") or {}).get("comment_density", 0) for o in observations) + / file_count, + 3, + ), + } + return {"patterns": patterns, "summary": summary, "file_count": file_count} + + +def crystallize_framework(project_id: str, *, force: bool = False) -> dict[str, Any]: + observations = _load_observations(project_id) + project = get_project(project_id) + if not observations: + return {"project_id": project_id, "patterns": [], "file_count": 0, "crystallized": False} + + if not force and len(observations) % _CRYSTALLIZE_EVERY != 0: + return { + "project_id": project_id, + "crystallized": False, + "note": f"Waiting for observation batch (every {_CRYSTALLIZE_EVERY} files)", + "file_count": len(observations), + } + + aggregated = _aggregate(observations) + mirror_summary = _mirror_summary(project_id) + framework = { + "project_id": project_id, + "display_name": project.display_name if project else project_id, + "root": project.root if project else "", + "crystallized_at": datetime.now(UTC).isoformat(), + "file_count": aggregated["file_count"], + "patterns": aggregated["patterns"], + "summary": aggregated["summary"], + "writing_guide": _build_writing_guide(aggregated), + "mirror_neurons": mirror_summary, + } + framework_path(project_id).write_text( + json.dumps(framework, indent=2, default=str), + encoding="utf-8", + ) + return framework + + +def _mirror_summary(project_id: str) -> dict[str, Any]: + try: + from agentdrive.codebase.mirrors import mirror_summary + + return mirror_summary(project_id) + except Exception: + return {} + + +def _build_writing_guide(aggregated: dict[str, Any]) -> str: + lines = ["# Writing guide (auto-learned)", ""] + summary = aggregated.get("summary") or {} + if summary.get("languages"): + langs = ", ".join(summary["languages"].keys()) + lines.append(f"- **Languages:** {langs}") + if summary.get("function_naming"): + top = max(summary["function_naming"], key=summary["function_naming"].get) + lines.append(f"- **Functions:** {top}") + if summary.get("frameworks"): + fws = ", ".join(summary["frameworks"].keys()) + lines.append(f"- **Frameworks:** {fws}") + lines.append("") + lines.append("## Patterns") + for pat in aggregated.get("patterns") or []: + lines.append( + f"- [{pat['category']}] {pat['rule']} " + f"(confidence {pat['confidence']}, n={pat['evidence_files']})" + ) + return "\n".join(lines) + + +def get_writing_guide(project_id: str) -> dict[str, Any]: + path = framework_path(project_id) + if path.is_file(): + return json.loads(path.read_text(encoding="utf-8")) + return crystallize_framework(project_id, force=True) + + +def match_against_framework( + project_id: str, + *, + code: str, + path: str = "snippet.py", +) -> dict[str, Any]: + from agentdrive.codebase.analyzer import analyze_content + + framework = get_writing_guide(project_id) + patterns = framework.get("patterns") or [] + if not patterns: + return { + "project_id": project_id, + "aligned": [], + "conflicts": [], + "note": "No framework yet — observe files first", + } + + snippet = analyze_content(path=path, content=code) + aligned: list[dict[str, Any]] = [] + conflicts: list[dict[str, Any]] = [] + + for pat in patterns: + pid = pat.get("id", "") + rule = pat.get("rule", "") + conf = float(pat.get("confidence") or 0) + + if pid.startswith("naming-functions-"): + expected = pid.replace("naming-functions-", "") + actual = (snippet.signals or {}).get("function_naming", "unknown") + if actual == expected or actual == "unknown": + aligned.append({"pattern": pid, "rule": rule, "confidence": conf}) + else: + conflicts.append( + { + "pattern": pid, + "rule": rule, + "expected": expected, + "actual": actual, + } + ) + elif pid.startswith("language-"): + expected = pid.replace("language-", "") + if snippet.language == expected: + aligned.append({"pattern": pid, "rule": rule, "confidence": conf}) + elif pid.startswith("framework-"): + expected = pid.replace("framework-", "") + if expected in snippet.frameworks: + aligned.append({"pattern": pid, "rule": rule, "confidence": conf}) + elif pid.startswith("convention-uses_type_hints"): + if snippet.signals.get("uses_type_hints"): + aligned.append({"pattern": pid, "rule": rule, "confidence": conf}) + elif "def " in code: + conflicts.append( + {"pattern": pid, "rule": rule, "expected": "type hints", "actual": "none"} + ) + + score = len(aligned) / max(1, len(patterns)) + return { + "project_id": project_id, + "alignment_score": round(score, 3), + "aligned": aligned, + "conflicts": conflicts, + "snippet_signals": snippet.to_dict(), + } diff --git a/src/agentdrive/codebase/mirrors.py b/src/agentdrive/codebase/mirrors.py new file mode 100644 index 0000000..f5acf81 --- /dev/null +++ b/src/agentdrive/codebase/mirrors.py @@ -0,0 +1,455 @@ +""" +Mirror-neuron layer — observation activates motor programs for mimicry. + +Like mirror neurons in humans: seeing how code is written primes the same +writing circuits. Observations link to exemplar motor templates; cross-project +resonance strengthens patterns shared across repos (universal priors). +""" + +from __future__ import annotations + +import json +import logging +import re +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from agentdrive.codebase.exemplars import extract_exemplars +from agentdrive.codebase.registry import framework_path, list_projects +from agentdrive.constants import get_agentdrive_home +from agentdrive.utils.safe_paths import safe_name + +logger = logging.getLogger(__name__) + + +def _mirrors_path(project_id: str) -> Path: + slug = safe_name(project_id) + return get_agentdrive_home() / "codebase-patterns" / slug / "mirrors.json" + + +def _resonance_path() -> Path: + root = get_agentdrive_home() / "codebase-patterns" + root.mkdir(parents=True, exist_ok=True) + return root / "mirror_resonance.json" + + +def _load_json(path: Path) -> dict[str, Any]: + if not path.is_file(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def _save_json(path: Path, data: dict[str, Any]) -> None: + path.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8") + + +def _pattern_key_from_observation(obs: dict[str, Any]) -> list[str]: + """Tags that identify which motor programs this observation should fire.""" + keys: list[str] = [] + lang = obs.get("language", "unknown") + keys.append(f"lang:{lang}") + sig = obs.get("signals") or {} + if sig.get("function_naming"): + keys.append(f"func_naming:{sig['function_naming']}") + if sig.get("class_naming"): + keys.append(f"class_naming:{sig['class_naming']}") + if sig.get("import_style") and sig["import_style"] != "unknown": + keys.append(f"imports:{sig['import_style']}") + for fw in obs.get("frameworks") or []: + keys.append(f"fw:{fw}") + for conv, val in ( + ("type_hints", sig.get("uses_type_hints")), + ("async", sig.get("uses_async")), + ("dataclass", sig.get("uses_dataclass")), + ("logging", sig.get("uses_structured_logging")), + ("hooks", sig.get("uses_react_hooks")), + ): + if val: + keys.append(f"conv:{conv}") + rel = obs.get("path", "") + if "/" in rel: + keys.append(f"dir:{rel.split('/')[0]}") + return keys + + +def ingest_observation_mirror( + project_id: str, + *, + path: str, + content: str, + observation: dict[str, Any], +) -> dict[str, Any]: + """ + Mirror-neuron ingest: observation → link exemplar motor templates + fire tags. + """ + language = observation.get("language", "unknown") + exemplars = extract_exemplars(path=path, content=content, language=language) + pattern_keys = _pattern_key_from_observation(observation) + + store = _load_json(_mirrors_path(project_id)) + store.setdefault("project_id", safe_name(project_id)) + store.setdefault("motor_programs", []) + store.setdefault("observation_links", []) + motors: list[dict[str, Any]] = list(store.get("motor_programs") or []) + links: list[dict[str, Any]] = list(store.get("observation_links") or []) + + fired: list[str] = [] + now = datetime.now(UTC).isoformat() + + for ex in exemplars: + motor_id = f"motor:{safe_name(project_id)}:{ex.get('kind')}:{ex.get('name')}" + existing = next((m for m in motors if m.get("id") == motor_id), None) + if existing: + existing["fire_count"] = int(existing.get("fire_count") or 0) + 1 + existing["last_fired_at"] = now + existing["source_paths"] = list( + dict.fromkeys((existing.get("source_paths") or []) + [path]) + )[-8:] + else: + motors.append( + { + "id": motor_id, + "kind": ex.get("kind"), + "name": ex.get("name"), + "pattern_keys": pattern_keys, + "motor_template": ex.get("motor_template", ""), + "source_paths": [path], + "fire_count": 1, + "first_seen_at": now, + "last_fired_at": now, + } + ) + fired.append(motor_id) + + links.append( + { + "path": path, + "observed_at": observation.get("observed_at", now), + "pattern_keys": pattern_keys, + "motors_fired": fired, + "exemplar_count": len(exemplars), + } + ) + links = links[-200:] + motors = motors[-120:] + + store["motor_programs"] = motors + store["observation_links"] = links + store["updated_at"] = now + _save_json(_mirrors_path(project_id), store) + + resonance = _update_global_resonance(project_id, pattern_keys, fired) + return { + "motors_fired": len(fired), + "motor_ids": fired[:6], + "pattern_keys": pattern_keys, + "resonance_updates": resonance.get("updated", 0), + } + + +def _update_global_resonance( + project_id: str, + pattern_keys: list[str], + motor_ids: list[str], +) -> dict[str, Any]: + """Cross-project mirror field — patterns seen in multiple repos resonate louder.""" + data = _load_json(_resonance_path()) + nodes: dict[str, Any] = dict(data.get("nodes") or {}) + edges: list[dict[str, Any]] = list(data.get("edges") or []) + slug = safe_name(project_id) + + for key in pattern_keys: + node = nodes.get(key) or { + "pattern_key": key, + "projects": [], + "fire_count": 0, + "motor_count": 0, + } + projects = list(node.get("projects") or []) + if slug not in projects: + projects.append(slug) + node["projects"] = projects[-20:] + node["fire_count"] = int(node.get("fire_count") or 0) + 1 + node["motor_count"] = int(node.get("motor_count") or 0) + len(motor_ids) + node["last_fired_at"] = datetime.now(UTC).isoformat() + # Resonance strength: more projects sharing = stronger universal prior + node["resonance"] = round( + min(1.0, 0.35 + 0.15 * len(projects) + 0.02 * node["fire_count"]), + 3, + ) + nodes[key] = node + + for i, mid in enumerate(motor_ids[:4]): + for key in pattern_keys[:3]: + edges.append( + { + "motor_id": mid, + "pattern_key": key, + "project_id": slug, + "weight": 1.0 / (1 + i * 0.1), + } + ) + edges = edges[-500:] + + data["nodes"] = nodes + data["edges"] = edges + data["updated_at"] = datetime.now(UTC).isoformat() + _save_json(_resonance_path(), data) + return {"updated": len(pattern_keys)} + + +def fire_mirrors_for_intent( + project_id: str, + *, + intent: str, + language: str | None = None, + limit: int = 5, +) -> dict[str, Any]: + """ + Given writing intent, fire mirror neurons — return motor templates to imitate. + """ + store = _load_json(_mirrors_path(project_id)) + motors: list[dict[str, Any]] = list(store.get("motor_programs") or []) + framework = _load_json(framework_path(project_id)) + patterns = framework.get("patterns") or [] + + intent_tokens = set(re.findall(r"[a-z]{3,}", intent.lower())) + scored: list[tuple[float, dict[str, Any]]] = [] + + for motor in motors: + score = 0.0 + name = str(motor.get("name", "")).lower() + template = str(motor.get("motor_template", "")).lower() + for tok in intent_tokens: + if tok in name: + score += 2.5 + if tok in template: + score += 0.8 + score += 0.15 * int(motor.get("fire_count") or 0) + if language: + keys = motor.get("pattern_keys") or [] + if f"lang:{language}" in keys: + score += 1.2 + if score > 0: + scored.append((score, motor)) + + scored.sort(key=lambda x: x[0], reverse=True) + fired = [m for _, m in scored[:limit]] + + if not fired and motors: + fired = sorted(motors, key=lambda m: int(m.get("fire_count") or 0), reverse=True)[:limit] + + resonance_hits = _resonance_for_project(project_id, patterns) + mimicry_prompt = _build_mimicry_prompt( + project_id=project_id, + intent=intent, + fired=fired, + patterns=patterns, + resonance=resonance_hits, + ) + + return { + "project_id": project_id, + "intent": intent, + "motors_fired": len(fired), + "motor_programs": [ + { + "id": m.get("id"), + "kind": m.get("kind"), + "name": m.get("name"), + "template": m.get("motor_template", ""), + "source_paths": m.get("source_paths", [])[:3], + "fire_count": m.get("fire_count", 0), + } + for m in fired + ], + "resonance": resonance_hits[:6], + "mimicry_prompt": mimicry_prompt, + } + + +def _resonance_for_project( + project_id: str, + patterns: list[dict[str, Any]], +) -> list[dict[str, Any]]: + data = _load_json(_resonance_path()) + nodes: dict[str, Any] = dict(data.get("nodes") or {}) + hits: list[dict[str, Any]] = [] + slug = safe_name(project_id) + + for pat in patterns: + pid = pat.get("id", "") + candidates = [] + if pid.startswith("language-"): + candidates.append(f"lang:{pid.replace('language-', '')}") + if pid.startswith("naming-functions-"): + candidates.append(f"func_naming:{pid.replace('naming-functions-', '')}") + if pid.startswith("imports-"): + candidates.append(f"imports:{pid.replace('imports-', '')}") + if pid.startswith("framework-"): + candidates.append(f"fw:{pid.replace('framework-', '')}") + for key in candidates: + node = nodes.get(key) + if not node: + continue + other_projects = [p for p in node.get("projects") or [] if p != slug] + hits.append( + { + "pattern_key": key, + "resonance": node.get("resonance", 0), + "shared_with_projects": other_projects[:5], + "fire_count": node.get("fire_count", 0), + } + ) + hits.sort(key=lambda h: float(h.get("resonance") or 0), reverse=True) + return hits + + +def _build_mimicry_prompt( + *, + project_id: str, + intent: str, + fired: list[dict[str, Any]], + patterns: list[dict[str, Any]], + resonance: list[dict[str, Any]], +) -> str: + lines = [ + f"# Mimicry brief — write like `{project_id}`", + "", + f"**Intent:** {intent}", + "", + "Mirror-neuron rule: imitate the motor programs below — same naming, imports,", + "structure, and rhythm. Do not invent a foreign style.", + "", + "## Project patterns", + ] + for pat in patterns[:6]: + lines.append(f"- {pat.get('rule', '')}") + if resonance: + lines.append("") + lines.append("## Cross-project resonance (shared with other observed repos)") + for hit in resonance[:4]: + shared = ", ".join(hit.get("shared_with_projects") or []) or "none yet" + lines.append( + f"- `{hit.get('pattern_key')}` resonance {hit.get('resonance')} — also in: {shared}" + ) + if fired: + lines.append("") + lines.append("## Motor programs to imitate") + for motor in fired[:3]: + lines.append(f"### {motor.get('kind')}: {motor.get('name')}") + lines.append("```") + lines.append(str(motor.get("motor_template", ""))[:600]) + lines.append("```") + return "\n".join(lines) + + +def transform_toward_style( + project_id: str, + *, + code: str, + path: str = "snippet.py", +) -> dict[str, Any]: + """Transform generic code toward observed project style (naming mimicry).""" + from agentdrive.codebase.analyzer import analyze_content + from agentdrive.codebase.framework import get_writing_guide, match_against_framework + + framework = get_writing_guide(project_id) + patterns = framework.get("patterns") or [] + match = match_against_framework(project_id, code=code, path=path) + snippet = analyze_content(path=path, content=code) + + func_style = "snake_case" + class_style = "PascalCase" + for pat in patterns: + pid = pat.get("id", "") + if pid.startswith("naming-functions-"): + func_style = pid.replace("naming-functions-", "") + if pid.startswith("naming-classes-"): + class_style = pid.replace("naming-classes-", "") + + transformed = code + suggestions: list[str] = [] + + if func_style == "snake_case": + for name in (snippet.identifiers or {}).get("functions", []): + if re.search(r"[A-Z]", name) and "_" not in name: + snake = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name).lower() + if snake != name: + transformed = re.sub(rf"\b{re.escape(name)}\b", snake, transformed) + suggestions.append(f"Rename function `{name}` → `{snake}`") + elif func_style == "camelCase": + for name in (snippet.identifiers or {}).get("functions", []): + if "_" in name: + parts = name.split("_") + camel = parts[0] + "".join(p.capitalize() for p in parts[1:]) + transformed = re.sub(rf"\b{re.escape(name)}\b", camel, transformed) + suggestions.append(f"Rename function `{name}` → `{camel}`") + + lang = framework.get("summary", {}).get("languages", {}) + primary = max(lang, key=lang.get) if lang else snippet.language + if primary == "python" and "def " in transformed and "->" not in transformed: + suggestions.append("Add return type hints to match project convention") + if primary == "typescript" and "export " not in transformed and "function " in transformed: + suggestions.append("Consider `export` on public functions (TypeScript project style)") + + fired = fire_mirrors_for_intent(project_id, intent="transform style", language=primary, limit=2) + + return { + "project_id": project_id, + "original_alignment": match.get("alignment_score"), + "transformed_code": transformed, + "suggestions": suggestions, + "target_function_naming": func_style, + "target_class_naming": class_style, + "mimicry_prompt": fired.get("mimicry_prompt", "")[:1200], + "conflicts_remaining": match.get("conflicts", []), + } + + +def mirror_summary(project_id: str) -> dict[str, Any]: + store = _load_json(_mirrors_path(project_id)) + motors = store.get("motor_programs") or [] + return { + "motor_program_count": len(motors), + "total_fires": sum(int(m.get("fire_count") or 0) for m in motors), + "top_motors": [ + {"name": m.get("name"), "kind": m.get("kind"), "fires": m.get("fire_count")} + for m in sorted(motors, key=lambda x: int(x.get("fire_count") or 0), reverse=True)[:5] + ], + } + + +def global_mirror_field(*, limit: int = 12) -> dict[str, Any]: + """Universal priors — pattern keys that resonate across multiple observed projects.""" + data = _load_json(_resonance_path()) + nodes: list[dict[str, Any]] = list((data.get("nodes") or {}).values()) + nodes.sort( + key=lambda n: (len(n.get("projects") or []), float(n.get("resonance") or 0)), + reverse=True, + ) + universal = [] + for node in nodes[:limit]: + projects = node.get("projects") or [] + if len(projects) < 1: + continue + universal.append( + { + "pattern_key": node.get("pattern_key"), + "resonance": node.get("resonance"), + "projects": projects, + "fire_count": node.get("fire_count"), + "kind": "universal_prior" if len(projects) >= 2 else "project_local", + } + ) + return { + "projects_registered": len(list_projects()), + "universal_priors": [u for u in universal if u["kind"] == "universal_prior"], + "top_resonances": universal, + "updated_at": data.get("updated_at"), + } diff --git a/src/agentdrive/codebase/observe.py b/src/agentdrive/codebase/observe.py new file mode 100644 index 0000000..4940bfc --- /dev/null +++ b/src/agentdrive/codebase/observe.py @@ -0,0 +1,201 @@ +"""Observe files and accumulate codebase writing patterns.""" + +from __future__ import annotations + +import logging +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from agentdrive.codebase.analyzer import analyze_content +from agentdrive.codebase.framework import crystallize_framework +from agentdrive.codebase.registry import ( + append_observation, + bump_observation, + get_project, + register_project, +) +from agentdrive.utils.safe_paths import safe_join + +logger = logging.getLogger(__name__) + + +def _record_mirror_trace(project_id: str, path: str, mirror_result: dict[str, Any]) -> None: + """Write mirror-neuron firing into the Experience Graph (observation → motor link).""" + if not mirror_result.get("motors_fired"): + return + try: + from agentdrive.operations.registry import _integrated_recorder # noqa: PLC2701 + + _, recorder = _integrated_recorder(None) + recorder.record_parent_fabric_reasoning( + cycle_id=f"mirror-{project_id}-{int(__import__('time').time())}", + reasoning={ + "summary": f"Mirror neurons fired on {path}", + "structural_pattern_matched": "mirror-neuron-mimicry", + "fabric_elements_considered": [ + f"codebase:{project_id}", + f"file:{path}", + *(mirror_result.get("pattern_keys") or [])[:5], + ], + "decision_rationale": ( + f"Observed code primed {mirror_result.get('motors_fired')} motor programs " + "for future mimicry (mirror-neuron coupling)." + ), + "expected_lift_signal": 0.1, + "llm_mode": "mirror_neuron", + "mirror_neurons": mirror_result, + }, + ) + except Exception: + logger.debug("mirror trace record failed", exc_info=True) + + +_ALLOWED_EXT = { + ".py", + ".ts", + ".tsx", + ".js", + ".jsx", + ".go", + ".rs", + ".md", + ".json", + ".yaml", + ".yml", +} + + +def _resolve_under_project(project_id: str, rel_path: str) -> Path: + project = get_project(project_id) + if project is None: + raise KeyError(f"Unknown project: {project_id}. Register with codebase_register_project.") + rel = (rel_path or "").strip().lstrip("/") + parts = [p for p in rel.split("/") if p and p not in (".", "..")] + if not parts: + raise ValueError("Empty path") + return safe_join(project.root, *parts) + + +def observe_file( + *, + project_id: str, + path: str, + max_lines: int = 400, + content: str | None = None, + auto_register_root: str | None = None, +) -> dict[str, Any]: + """Read (or accept) a file under a registered project and learn its patterns.""" + if get_project(project_id) is None: + if not auto_register_root: + raise KeyError(f"Unknown project: {project_id}") + register_project(project_id=project_id, root=auto_register_root) + + full_path = _resolve_under_project(project_id, path) + if full_path.suffix.lower() not in _ALLOWED_EXT: + return { + "success": False, + "error": f"Extension {full_path.suffix} not allowed", + "project_id": project_id, + } + if content is None: + if not full_path.is_file(): + return {"success": False, "error": f"File not found: {path}", "project_id": project_id} + raw = full_path.read_text(encoding="utf-8", errors="replace") + lines = raw.splitlines()[:max_lines] + content = "\n".join(lines) + + rel = path.strip().lstrip("/") + signals = analyze_content(path=rel, content=content) + observation = { + **signals.to_dict(), + "observed_at": datetime.now(UTC).isoformat(), + "source": "codebase_observe", + } + append_observation(project_id, observation) + bump_observation(project_id) + + mirror_result: dict[str, Any] = {} + try: + from agentdrive.codebase.mirrors import ingest_observation_mirror + + mirror_result = ingest_observation_mirror( + project_id, + path=rel, + content=content, + observation=observation, + ) + _record_mirror_trace(project_id, rel, mirror_result) + except Exception: + logger.debug("mirror neuron ingest failed", exc_info=True) + + framework = crystallize_framework(project_id) + + return { + "success": True, + "project_id": project_id, + "path": rel, + "language": signals.language, + "frameworks": signals.frameworks, + "signals": signals.signals, + "files_observed": framework.get("file_count", 0), + "patterns_count": len(framework.get("patterns") or []), + "crystallized": bool(framework.get("crystallized_at")), + "framework": framework if framework.get("crystallized_at") else None, + "mirror_neurons": mirror_result, + } + + +def observe_text( + *, + project_id: str, + path: str, + content: str, +) -> dict[str, Any]: + """Observe in-memory content (e.g. from inhabitant_read_source) without disk read.""" + return observe_file(project_id=project_id, path=path, content=content) + + +def auto_observe_inhabitant_read( + *, + rel_path: str, + content: str, + project_id: str = "agentdrive", + package_root: str | None = None, +) -> dict[str, Any] | None: + """Hook for inhabitant_read_source — learn AgentDrive (or package) writing style.""" + try: + if package_root and get_project(project_id) is None: + register_project(project_id=project_id, root=package_root) + return observe_text(project_id=project_id, path=rel_path, content=content) + except Exception: + logger.debug("auto_observe_inhabitant_read failed", exc_info=True) + return None + + +def observe_from_absolute( + *, + project_id: str, + absolute_path: str, + max_lines: int = 400, + auto_register_root: str | None = None, +) -> dict[str, Any]: + """Observe using an absolute path — derives project-relative path when possible.""" + resolved = Path(absolute_path).expanduser().resolve() + project = get_project(project_id) + if project is None: + root = auto_register_root or str(resolved.parent) + register_project(project_id=project_id, root=root) + project = get_project(project_id) + assert project is not None + try: + rel = str(resolved.relative_to(Path(project.root).resolve())) + except ValueError: + rel = resolved.name + content = resolved.read_text(encoding="utf-8", errors="replace") + lines = content.splitlines()[:max_lines] + return observe_file( + project_id=project_id, + path=rel, + content="\n".join(lines), + ) diff --git a/src/agentdrive/codebase/registry.py b/src/agentdrive/codebase/registry.py new file mode 100644 index 0000000..c82cb77 --- /dev/null +++ b/src/agentdrive/codebase/registry.py @@ -0,0 +1,145 @@ +"""Register codebase roots for safe pattern observation.""" + +from __future__ import annotations + +import json +import logging +from dataclasses import asdict, dataclass +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +import yaml + +from agentdrive.constants import get_agentdrive_home +from agentdrive.utils.safe_paths import safe_name + +logger = logging.getLogger(__name__) + +_REGISTRY_NAME = "projects.yaml" + + +@dataclass +class CodebaseProject: + project_id: str + root: str + display_name: str = "" + primary_language: str = "" + registered_at: str = "" + files_observed: int = 0 + last_observed_at: str = "" + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +def _registry_path() -> Path: + root = get_agentdrive_home() / "codebase-patterns" + root.mkdir(parents=True, exist_ok=True) + return root / _REGISTRY_NAME + + +def _profile_dir(project_id: str) -> Path: + slug = safe_name(project_id) + path = get_agentdrive_home() / "codebase-patterns" / slug + path.mkdir(parents=True, exist_ok=True) + return path + + +def _load_registry() -> dict[str, Any]: + path = _registry_path() + if not path.is_file(): + return {"projects": {}} + try: + data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + except Exception: + logger.debug("Failed to load codebase registry", exc_info=True) + return {"projects": {}} + if not isinstance(data, dict): + return {"projects": {}} + data.setdefault("projects", {}) + return data + + +def _save_registry(data: dict[str, Any]) -> None: + path = _registry_path() + path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") + + +def register_project( + *, + project_id: str, + root: str, + display_name: str = "", + primary_language: str = "", +) -> CodebaseProject: + slug = safe_name(project_id) + resolved = Path(root).expanduser().resolve() + if not resolved.is_dir(): + raise FileNotFoundError(f"Project root not found: {resolved}") + + data = _load_registry() + projects = data["projects"] + now = datetime.now(UTC).isoformat() + existing = projects.get(slug) or {} + project = CodebaseProject( + project_id=slug, + root=str(resolved), + display_name=display_name or existing.get("display_name") or slug, + primary_language=primary_language or existing.get("primary_language") or "", + registered_at=existing.get("registered_at") or now, + files_observed=int(existing.get("files_observed") or 0), + last_observed_at=existing.get("last_observed_at") or "", + ) + projects[slug] = project.to_dict() + _save_registry(data) + _profile_dir(slug) + return project + + +def list_projects() -> list[CodebaseProject]: + data = _load_registry() + projects: list[CodebaseProject] = [] + for raw in (data.get("projects") or {}).values(): + if isinstance(raw, dict): + projects.append( + CodebaseProject(**{k: raw.get(k, "") for k in CodebaseProject.__dataclass_fields__}) + ) + return sorted(projects, key=lambda p: p.project_id) + + +def get_project(project_id: str) -> CodebaseProject | None: + slug = safe_name(project_id) + raw = (_load_registry().get("projects") or {}).get(slug) + if not isinstance(raw, dict): + return None + return CodebaseProject(**{k: raw.get(k, "") for k in CodebaseProject.__dataclass_fields__}) + + +def bump_observation(project_id: str) -> None: + slug = safe_name(project_id) + data = _load_registry() + projects = data.get("projects") or {} + raw = projects.get(slug) + if not isinstance(raw, dict): + return + raw["files_observed"] = int(raw.get("files_observed") or 0) + 1 + raw["last_observed_at"] = datetime.now(UTC).isoformat() + projects[slug] = raw + data["projects"] = projects + _save_registry(data) + + +def observations_path(project_id: str) -> Path: + return _profile_dir(project_id) / "observations.jsonl" + + +def framework_path(project_id: str) -> Path: + return _profile_dir(project_id) / "framework.json" + + +def append_observation(project_id: str, observation: dict[str, Any]) -> None: + path = observations_path(project_id) + line = json.dumps(observation, default=str) + with path.open("a", encoding="utf-8") as handle: + handle.write(line + "\n") diff --git a/src/agentdrive/cognition/__init__.py b/src/agentdrive/cognition/__init__.py new file mode 100644 index 0000000..aa59617 --- /dev/null +++ b/src/agentdrive/cognition/__init__.py @@ -0,0 +1,55 @@ +""" +AgentDrive Cognition — live operational thinking primitives for the 6-step loop. + +Distinct from ``agentdrive.reasoning`` (post-hoc DNA extraction from runs). +Cognition modules power Parent/Overseer decision-making during active cycles. +""" + +from __future__ import annotations + +from typing import Any + +from .llm_spawner import LLMBranchSpawner, resolve_available_local_model +from .multiverse import ( + COGNITIVE_ROLES, + MULTIVERSE_RELATIONS, + AdversaryVerdict, + Branch, + CollapsePolicy, + ForwardStep, + Invariant, + InvariantKind, + MultiverseEngine, + MultiverseSession, +) +from .roles import AXIS_GUIDANCE, ROLE_PROMPTS, role_system_prompt +from .store import MultiverseSessionStore, session_from_dict, session_to_dict + +COGNITION_VERSION = "agentdrive-cognition-0.2.0" + +__all__ = [ + "AXIS_GUIDANCE", + "COGNITION_VERSION", + "LLMBranchSpawner", + "ROLE_PROMPTS", + "resolve_available_local_model", + "role_system_prompt", + "MULTIVERSE_RELATIONS", + "AdversaryVerdict", + "Branch", + "CollapsePolicy", + "COGNITIVE_ROLES", + "ForwardStep", + "Invariant", + "InvariantKind", + "MultiverseEngine", + "MultiverseSession", + "MultiverseSessionStore", + "session_from_dict", + "session_to_dict", +] + + +def get_multiverse_engine(recorder: Any, **kwargs: Any) -> MultiverseEngine: + """Factory for MultiverseEngine bound to a recorder.""" + return MultiverseEngine(recorder, **kwargs) diff --git a/src/agentdrive/cognition/llm_spawner.py b/src/agentdrive/cognition/llm_spawner.py new file mode 100644 index 0000000..0e9ca5b --- /dev/null +++ b/src/agentdrive/cognition/llm_spawner.py @@ -0,0 +1,217 @@ +""" +LLM-backed branch spawning and forward simulation for Multiverse Cognition (M2). + +Uses Harness DNA compose + local model dispatch when available. +Falls back gracefully to heuristic mode when no model is reachable. +""" + +from __future__ import annotations + +import json +import logging +import re +from typing import Any + +from agentdrive.cognition.multiverse import ( + AdversaryVerdict, + Branch, + ForwardStep, +) +from agentdrive.cognition.roles import role_system_prompt + +logger = logging.getLogger(__name__) + + +def _extract_json(raw: str) -> dict[str, Any] | None: + text = (raw or "").strip() + if not text: + return None + try: + return json.loads(text) + except json.JSONDecodeError: + pass + match = re.search(r"\{[\s\S]*\}", text) + if match: + try: + return json.loads(match.group(0)) + except json.JSONDecodeError: + return None + return None + + +def resolve_available_local_model() -> Any | None: + """Return first reachable LocalModelSpec from ~/.agentdrive/local_models.yaml.""" + try: + from agentdrive.local_models import is_available, load_specs + + for spec in load_specs(): + if is_available(spec): + return spec + except Exception as exc: + logger.debug("resolve_available_local_model failed: %s", exc) + return None + + +class LLMBranchSpawner: + """Spawn and simulate branches via local LLM + Harness context.""" + + def __init__( + self, + *, + agent_id: str = "multiverse-llm-spawner", + model_spec: Any | None = None, + harness_task: str | None = None, + fabric_context: dict[str, Any] | None = None, + ) -> None: + self.agent_id = agent_id + self.model_spec = model_spec or resolve_available_local_model() + self.harness_task = harness_task + self.fabric_context = fabric_context or {} + self.llm_calls = 0 + self.llm_available = self.model_spec is not None + + def _generate(self, system: str, user: str) -> str: + if not self.model_spec: + return "" + from agentdrive.local_models import LocalModelError, generate + + self.llm_calls += 1 + try: + return generate(self.model_spec, user, system=system) + except LocalModelError as exc: + logger.debug("LLM generate failed: %s", exc) + return "" + + def spawn_branch( + self, + trigger: str, + role: str, + axis: str, + *, + index: int, + ) -> Branch | None: + """LLM spawn one branch. Returns None on failure (caller uses heuristic).""" + if not self.model_spec: + return None + + system = role_system_prompt(role, axis) + user = json.dumps( + { + "trigger": trigger, + "role": role, + "divergence_axis": axis, + "fabric_context_snippet": { + k: self.fabric_context.get(k) + for k in ( + "top_weak_clusters", + "strong_continuations", + "actionable_recommendations", + ) + if k in self.fabric_context + }, + "required_json_schema": { + "path_summary": "one sentence describing this timeline", + "assumptions": ["list", "of", "assumptions"], + "fragility_flags": ["optional fragile assumptions"], + }, + }, + default=str, + )[:3500] + + raw = self._generate(system, user) + data = _extract_json(raw) + if not data or not data.get("path_summary"): + return None + + branch_id = f"branch:{role}-{index}" + return Branch( + branch_id=branch_id, + role=role, + path_summary=str(data["path_summary"]), + assumptions=[str(a) for a in (data.get("assumptions") or [])[:6]], + divergence_axes=[axis], + fragility_flags=[str(f) for f in (data.get("fragility_flags") or [])[:4]], + ) + + def simulate_forward( + self, + trigger: str, + branch: Branch, + steps: int, + ) -> list[ForwardStep] | None: + if not self.model_spec: + return None + + system = role_system_prompt( + branch.role, branch.divergence_axes[0] if branch.divergence_axes else "risk" + ) + user = json.dumps( + { + "trigger": trigger, + "branch": { + "role": branch.role, + "path_summary": branch.path_summary, + "assumptions": branch.assumptions, + }, + "forward_steps_required": steps, + "required_json_schema": { + "forward_steps": [ + {"step_index": 1, "description": "immediate consequence", "confidence": 0.7} + ] + }, + }, + default=str, + )[:3500] + + raw = self._generate(system, user) + data = _extract_json(raw) + if not data: + return None + + out: list[ForwardStep] = [] + for item in (data.get("forward_steps") or [])[:steps]: + if not isinstance(item, dict): + continue + out.append( + ForwardStep( + step_index=int(item.get("step_index", len(out) + 1)), + description=str(item.get("description", ""))[:300], + confidence=float(item.get("confidence", 0.65)), + ) + ) + return out or None + + def adversary_stress_test(self, branch: Branch) -> AdversaryVerdict | None: + if not self.model_spec: + return None + + system = role_system_prompt("adversary", "risk") + user = json.dumps( + { + "branch": { + "role": branch.role, + "path_summary": branch.path_summary, + "assumptions": branch.assumptions, + "forward_steps": [s.description for s in branch.forward_steps], + }, + "required_json_schema": { + "passed": True, + "fatal_flaws": [], + "mitigations": [], + "rationale": "short string", + }, + }, + default=str, + )[:3000] + + raw = self._generate(system, user) + data = _extract_json(raw) + if not data: + return None + + return AdversaryVerdict( + passed=bool(data.get("passed", False)), + fatal_flaws=[str(f) for f in (data.get("fatal_flaws") or [])], + mitigations=[str(m) for m in (data.get("mitigations") or [])], + rationale=str(data.get("rationale", "LLM adversary stress-test")), + ) diff --git a/src/agentdrive/cognition/multiverse.py b/src/agentdrive/cognition/multiverse.py new file mode 100644 index 0000000..87bdd32 --- /dev/null +++ b/src/agentdrive/cognition/multiverse.py @@ -0,0 +1,1204 @@ +""" +Multiverse Cognition — parallel timeline superposition for Parent decisions. + +Holds competing futures in superposition, extracts cross-branch invariants, +stress-tests via Adversary lens, collapses to one governed path, and records +everything as first-class Experience Graph v3 DNA. + +Integrates with: +- ExperienceGraphRecorder (TypedEdges + page_type observations) +- IntegratedRealTimeEvolutionSystem.record_parent_decision (fabric_reasoning) +- AD-Grid Council (Adversary stress-test, Guardian veto hook) +- Cognitive Agent Team roles (branch generators) + +See docs/MULTIVERSE_COGNITION.md for the full architecture. +""" + +from __future__ import annotations + +import json +import time +import uuid +from dataclasses import asdict, dataclass, field +from enum import Enum +from pathlib import Path +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from agentdrive.cognition.store import MultiverseSessionStore + from agentdrive.evolution.experience_graph import ExperienceGraphRecorder + +# ------------------------------------------------------------------ +# TypedEdge relations (dual-written via recorder.record_connection) +# ------------------------------------------------------------------ + +MULTIVERSE_SESSION = "multiverse_session" +BRANCH_SPAWNED = "branch_spawned" +BRANCH_SIMULATED_FORWARD = "branch_simulated_forward" +INVARIANT_EXTRACTED = "invariant_extracted" +CONVERGENCE_DETECTED = "convergence_detected" +DIVERGENCE_DETECTED = "divergence_detected" +PATH_COLLAPSED = "path_collapsed" +BRANCH_STRESS_TESTED = "branch_stress_tested" +MULTIVERSE_INFORMED_DECISION = "multiverse_informed_decision" + +MULTIVERSE_RELATIONS: dict[str, str] = { + MULTIVERSE_SESSION: "session_contains_multiverse", + BRANCH_SPAWNED: "spawned_in_multiverse", + BRANCH_SIMULATED_FORWARD: "forward_simulation_of_branch", + INVARIANT_EXTRACTED: "invariant_of_multiverse", + CONVERGENCE_DETECTED: "converges_via_multiverse", + DIVERGENCE_DETECTED: "diverges_at_multiverse_point", + PATH_COLLAPSED: "collapsed_from_multiverse", + BRANCH_STRESS_TESTED: "stress_test_on_branch", + MULTIVERSE_INFORMED_DECISION: "decision_informed_by_multiverse", +} + +# Cognitive Agent Team roles → branch generators +COGNITIVE_ROLES: tuple[str, ...] = ( + "architect", + "adversary", + "scout", + "operator", + "surgeon", + "beacon", + "watchdog", +) + +DIVERGENCE_AXES: tuple[str, ...] = ( + "risk", + "speed", + "reversibility", + "cost", + "dependency_order", +) + +ROLE_PATH_TEMPLATES: dict[str, str] = { + "architect": "Structural path: map whole system first, then locate intervention point", + "adversary": "Failure path: assume optimistic plan breaks at weakest assumption", + "scout": "Intelligence path: gather unknowns before committing resources", + "operator": "Velocity path: smallest shippable slice with momentum preservation", + "surgeon": "Precision path: minimal cut at highest-leverage intervention point", + "beacon": "Signal path: optimize for discoverability and audience propagation", + "watchdog": "Defense path: trace attack surfaces and anomaly blast radius", +} + + +class CollapsePolicy(str, Enum): + PATTERN_CRYSTALLIZED = "pattern_crystallized" + ADVERSARY_CLEAR = "adversary_clear" + HARNESS_SCORE = "harness_score" + CONDUCTOR_OVERRIDE = "conductor_override" + BUDGET_EXHAUSTED = "budget_exhausted" + EXTERNAL_PARENT = "external_parent" + + +class InvariantKind(str, Enum): + ROBUST = "robust" + FRAGILE = "fragile" + CONVERGENCE = "convergence" + DIVERGENCE = "divergence" + + +class SessionStatus(str, Enum): + OPEN = "open" + COLLAPSED = "collapsed" + REOPENED = "reopened" + + +@dataclass +class ForwardStep: + step_index: int + description: str + confidence: float = 0.7 + + +@dataclass +class AdversaryVerdict: + passed: bool + fatal_flaws: list[str] = field(default_factory=list) + mitigations: list[str] = field(default_factory=list) + rationale: str = "" + + +@dataclass +class Branch: + branch_id: str + role: str + path_summary: str + assumptions: list[str] = field(default_factory=list) + divergence_axes: list[str] = field(default_factory=list) + forward_steps: list[ForwardStep] = field(default_factory=list) + robustness_score: float = 0.0 + fragility_flags: list[str] = field(default_factory=list) + stress_test: AdversaryVerdict | None = None + + +@dataclass +class Invariant: + statement: str + branch_coverage: float + kind: InvariantKind + source_branches: list[str] = field(default_factory=list) + + +@dataclass +class MultiverseSession: + session_id: str + trigger: str + cycle_id: str + correlation_id: str + branches: list[Branch] = field(default_factory=list) + invariants: list[Invariant] = field(default_factory=list) + convergence_points: list[str] = field(default_factory=list) + divergence_points: list[str] = field(default_factory=list) + status: SessionStatus = SessionStatus.OPEN + collapsed_branch_id: str | None = None + collapse_reason: str | None = None + collapse_policy: CollapsePolicy | None = None + program_id: str | None = None + constitution_refs: list[str] = field(default_factory=list) + user_objective_refs: list[str] = field(default_factory=list) + created_at: float = field(default_factory=time.time) + # Set when an MCP-connected frontier/local chat model supplies branches (Grok, Claude, Codex, etc.) + reasoning_provider: str | None = None + llm_mode: str = "heuristic" # heuristic | llm | external + external_fabric_reasoning: dict[str, Any] | None = None + + +class MultiverseEngine: + """ + Orchestrates multiverse cognition sessions against a live Experience Graph recorder. + + Tier 0 implementation: heuristic branch spawning + forward simulation. + Swap ``branch_generator`` / ``forward_simulator`` callables for LLM-backed tiers. + """ + + def __init__( + self, + recorder: ExperienceGraphRecorder, + *, + program_id: str | None = None, + constitution_refs: list[str] | None = None, + user_objective_refs: list[str] | None = None, + default_forward_steps: int = 3, + robust_threshold: float = 0.7, + use_llm: bool = True, + ) -> None: + self.recorder = recorder + self.program_id = program_id + self.constitution_refs = constitution_refs or [ + "research-constitution-multiverse-cognition@stabilization-wave-20260531" + ] + self.user_objective_refs = user_objective_refs or ["multiverse-cognition-integration"] + self.default_forward_steps = default_forward_steps + self.robust_threshold = robust_threshold + self.use_llm = use_llm + self._llm_spawner: Any | None = None + self._sessions: dict[str, MultiverseSession] = {} + drive_path = getattr(recorder, "drive_path", None) + self._store: MultiverseSessionStore | None = None + if drive_path: + from agentdrive.cognition.store import MultiverseSessionStore + + self._store = MultiverseSessionStore(Path(drive_path)) + if self._store: + for session in self._store.list_recent(limit=20): + self._sessions[session.session_id] = session + + def _get_llm_spawner(self, trigger: str) -> Any | None: + if not self.use_llm: + return None + if self._llm_spawner is not None: + return self._llm_spawner + try: + from agentdrive.cognition.llm_spawner import LLMBranchSpawner + + fabric_context: dict[str, Any] = {} + if hasattr(self.recorder, "get_fabric_context_pack"): + try: + fabric_context = self.recorder.get_fabric_context_pack(max_tokens=800) + except Exception: + pass + spawner = LLMBranchSpawner( + agent_id=self.program_id or "multiverse-llm", + harness_task=trigger, + fabric_context=fabric_context, + ) + if spawner.llm_available: + self._llm_spawner = spawner + return spawner + except Exception: + pass + return None + + # ------------------------------------------------------------------ + # Session lifecycle + # ------------------------------------------------------------------ + + def spawn_session( + self, + trigger: str, + *, + cycle_id: str | None = None, + correlation_id: str | None = None, + n_branches: int = 7, + roles: list[str] | None = None, + durable: bool = False, + ) -> MultiverseSession: + """Open a new superposition session and spawn orthogonal branches.""" + ts = int(time.time()) + if not cycle_id: + cycle_id = self.recorder.start_cycle( + str(ts), {"source": "multiverse_spawn_session", "trigger": trigger[:120]} + ) + session_id = f"multiverse-session:{ts}" + correlation_id = correlation_id or f"multiverse-corr:{uuid.uuid4().hex[:12]}" + + session = MultiverseSession( + session_id=session_id, + trigger=trigger, + cycle_id=cycle_id, + correlation_id=correlation_id, + program_id=self.program_id, + constitution_refs=list(self.constitution_refs), + user_objective_refs=list(self.user_objective_refs), + ) + + active_roles = roles or list(COGNITIVE_ROLES[: max(1, n_branches)]) + for i in range(n_branches): + role = active_roles[i % len(active_roles)] + axis = DIVERGENCE_AXES[i % len(DIVERGENCE_AXES)] + branch = self._spawn_branch(session, role, axis, index=i) + session.branches.append(branch) + + self._sessions[session_id] = session + self._persist_session_observation(session, page_type="multiverse-session") + self._record_session_edges(session, event="spawned") + if durable: + self._mark_durable(session) + self._publish_multiverse_event(session, phase="spawned") + self._save_session(session) + return session + + def simulate_branches( + self, + session_id: str, + *, + forward_steps: int | None = None, + ) -> MultiverseSession: + """Roll each branch forward N steps.""" + session = self._require_session(session_id) + steps = forward_steps or self.default_forward_steps + + for branch in session.branches: + branch.forward_steps = self._simulate_forward(session.trigger, branch, steps) + self._persist_branch_observation(session, branch) + self._record_branch_edge(session, branch, BRANCH_SIMULATED_FORWARD) + + self._save_session(session) + return session + + def extract_invariants(self, session_id: str) -> MultiverseSession: + """Compute robust/fragile/convergence/divergence across branches.""" + session = self._require_session(session_id) + session.invariants = self._compute_invariants(session.branches) + session.convergence_points = self._find_convergence_points(session.branches) + session.divergence_points = self._find_divergence_points(session.branches) + + for inv in session.invariants: + rel = INVARIANT_EXTRACTED + if inv.kind == InvariantKind.CONVERGENCE: + rel = CONVERGENCE_DETECTED + elif inv.kind == InvariantKind.DIVERGENCE: + rel = DIVERGENCE_DETECTED + self.recorder.record_connection( + session.cycle_id, + session.session_id, + f"invariant:{hash(inv.statement) & 0xFFFFFF:06x}", + rel, + metadata={ + "statement": inv.statement, + "branch_coverage": inv.branch_coverage, + "kind": inv.kind.value, + "gbrain_signal_score": round(0.4 + inv.branch_coverage * 0.5, 3), + "program_id": self.program_id, + }, + ) + + self._persist_session_observation(session, page_type="multiverse-invariants") + self._save_session(session) + return session + + def densify_invariant_clusters(self, session_id: str) -> dict[str, Any]: + """M3: strengthen robust invariant edges in the Experience Graph (GraphGardener hook).""" + session = self._require_session(session_id) + densified = 0 + try: + from agentdrive.evolution.experience_graph import DENSIFIED_VIA_GARDENER + except ImportError: + DENSIFIED_VIA_GARDENER = "densified_via_gardener" + + for inv in session.invariants: + if inv.kind != InvariantKind.ROBUST: + continue + inv_slug = f"invariant:{hash(inv.statement) & 0xFFFFFF:06x}" + try: + self.recorder.record_connection( + session.cycle_id, + session.session_id, + inv_slug, + DENSIFIED_VIA_GARDENER, + metadata={ + "statement": inv.statement[:200], + "branch_coverage": inv.branch_coverage, + "multiverse_densification": True, + "gbrain_signal_score": round(0.5 + inv.branch_coverage * 0.35, 3), + }, + ) + densified += 1 + except Exception: + pass + + result = {"session_id": session_id, "densified_invariants": densified} + if densified and hasattr(self.recorder, "compute_cycle_density"): + try: + result["cycle_density"] = self.recorder.compute_cycle_density(session.cycle_id) + except Exception: + pass + self._write_observation( + f"multiverse-densify-{session_id}", + "multiverse-invariant-densification", + result, + ) + return result + + def reopen_stale_sessions(self, *, max_age_hours: float = 24.0) -> list[str]: + """M4: mark stale open superposition sessions as reopened.""" + drive_path = getattr(self.recorder, "drive_path", None) + if not drive_path: + return [] + from agentdrive.cognition.research_thread import find_stale_open_sessions + + reopened: list[str] = [] + for sid in find_stale_open_sessions(Path(drive_path), max_age_hours=max_age_hours): + session = self.get_session(sid) + if not session or session.status != SessionStatus.OPEN: + continue + session.status = SessionStatus.REOPENED + self._save_session(session) + self._publish_multiverse_event(session, phase="reopened") + reopened.append(sid) + return reopened + + def stress_test_branch( + self, + session_id: str, + branch_id: str, + *, + adversary_rationale: str | None = None, + ) -> AdversaryVerdict: + """Adversary pre-mortem on a candidate branch (LLM when available, else heuristic).""" + session = self._require_session(session_id) + branch = self._require_branch(session, branch_id) + + spawner = self._get_llm_spawner(session.trigger) + if spawner: + llm_verdict = spawner.adversary_stress_test(branch) + if llm_verdict: + branch.stress_test = llm_verdict + self._record_stress_test_edges(session, branch, llm_verdict) + return llm_verdict + + fatal: list[str] = [] + for assumption in branch.assumptions: + if any(w in assumption.lower() for w in ("always", "never", "guaranteed", "zero risk")): + fatal.append(f"Overconfident assumption: {assumption}") + + if branch.role == "adversary": + fatal.append("Branch is already adversarial — double-counting risk") + + passed = len(fatal) == 0 + verdict = AdversaryVerdict( + passed=passed, + fatal_flaws=fatal if not passed else [], + mitigations=["Add reversible gate", "Run probe before full commit"] + if not passed + else [], + rationale=adversary_rationale or f"Adversary stress-test on {branch_id}", + ) + branch.stress_test = verdict + self._record_stress_test_edges(session, branch, verdict) + return verdict + + def collapse( + self, + session_id: str, + *, + branch_id: str | None = None, + policy: CollapsePolicy | None = None, + reason: str | None = None, + ) -> MultiverseSession: + """Collapse superposition to one committed path.""" + session = self._require_session(session_id) + + if branch_id is None: + branch_id, policy, reason = self._auto_select_collapse(session) + + branch = self._require_branch(session, branch_id) + session.collapsed_branch_id = branch_id + session.collapse_policy = policy or CollapsePolicy.PATTERN_CRYSTALLIZED + session.collapse_reason = reason or f"Collapsed to {branch.role} path" + session.status = SessionStatus.COLLAPSED + + self.recorder.record_connection( + session.cycle_id, + session.session_id, + branch_id, + PATH_COLLAPSED, + metadata={ + "collapse_policy": session.collapse_policy.value, + "collapse_reason": session.collapse_reason, + "robustness_score": branch.robustness_score, + "gbrain_signal_score": round(0.5 + branch.robustness_score * 0.4, 3), + "program_id": self.program_id, + }, + ) + self._write_observation( + f"multiverse-collapse-{session.session_id}", + "multiverse-collapse", + { + "session_id": session_id, + "collapsed_branch_id": branch_id, + "policy": session.collapse_policy.value, + "reason": session.collapse_reason, + "invariants": [asdict(i) for i in session.invariants], + }, + ) + self._publish_multiverse_event(session, phase="collapsed") + self._save_session(session) + return session + + def run_full( + self, + trigger: str, + *, + n_branches: int = 7, + forward_steps: int | None = None, + stress_test_top_n: int = 2, + durable: bool = False, + densify_invariants: bool = True, + ) -> MultiverseSession: + """One-shot: spawn → simulate → invariants → stress-test → collapse.""" + session = self.spawn_session(trigger, n_branches=n_branches, durable=durable) + self.simulate_branches(session.session_id, forward_steps=forward_steps) + self.extract_invariants(session.session_id) + if densify_invariants: + self.densify_invariant_clusters(session.session_id) + + ranked = sorted(session.branches, key=lambda b: b.robustness_score, reverse=True) + for branch in ranked[:stress_test_top_n]: + self.stress_test_branch(session.session_id, branch.branch_id) + + return self.collapse(session.session_id) + + def resolve_llm_mode(self, trigger: str | None = None) -> str: + """Return how branches were produced: local llm, heuristic fallback, or external MCP parent.""" + if self._get_llm_spawner(trigger or "") and self._llm_spawner: + return "llm" + return "heuristic" + + def ingest_external_parent_decision( + self, + trigger: str, + branches: list[dict[str, Any]], + *, + collapsed_branch_id: str, + invariants: list[dict[str, Any]] | None = None, + collapse_reason: str = "", + collapse_policy: str | CollapsePolicy | None = None, + reasoning_provider: str = "mcp-external", + convergence_points: list[str] | None = None, + divergence_points: list[str] | None = None, + fabric_reasoning: dict[str, Any] | None = None, + program_id: str | None = None, + user_objective_refs: list[str] | None = None, + densify_invariants: bool = True, + ) -> MultiverseSession: + """ + Record a full multiverse collapse supplied by an external MCP client (Grok, Claude, Codex, etc.). + + The connected model performs branch reasoning in its own context, then submits structured + branches + collapse here. AgentDrive persists session DNA and wires record_parent_decision. + """ + if not branches: + raise ValueError("branches must be a non-empty list") + if not collapsed_branch_id: + raise ValueError("collapsed_branch_id is required") + + ts = int(time.time()) + cycle_id = self.recorder.start_cycle( + str(ts), + { + "source": "external_parent_decision", + "trigger": trigger[:120], + "reasoning_provider": reasoning_provider, + }, + ) + session_id = f"multiverse-session:{ts}" + correlation_id = f"multiverse-corr:{uuid.uuid4().hex[:12]}" + + if program_id: + self.program_id = program_id + if user_objective_refs: + self.user_objective_refs = list(user_objective_refs) + + parsed_branches = [ + self._branch_from_external_dict(b, index=i) for i, b in enumerate(branches) + ] + branch_ids = {b.branch_id for b in parsed_branches} + if collapsed_branch_id not in branch_ids: + raise ValueError( + f"collapsed_branch_id {collapsed_branch_id!r} not found in branches " + f"({sorted(branch_ids)})" + ) + + policy = CollapsePolicy.EXTERNAL_PARENT + if collapse_policy is not None: + policy = ( + collapse_policy + if isinstance(collapse_policy, CollapsePolicy) + else CollapsePolicy(str(collapse_policy)) + ) + + session = MultiverseSession( + session_id=session_id, + trigger=trigger, + cycle_id=cycle_id, + correlation_id=correlation_id, + branches=parsed_branches, + invariants=self._invariants_from_external(invariants or [], parsed_branches), + convergence_points=list(convergence_points or []), + divergence_points=list(divergence_points or []), + status=SessionStatus.COLLAPSED, + collapsed_branch_id=collapsed_branch_id, + collapse_reason=collapse_reason + or f"External parent ({reasoning_provider}) collapsed path", + collapse_policy=policy, + program_id=self.program_id, + constitution_refs=list(self.constitution_refs), + user_objective_refs=list(self.user_objective_refs), + reasoning_provider=reasoning_provider, + llm_mode="external", + external_fabric_reasoning=dict(fabric_reasoning) if fabric_reasoning else None, + ) + + self._sessions[session_id] = session + self._persist_session_observation(session, page_type="multiverse-session-external") + self._record_session_edges(session, event="spawned") + for branch in session.branches: + self._persist_branch_observation(session, branch) + self._record_branch_edge(session, branch, BRANCH_SPAWNED) + if branch.forward_steps: + self._record_branch_edge(session, branch, BRANCH_SIMULATED_FORWARD) + for inv in session.invariants: + rel = INVARIANT_EXTRACTED + if inv.kind == InvariantKind.CONVERGENCE: + rel = CONVERGENCE_DETECTED + elif inv.kind == InvariantKind.DIVERGENCE: + rel = DIVERGENCE_DETECTED + self.recorder.record_connection( + session.cycle_id, + session.session_id, + f"invariant:{hash(inv.statement) & 0xFFFFFF:06x}", + rel, + metadata={ + "statement": inv.statement, + "branch_coverage": inv.branch_coverage, + "kind": inv.kind.value, + "external_parent": True, + "reasoning_provider": reasoning_provider, + "gbrain_signal_score": round(0.45 + inv.branch_coverage * 0.45, 3), + "program_id": self.program_id, + }, + ) + + collapsed = self._require_branch(session, collapsed_branch_id) + self.recorder.record_connection( + session.cycle_id, + session.session_id, + collapsed_branch_id, + PATH_COLLAPSED, + metadata={ + "collapse_policy": session.collapse_policy.value, + "collapse_reason": session.collapse_reason, + "robustness_score": collapsed.robustness_score, + "reasoning_provider": reasoning_provider, + "llm_mode": "external", + "gbrain_signal_score": round(0.55 + collapsed.robustness_score * 0.35, 3), + "program_id": self.program_id, + }, + ) + self._write_observation( + f"multiverse-collapse-{session.session_id}", + "multiverse-collapse-external", + { + "session_id": session_id, + "collapsed_branch_id": collapsed_branch_id, + "policy": session.collapse_policy.value, + "reason": session.collapse_reason, + "reasoning_provider": reasoning_provider, + "invariants": [asdict(i) for i in session.invariants], + }, + ) + self._publish_multiverse_event(session, phase="collapsed") + self._save_session(session) + if densify_invariants: + self.densify_invariant_clusters(session.session_id) + return session + + def to_fabric_reasoning(self, session: MultiverseSession) -> dict[str, Any]: + """Produce fabric_reasoning payload for record_parent_fabric_reasoning / record_parent_decision.""" + if session.external_fabric_reasoning: + payload = dict(session.external_fabric_reasoning) + payload.setdefault("multiverse_session_id", session.session_id) + payload.setdefault("program_id", self.program_id) + payload.setdefault("constitution_refs", self.constitution_refs) + payload.setdefault("user_objective_refs", self.user_objective_refs) + payload.setdefault( + "collapse_policy", + session.collapse_policy.value if session.collapse_policy else None, + ) + payload.setdefault("reasoning_provider", session.reasoning_provider) + payload.setdefault("llm_mode", session.llm_mode) + return payload + + collapsed = ( + self._require_branch(session, session.collapsed_branch_id) + if session.collapsed_branch_id + else None + ) + robust_invariants = [ + inv.statement for inv in session.invariants if inv.kind == InvariantKind.ROBUST + ] + + elements = [session.session_id] + elements.extend(f"branch:{b.branch_id}" for b in session.branches[:4]) + elements.extend(f"invariant:{inv.statement[:48]}" for inv in session.invariants[:3]) + + coverage = ( + sum(i.branch_coverage for i in session.invariants) / len(session.invariants) + if session.invariants + else 0.5 + ) + + return { + "fabric_elements_considered": elements, + "structural_pattern_matched": ( + f"multiverse_cognition:robust_invariant_coverage_{coverage:.2f}" + ), + "decision_rationale": ( + f"Collapsed to {collapsed.role if collapsed else 'unknown'} path " + f"({session.collapse_reason}). " + f"Robust invariants: {robust_invariants[:2]}" + ), + "expected_lift_signal": round(0.03 + coverage * 0.05, 3), + "multiverse_session_id": session.session_id, + "invariants": robust_invariants, + "collapse_policy": (session.collapse_policy.value if session.collapse_policy else None), + "program_id": self.program_id, + "constitution_refs": self.constitution_refs, + "user_objective_refs": self.user_objective_refs, + "reasoning_provider": session.reasoning_provider, + "llm_mode": session.llm_mode, + } + + def get_session(self, session_id: str) -> MultiverseSession | None: + if session_id in self._sessions: + return self._sessions[session_id] + if self._store: + loaded = self._store.load(session_id) + if loaded: + self._sessions[session_id] = loaded + return loaded + return None + + def list_sessions(self, *, limit: int = 10) -> list[MultiverseSession]: + if self._store: + sessions = self._store.list_recent(limit=limit) + for s in sessions: + self._sessions[s.session_id] = s + return sessions + return list(self._sessions.values())[:limit] + + def briefing_context(self, *, limit: int = 5) -> dict[str, Any]: + if self._store: + return self._store.briefing_context(limit=limit) + return {"session_count_recent": len(self._sessions)} + + def record_parent_decision( + self, + session: MultiverseSession, + *, + integrated: Any | None = None, + actions_taken: list[str] | None = None, + ) -> dict[str, Any]: + """ + Wire collapsed multiverse session into the canonical Parent decision path. + + Calls IntegratedRealTimeEvolutionSystem.record_parent_decision when + ``integrated`` is provided; otherwise only returns the decision payload. + """ + collapsed = ( + self._require_branch(session, session.collapsed_branch_id) + if session.collapsed_branch_id + else None + ) + fabric_reasoning = self.to_fabric_reasoning(session) + decision = { + "directive": collapsed.path_summary if collapsed else session.trigger, + "multiverse_session_id": session.session_id, + "collapsed_branch_id": session.collapsed_branch_id, + "collapse_policy": (session.collapse_policy.value if session.collapse_policy else None), + } + + result: dict[str, Any] = { + "decision": decision, + "fabric_reasoning": fabric_reasoning, + "cycle_id": session.cycle_id, + } + + if integrated is not None and hasattr(integrated, "record_parent_decision"): + slug = integrated.record_parent_decision( + session.cycle_id, + decision, + actions_taken=actions_taken + or [f"multiverse_collapse:{session.collapsed_branch_id}"], + fabric_reasoning=fabric_reasoning, + ) + result["parent_decision_slug"] = slug + if hasattr(self.recorder, "record_connection"): + self.recorder.record_connection( + session.cycle_id, + session.session_id, + slug or f"parent_decision:{session.session_id}", + MULTIVERSE_INFORMED_DECISION, + metadata={ + "collapse_policy": session.collapse_policy.value + if session.collapse_policy + else None, + "gbrain_signal_score": 0.74, + "program_id": self.program_id, + }, + ) + + return result + + def to_mcp_dict(self, session: MultiverseSession) -> dict[str, Any]: + """Serialize session for MCP tool responses.""" + return { + "session_id": session.session_id, + "trigger": session.trigger, + "status": session.status.value, + "branch_count": len(session.branches), + "branches": [ + { + "branch_id": b.branch_id, + "role": b.role, + "path_summary": b.path_summary, + "robustness_score": b.robustness_score, + "forward_steps": [asdict(s) for s in b.forward_steps], + "stress_test_passed": (b.stress_test.passed if b.stress_test else None), + } + for b in session.branches + ], + "invariants": [asdict(i) for i in session.invariants], + "convergence_points": session.convergence_points, + "divergence_points": session.divergence_points, + "collapsed_branch_id": session.collapsed_branch_id, + "collapse_policy": (session.collapse_policy.value if session.collapse_policy else None), + "collapse_reason": session.collapse_reason, + "fabric_reasoning": self.to_fabric_reasoning(session), + "reasoning_provider": session.reasoning_provider, + "llm_mode": session.llm_mode, + } + + # ------------------------------------------------------------------ + # Internal: branch spawning & simulation (Tier 0 heuristics) + # ------------------------------------------------------------------ + + def _branch_from_external_dict(self, raw: dict[str, Any], *, index: int) -> Branch: + branch_id = str(raw.get("branch_id") or f"branch:{raw.get('role', 'branch')}-{index}") + steps_raw = raw.get("forward_steps") or [] + steps: list[ForwardStep] = [] + for i, step in enumerate(steps_raw): + if isinstance(step, dict): + steps.append( + ForwardStep( + step_index=int(step.get("step_index", i + 1)), + description=str(step.get("description", "")), + confidence=float(step.get("confidence", 0.7)), + ) + ) + st = raw.get("stress_test") + stress: AdversaryVerdict | None = None + if isinstance(st, dict): + stress = AdversaryVerdict( + passed=bool(st.get("passed", st.get("stress_test_passed", True))), + fatal_flaws=list(st.get("fatal_flaws", [])), + mitigations=list(st.get("mitigations", [])), + rationale=str(st.get("rationale", "")), + ) + elif "stress_test_passed" in raw: + passed = bool(raw.get("stress_test_passed")) + stress = AdversaryVerdict( + passed=passed, + fatal_flaws=list(raw.get("fatal_flaws", [])) if not passed else [], + mitigations=list(raw.get("mitigations", [])), + rationale=str(raw.get("stress_rationale", "")), + ) + return Branch( + branch_id=branch_id, + role=str(raw.get("role", COGNITIVE_ROLES[index % len(COGNITIVE_ROLES)])), + path_summary=str(raw.get("path_summary", "")), + assumptions=[str(a) for a in raw.get("assumptions", [])], + divergence_axes=[str(a) for a in raw.get("divergence_axes", [])], + forward_steps=steps, + robustness_score=float(raw.get("robustness_score", 0.0)), + fragility_flags=[str(f) for f in raw.get("fragility_flags", [])], + stress_test=stress, + ) + + def _invariants_from_external( + self, + raw_invariants: list[dict[str, Any]], + branches: list[Branch], + ) -> list[Invariant]: + if not raw_invariants: + return self._compute_invariants(branches) + parsed: list[Invariant] = [] + for raw in raw_invariants: + kind = raw.get("kind", InvariantKind.ROBUST) + if isinstance(kind, str): + kind = InvariantKind(kind) + parsed.append( + Invariant( + statement=str(raw.get("statement", "")), + branch_coverage=float(raw.get("branch_coverage", 0.0)), + kind=kind, + source_branches=[str(s) for s in raw.get("source_branches", [])], + ) + ) + return parsed + + def _spawn_branch( + self, + session: MultiverseSession, + role: str, + axis: str, + *, + index: int, + ) -> Branch: + spawner = self._get_llm_spawner(session.trigger) + if spawner: + llm_branch = spawner.spawn_branch(session.trigger, role, axis, index=index) + if llm_branch: + self._persist_branch_observation(session, llm_branch) + self._record_branch_edge(session, llm_branch, BRANCH_SPAWNED) + return llm_branch + + template = ROLE_PATH_TEMPLATES.get(role, "Custom path") + branch_id = f"branch:{role}-{index}" + path_summary = f"[{axis}] {template} — for: {session.trigger[:80]}" + + assumptions = [ + f"{axis} is the primary divergence axis", + f"{role} lens reveals non-obvious structure", + ] + if role == "adversary": + assumptions.append("At least one optimistic assumption fails") + if role == "operator": + assumptions.append("Smallest shippable slice is sufficient to learn") + + branch = Branch( + branch_id=branch_id, + role=role, + path_summary=path_summary, + assumptions=assumptions, + divergence_axes=[axis], + ) + self._persist_branch_observation(session, branch) + self._record_branch_edge(session, branch, BRANCH_SPAWNED) + return branch + + def _simulate_forward( + self, + trigger: str, + branch: Branch, + steps: int, + ) -> list[ForwardStep]: + spawner = self._get_llm_spawner(trigger) + if spawner: + llm_steps = spawner.simulate_forward(trigger, branch, steps) + if llm_steps: + return llm_steps + + axis = branch.divergence_axes[0] if branch.divergence_axes else "risk" + projections = [ + f"Immediate: apply {branch.role} lens on '{trigger[:60]}' via {axis} axis", + f"Second-order: dependencies shift; {branch.role} assumptions stress-tested", + f"Equilibrium: path {'stabilizes' if branch.role != 'adversary' else 'exposes fatal flaw'}", + ] + return [ + ForwardStep(step_index=i + 1, description=projections[i], confidence=0.65 + i * 0.05) + for i in range(min(steps, len(projections))) + ] + + def _compute_invariants(self, branches: list[Branch]) -> list[Invariant]: + if not branches: + return [] + + n = len(branches) + # Tier 0: shared role/axis patterns as synthetic invariants + axis_counts: dict[str, int] = {} + role_endings: dict[str, list[str]] = {} + + for b in branches: + for axis in b.divergence_axes: + axis_counts[axis] = axis_counts.get(axis, 0) + 1 + ending = b.forward_steps[-1].description if b.forward_steps else "" + role_endings.setdefault(b.role, []).append(ending) + + invariants: list[Invariant] = [] + + invariants.append( + Invariant( + statement="Every path requires explicit assumption naming before execution", + branch_coverage=min(1.0, len(branches) / n), + kind=InvariantKind.ROBUST, + source_branches=[b.branch_id for b in branches], + ) + ) + + for axis, count in axis_counts.items(): + coverage = count / n + kind = ( + InvariantKind.ROBUST if coverage >= self.robust_threshold else InvariantKind.FRAGILE + ) + invariants.append( + Invariant( + statement=f"Divergence axis '{axis}' shapes distinct outcome class", + branch_coverage=coverage, + kind=kind, + source_branches=[b.branch_id for b in branches if axis in b.divergence_axes], + ) + ) + + for branch in branches: + branch.robustness_score = round( + sum(1 for inv in invariants if inv.kind == InvariantKind.ROBUST) + / max(len(invariants), 1), + 3, + ) + + return invariants + + def _find_convergence_points(self, branches: list[Branch]) -> list[str]: + if len(branches) < 2: + return [] + return ["All paths require fabric DNA recording before execution"] + + def _find_divergence_points(self, branches: list[Branch]) -> list[str]: + axes = {b.divergence_axes[0] for b in branches if b.divergence_axes} + return [f"Axis '{a}' produces incompatible commitment timing" for a in sorted(axes)[:3]] + + def _auto_select_collapse( + self, + session: MultiverseSession, + ) -> tuple[str, CollapsePolicy, str]: + ranked = sorted(session.branches, key=lambda b: b.robustness_score, reverse=True) + top = ranked[0] + + for branch in ranked[:2]: + if branch.stress_test is None: + self.stress_test_branch(session.session_id, branch.branch_id) + + if top.stress_test and not top.stress_test.passed: + for alt in ranked[1:]: + if alt.stress_test and alt.stress_test.passed: + return ( + alt.branch_id, + CollapsePolicy.ADVERSARY_CLEAR, + f"Top branch failed stress-test; {alt.role} path is adversary-clear", + ) + + if top.robustness_score >= 0.75: + return ( + top.branch_id, + CollapsePolicy.PATTERN_CRYSTALLIZED, + f"Pattern crystallized on {top.role} path (robustness={top.robustness_score})", + ) + + return ( + top.branch_id, + CollapsePolicy.BUDGET_EXHAUSTED, + f"Highest robustness {top.role} path selected (score={top.robustness_score})", + ) + + # ------------------------------------------------------------------ + # Persistence helpers + # ------------------------------------------------------------------ + + def _record_session_edges(self, session: MultiverseSession, *, event: str) -> None: + self.recorder.record_connection( + session.cycle_id, + session.cycle_id, + session.session_id, + MULTIVERSE_SESSION, + metadata={ + "event": event, + "trigger": session.trigger[:200], + "correlation_id": session.correlation_id, + "gbrain_signal_score": 0.72, + "program_id": self.program_id, + "constitution_refs": self.constitution_refs, + }, + ) + + def _record_branch_edge( + self, session: MultiverseSession, branch: Branch, relation: str + ) -> None: + self.recorder.record_connection( + session.cycle_id, + session.session_id, + branch.branch_id, + relation, + metadata={ + "role": branch.role, + "divergence_axes": branch.divergence_axes, + "gbrain_signal_score": 0.6, + "program_id": self.program_id, + }, + ) + + def _persist_session_observation(self, session: MultiverseSession, *, page_type: str) -> None: + self._write_observation( + session.session_id, + page_type, + { + "session": asdict(session), + "branches": [asdict(b) for b in session.branches], + }, + ) + + def _persist_branch_observation(self, session: MultiverseSession, branch: Branch) -> None: + self._write_observation( + branch.branch_id, + "multiverse-branch", + {"session_id": session.session_id, "branch": asdict(branch)}, + ) + + def _write_observation(self, slug: str, page_type: str, content: dict[str, Any]) -> Path | None: + drive_path = getattr(self.recorder, "drive_path", None) + if not drive_path: + return None + obs_dir = Path(drive_path) / "observations" / "meta-evolution" / "multiverse" + obs_dir.mkdir(parents=True, exist_ok=True) + safe_slug = "".join(c if c.isalnum() or c in "-_:" else "_" for c in slug)[:120] + path = obs_dir / f"{safe_slug}.json" + payload = { + "page_type": page_type, + "slug": slug, + "timestamp": time.time(), + "program_id": self.program_id, + "constitution_refs": self.constitution_refs, + "content": content, + } + path.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8") + return path + + def _record_stress_test_edges( + self, + session: MultiverseSession, + branch: Branch, + verdict: AdversaryVerdict, + ) -> None: + self.recorder.record_connection( + session.cycle_id, + session.session_id, + branch.branch_id, + BRANCH_STRESS_TESTED, + metadata={ + "passed": verdict.passed, + "fatal_flaws": verdict.fatal_flaws, + "gbrain_signal_score": 0.65 if verdict.passed else 0.35, + "program_id": self.program_id, + }, + ) + self._write_observation( + f"multiverse-council-verdict-{branch.branch_id}", + "multiverse-council-verdict", + { + "session_id": session.session_id, + "branch_id": branch.branch_id, + "verdict": asdict(verdict), + }, + ) + + def _mark_durable(self, session: MultiverseSession) -> None: + drive_path = getattr(self.recorder, "drive_path", None) + if not drive_path: + return + from agentdrive.cognition.research_thread import write_durable_research_thread_manifest + + write_durable_research_thread_manifest(Path(drive_path), session) + self.recorder.record_connection( + session.cycle_id, + session.session_id, + f"research-thread:{session.session_id}", + MULTIVERSE_SESSION, + metadata={"durable": True, "gbrain_signal_score": 0.68}, + ) + + def _publish_multiverse_event(self, session: MultiverseSession, *, phase: str) -> None: + try: + from agentdrive.mission_control.events import MultiverseUpdateEvent + from agentdrive.mission_control.server import publish_event_sync + + publish_event_sync( + MultiverseUpdateEvent( + event_type="multiverse_update", + timestamp=time.time(), + cycle_id=session.cycle_id, + correlation_id=session.correlation_id, + session_id=session.session_id, + phase=phase, + status=session.status.value, + branch_count=len(session.branches), + collapsed_branch_id=session.collapsed_branch_id, + invariants=[inv.statement for inv in session.invariants[:5]], + branches_summary=[ + {"id": b.branch_id, "role": b.role, "robustness": b.robustness_score} + for b in session.branches[:8] + ], + ) + ) + except Exception: + pass + + def _save_session(self, session: MultiverseSession) -> None: + self._sessions[session.session_id] = session + if self._store: + self._store.save(session) + + def _require_session(self, session_id: str) -> MultiverseSession: + session = self.get_session(session_id) + if not session: + raise KeyError(f"Unknown multiverse session: {session_id}") + return session + + def _require_branch(self, session: MultiverseSession, branch_id: str) -> Branch: + for branch in session.branches: + if branch.branch_id == branch_id: + return branch + raise KeyError(f"Unknown branch {branch_id} in session {session.session_id}") diff --git a/src/agentdrive/cognition/research_thread.py b/src/agentdrive/cognition/research_thread.py new file mode 100644 index 0000000..ac67c0e --- /dev/null +++ b/src/agentdrive/cognition/research_thread.py @@ -0,0 +1,77 @@ +""" +Durable multiverse superposition as research-thread manifests (M4). + +Links open multiverse sessions to GridEngine research-thread lifecycle. +""" + +from __future__ import annotations + +import json +import time +from pathlib import Path +from typing import Any + +from agentdrive.cognition.multiverse import MultiverseSession, SessionStatus + + +def write_durable_research_thread_manifest( + drive_path: Path, + session: MultiverseSession, + *, + reopen_after_hours: float = 24.0, +) -> Path: + """Write research-thread-manifest observation linked to a multiverse session.""" + obs_dir = ( + Path(drive_path) / "observations" / "meta-evolution" / "multiverse" / "research-threads" + ) + obs_dir.mkdir(parents=True, exist_ok=True) + slug = f"multiverse-research-thread-{session.session_id}" + path = obs_dir / f"{slug}.json" + + payload = { + "page_type": "research-thread-manifest", + "id": slug, + "multiverse_session_id": session.session_id, + "trigger": session.trigger, + "status": session.status.value, + "correlation_id": session.correlation_id, + "cycle_id": session.cycle_id, + "branch_count": len(session.branches), + "durable": True, + "reopen_after_hours": reopen_after_hours, + "constitution_ref": "research-constitution-multiverse-cognition@stabilization-wave-20260531", + "created_at": session.created_at, + "timestamp": time.time(), + } + path.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8") + return path + + +def list_durable_manifests(drive_path: Path, *, limit: int = 20) -> list[dict[str, Any]]: + d = Path(drive_path) / "observations" / "meta-evolution" / "multiverse" / "research-threads" + if not d.is_dir(): + return [] + out: list[dict[str, Any]] = [] + for p in sorted(d.glob("*.json"), key=lambda x: x.stat().st_mtime, reverse=True)[:limit]: + try: + out.append(json.loads(p.read_text(encoding="utf-8"))) + except (json.JSONDecodeError, OSError): + continue + return out + + +def find_stale_open_sessions( + drive_path: Path, + *, + max_age_hours: float = 24.0, +) -> list[str]: + """Return session ids that are open and older than max_age_hours.""" + from agentdrive.cognition.store import MultiverseSessionStore + + store = MultiverseSessionStore(Path(drive_path)) + cutoff = time.time() - (max_age_hours * 3600) + stale: list[str] = [] + for session in store.list_recent(limit=50): + if session.status == SessionStatus.OPEN and session.created_at < cutoff: + stale.append(session.session_id) + return stale diff --git a/src/agentdrive/cognition/roles.py b/src/agentdrive/cognition/roles.py new file mode 100644 index 0000000..b392067 --- /dev/null +++ b/src/agentdrive/cognition/roles.py @@ -0,0 +1,58 @@ +""" +Cognitive Agent Team role lenses for Multiverse branch generation. + +Condensed from the Cognitive Agent Team framework (architect, adversary, scout, etc.). +Each role defines how one parallel timeline is spawned and simulated. +""" + +from __future__ import annotations + +ROLE_PROMPTS: dict[str, str] = { + "architect": ( + "You are the Architect lens. Extract the structural skeleton underneath the decision. " + "Map whole system first, then locate the intervention point. Name underlying patterns, " + "tensions, and load-bearing vs decorative elements." + ), + "adversary": ( + "You are the Adversary lens. Stress-test before reality does. Find the timeline where " + "the optimistic plan breaks. Run a pre-mortem: what fatal flaw kills this path?" + ), + "scout": ( + "You are the Scout lens. Map terrain before anyone moves. What is known, unknown, assumed? " + "Which intelligence gaps become blind spots if not filled first?" + ), + "operator": ( + "You are the Operator lens. Convert vision into velocity. Smallest shippable slice, " + "dependency order, momentum preservation. What ships first to learn fastest?" + ), + "surgeon": ( + "You are the Surgeon lens. Precision intervention. Find the highest-leverage minimal cut. " + "What is the smallest change that moves the whole system?" + ), + "beacon": ( + "You are the Beacon lens. Signal and propagation. How does this message spread through " + "networks? Optimize for discoverability and audience reception." + ), + "watchdog": ( + "You are the Watchdog lens. Security and attack paths. Trace anomalies to source. " + "What attack surfaces open during this path? What is the blast radius?" + ), +} + +AXIS_GUIDANCE: dict[str, str] = { + "risk": "Prioritize risk posture: what breaks, what is reversible, what is catastrophic.", + "speed": "Prioritize time-to-learning: fastest probe that yields real signal.", + "reversibility": "Prioritize rollback: every step must have an undo path.", + "cost": "Prioritize resource constraint: minimum spend for maximum structural clarity.", + "dependency_order": "Prioritize sequencing: what must happen before what.", +} + + +def role_system_prompt(role: str, axis: str) -> str: + """Compose system prompt for one branch's LLM call.""" + base = ROLE_PROMPTS.get(role, f"You are the {role} cognitive lens.") + axis_hint = AXIS_GUIDANCE.get(axis, f"Diverge on axis: {axis}.") + return ( + f"{base}\n\nDivergence axis: {axis}. {axis_hint}\n\n" + "Output ONLY valid compact JSON. No markdown fences, no prose outside JSON." + ) diff --git a/src/agentdrive/cognition/store.py b/src/agentdrive/cognition/store.py new file mode 100644 index 0000000..4b73897 --- /dev/null +++ b/src/agentdrive/cognition/store.py @@ -0,0 +1,193 @@ +""" +Persistent multiverse session store — survives process restarts and MCP calls. + +Sessions are first-class drive artifacts under: + drive/meta_evolution/multiverse/sessions/.json +""" + +from __future__ import annotations + +import json +from dataclasses import asdict +from pathlib import Path +from typing import Any + +from agentdrive.cognition.multiverse import ( + AdversaryVerdict, + Branch, + CollapsePolicy, + ForwardStep, + Invariant, + InvariantKind, + MultiverseSession, + SessionStatus, +) + + +def _sessions_dir(drive_path: Path) -> Path: + d = drive_path / "meta_evolution" / "multiverse" / "sessions" + d.mkdir(parents=True, exist_ok=True) + return d + + +def _enum_val(obj: Any) -> Any: + if hasattr(obj, "value"): + return obj.value + return obj + + +def session_to_dict(session: MultiverseSession) -> dict[str, Any]: + """JSON-serializable dict for persistence.""" + data = asdict(session) + data["status"] = session.status.value + data["collapse_policy"] = session.collapse_policy.value if session.collapse_policy else None + for inv in data.get("invariants", []): + if isinstance(inv.get("kind"), InvariantKind): + inv["kind"] = inv["kind"].value + return data + + +def session_from_dict(data: dict[str, Any]) -> MultiverseSession: + """Rehydrate MultiverseSession from persisted JSON.""" + branches: list[Branch] = [] + for raw in data.get("branches", []): + steps = [ + ForwardStep(**s) if isinstance(s, dict) else s for s in raw.get("forward_steps", []) + ] + st = raw.get("stress_test") + stress = AdversaryVerdict(**st) if isinstance(st, dict) else None + branches.append( + Branch( + branch_id=raw["branch_id"], + role=raw["role"], + path_summary=raw["path_summary"], + assumptions=list(raw.get("assumptions", [])), + divergence_axes=list(raw.get("divergence_axes", [])), + forward_steps=steps, + robustness_score=float(raw.get("robustness_score", 0.0)), + fragility_flags=list(raw.get("fragility_flags", [])), + stress_test=stress, + ) + ) + + invariants: list[Invariant] = [] + for raw in data.get("invariants", []): + kind = raw.get("kind", InvariantKind.ROBUST) + if isinstance(kind, str): + kind = InvariantKind(kind) + invariants.append( + Invariant( + statement=raw["statement"], + branch_coverage=float(raw.get("branch_coverage", 0.0)), + kind=kind, + source_branches=list(raw.get("source_branches", [])), + ) + ) + + status = data.get("status", SessionStatus.OPEN) + if isinstance(status, str): + status = SessionStatus(status) + + collapse_policy = data.get("collapse_policy") + if isinstance(collapse_policy, str): + collapse_policy = CollapsePolicy(collapse_policy) + + return MultiverseSession( + session_id=data["session_id"], + trigger=data["trigger"], + cycle_id=data["cycle_id"], + correlation_id=data["correlation_id"], + branches=branches, + invariants=invariants, + convergence_points=list(data.get("convergence_points", [])), + divergence_points=list(data.get("divergence_points", [])), + status=status, + collapsed_branch_id=data.get("collapsed_branch_id"), + collapse_reason=data.get("collapse_reason"), + collapse_policy=collapse_policy, + program_id=data.get("program_id"), + constitution_refs=list(data.get("constitution_refs", [])), + user_objective_refs=list(data.get("user_objective_refs", [])), + created_at=float(data.get("created_at", 0.0)), + reasoning_provider=data.get("reasoning_provider"), + llm_mode=str(data.get("llm_mode", "heuristic")), + external_fabric_reasoning=data.get("external_fabric_reasoning"), + ) + + +class MultiverseSessionStore: + """Read/write multiverse sessions on the drive.""" + + def __init__(self, drive_path: Path) -> None: + self.drive_path = Path(drive_path) + + def save(self, session: MultiverseSession) -> Path: + path = _sessions_dir(self.drive_path) / f"{session.session_id}.json" + path.write_text( + json.dumps(session_to_dict(session), indent=2, default=str), + encoding="utf-8", + ) + return path + + def load(self, session_id: str) -> MultiverseSession | None: + path = _sessions_dir(self.drive_path) / f"{session_id}.json" + if not path.is_file(): + return None + try: + data = json.loads(path.read_text(encoding="utf-8")) + return session_from_dict(data) + except (json.JSONDecodeError, KeyError, ValueError): + return None + + def list_recent(self, *, limit: int = 10) -> list[MultiverseSession]: + sessions_dir = _sessions_dir(self.drive_path) + paths = sorted( + sessions_dir.glob("multiverse-session:*.json"), + key=lambda p: p.stat().st_mtime, + reverse=True, + ) + out: list[MultiverseSession] = [] + for path in paths[:limit]: + try: + data = json.loads(path.read_text(encoding="utf-8")) + out.append(session_from_dict(data)) + except (json.JSONDecodeError, KeyError, ValueError): + continue + return out + + def briefing_context(self, *, limit: int = 5) -> dict[str, Any]: + """Compact multiverse context for Parent/Overseer briefings.""" + recent = self.list_recent(limit=limit) + collapsed = [s for s in recent if s.status == SessionStatus.COLLAPSED] + open_sessions = [s for s in recent if s.status == SessionStatus.OPEN] + + top_invariants: list[str] = [] + for s in collapsed[:3]: + for inv in s.invariants: + if inv.kind == InvariantKind.ROBUST and inv.statement not in top_invariants: + top_invariants.append(inv.statement) + + return { + "recent_collapses": [ + { + "session_id": s.session_id, + "trigger": s.trigger[:120], + "collapsed_branch_id": s.collapsed_branch_id, + "collapse_policy": _enum_val(s.collapse_policy), + "robust_invariant_count": sum( + 1 for i in s.invariants if i.kind == InvariantKind.ROBUST + ), + } + for s in collapsed[:3] + ], + "open_superposition": [ + { + "session_id": s.session_id, + "trigger": s.trigger[:120], + "branch_count": len(s.branches), + } + for s in open_sessions[:2] + ], + "top_invariants_from_recent_sessions": top_invariants[:5], + "session_count_recent": len(recent), + } diff --git a/src/agentdrive/constants.py b/src/agentdrive/constants.py index 094e3ad..7f119bc 100644 --- a/src/agentdrive/constants.py +++ b/src/agentdrive/constants.py @@ -238,7 +238,7 @@ def get_default_drive_path() -> Path: def get_swarms_dir() -> Path: - """Root for all swarm-isolated AgentDrives (each sub-agent gets its own DNA).""" + """Root for all swarm-scoped AgentDrives (shared ``/drive/`` per swarm).""" return get_agentdrive_home() / "swarms" diff --git a/src/agentdrive/dreaming/__init__.py b/src/agentdrive/dreaming/__init__.py index b7c367c..4eb54f5 100644 --- a/src/agentdrive/dreaming/__init__.py +++ b/src/agentdrive/dreaming/__init__.py @@ -10,6 +10,15 @@ from __future__ import annotations from agentdrive.dreaming.candidate import CandidateSignal, DreamCandidate +from agentdrive.dreaming.cycle import ( + DREAM_PHASES, + DreamCycleLockError, + DreamCyclePending, + DreamCycleResult, + DreamPhaseSpec, + get_dream_cycle_status, + run_dream_cycle, +) from agentdrive.dreaming.dilation import DilationPolicy, SleepWindow from agentdrive.dreaming.durable import ( AGENTDRIVE_SWARM_ID, @@ -33,15 +42,6 @@ run_tranche3_auto_calibration_job, ) from agentdrive.dreaming.engine import DreamEngine, DreamEngineConfig -from agentdrive.dreaming.cycle import ( - DREAM_PHASES, - DreamCycleLockError, - DreamCyclePending, - DreamCycleResult, - DreamPhaseSpec, - get_dream_cycle_status, - run_dream_cycle, -) from agentdrive.dreaming.phases import AdversarialResult, DeepResult, LightResult, RemResult __all__ = [ diff --git a/src/agentdrive/dreaming/cycle.py b/src/agentdrive/dreaming/cycle.py index f2b9b0f..c3adae5 100644 --- a/src/agentdrive/dreaming/cycle.py +++ b/src/agentdrive/dreaming/cycle.py @@ -20,8 +20,8 @@ from agentdrive.constants import get_agentdrive_home from agentdrive.drive.drive import AgentDrive, get_default_drive -from agentdrive.registry import GenomeRegistry from agentdrive.reconciliation import ReconciliationRunner +from agentdrive.registry import GenomeRegistry logger = logging.getLogger(__name__) @@ -52,7 +52,10 @@ def _publish_dream_phase_event( stop_gate=stop_gate, run_id=run_id, detail=result.detail, - metadata={"source": "dreaming.cycle", "stabilization_wave": "stabilization-wave-20260531"}, + metadata={ + "source": "dreaming.cycle", + "stabilization_wave": "stabilization-wave-20260531", + }, ) ) except Exception: @@ -236,8 +239,7 @@ def _run_reconcile( report = runner.scan_once() detail = report.to_dict() msg = ( - f"Reconciliation: {len(report.new_genomes)} new, " - f"{len(report.updated_genomes)} updated" + f"Reconciliation: {len(report.new_genomes)} new, {len(report.updated_genomes)} updated" ) success = True return DreamCycleResult( @@ -460,8 +462,8 @@ def _run_purge_stale(*, dry_run: bool, swarm_id: str | None = None) -> DreamCycl "consolidate": lambda *, pool, registry, dry_run, swarm_id, state_path: _run_consolidate( dry_run=dry_run ), - "grade_confidence": lambda *, pool, registry, dry_run, swarm_id, state_path: _run_grade_confidence( - registry=registry + "grade_confidence": lambda *, pool, registry, dry_run, swarm_id, state_path: ( + _run_grade_confidence(registry=registry) ), "purge_stale": lambda *, pool, registry, dry_run, swarm_id, state_path: _run_purge_stale( dry_run=dry_run, swarm_id=swarm_id @@ -535,7 +537,9 @@ def run_dream_cycle( result.dry_run = dry_run result.duration_ms = result.duration_ms or int((time.monotonic() - t_phase) * 1000) results.append(result) - _append_audit(run_id=run_id, result=result, home=agent_home, extra={"run_complete": False}) + _append_audit( + run_id=run_id, result=result, home=agent_home, extra={"run_complete": False} + ) _publish_dream_phase_event(result, run_id=run_id, stop_gate=spec.stop_gate) if not result.success: @@ -603,4 +607,4 @@ def get_dream_cycle_status(*, home: Path | None = None) -> dict[str, Any]: "dream_lock_path", "get_dream_cycle_status", "run_dream_cycle", -] \ No newline at end of file +] diff --git a/src/agentdrive/dreaming/dilation.py b/src/agentdrive/dreaming/dilation.py index 7670e86..fd8b99e 100644 --- a/src/agentdrive/dreaming/dilation.py +++ b/src/agentdrive/dreaming/dilation.py @@ -29,7 +29,10 @@ class DilationPolicy: ticks_per_simulated_hour: int = 60 wake_paths: list[Path] = field( default_factory=lambda: [ - Path("~/.agentdrive/pool/ingest.jsonl").expanduser(), + __import__( + "agentdrive.constants", fromlist=["get_default_drive_path"] + ).get_default_drive_path() + / "ingest.jsonl", Path("~/.agentdrive/swarms").expanduser(), Path("~/.agentdrive/reasoning/ledger").expanduser(), ] diff --git a/src/agentdrive/dreaming/durable.py b/src/agentdrive/dreaming/durable.py index c3967df..dd537ba 100644 --- a/src/agentdrive/dreaming/durable.py +++ b/src/agentdrive/dreaming/durable.py @@ -281,7 +281,14 @@ def run_phase(self, job_id: str, runner_callable): extra={"correlation_id": _cid, "job_id": job_id}, ) # Wave2: dream/durable phases (incl. daily_consolidation) emit for free via mission publish - if job.phase in ("daily_consolidation", "consol-deep", "consol-light", "consol-rem", "role-calibration", "healing"): + if job.phase in ( + "daily_consolidation", + "consol-deep", + "consol-light", + "consol-rem", + "role-calibration", + "healing", + ): _publish_mission_event( "loop_step", cycle_id=job.metadata.get("correlation_id") or _cid, @@ -289,7 +296,10 @@ def run_phase(self, job_id: str, runner_callable): step=5 if "consol" in job.phase or job.phase == "daily_consolidation" else 6, description=f"Durable dream phase completed: {job.phase}", data={"job_id": job_id, "swarm": self.swarm_id, "has_result": bool(result)}, - metadata={"job_phase": job.phase, "stabilization_wave": "stabilization-wave-20260531"}, + metadata={ + "job_phase": job.phase, + "stabilization_wave": "stabilization-wave-20260531", + }, ) return result except Exception as e: @@ -1261,7 +1271,10 @@ def run_consolidation_deep_phase(adv_result: dict | None = None) -> dict[str, An cycle_id=f"consol-deep-{now[:10]}", correlation_id=get_correlation_id(), summary="role-family deep consolidation (graph + salience + dream_jobs) feeding daily_consolidation", - graph_delta={"top_salience": top_salience[:3] if "top_salience" in locals() else [], "total_edges": total_edges if "total_edges" in locals() else 0}, + graph_delta={ + "top_salience": top_salience[:3] if "top_salience" in locals() else [], + "total_edges": total_edges if "total_edges" in locals() else 0, + }, ) return { @@ -1665,7 +1678,11 @@ def run_daily_consolidation_job() -> dict[str, Any]: correlation_id=_cid, step=6, description="Daily consolidation Drive.think(prefer_experience_layer) + family synthesis complete", - data={"gaps": len(gaps), "contradictions": len(contradictions), "citations": len(citations)}, + data={ + "gaps": len(gaps), + "contradictions": len(contradictions), + "citations": len(citations), + }, ) # 2. Graph signals + calibration snapshot for fusion_checkpoint (hybrid fusion + graph signals) @@ -1714,10 +1731,16 @@ def run_daily_consolidation_job() -> dict[str, Any]: try: if get_recorder_for_drive is not None: # Instantiate recorder for the swarm drive (explicit stabilization-wave-20260531 target) - stab_drive_path = Path.home() / ".agentdrive" / "swarms" / "stabilization-wave-20260531" / "drive" - recorder = get_recorder_for_drive(stab_drive_path, swarm_id="stabilization-wave-20260531") + stab_drive_path = ( + Path.home() / ".agentdrive" / "swarms" / "stabilization-wave-20260531" / "drive" + ) + recorder = get_recorder_for_drive( + stab_drive_path, swarm_id="stabilization-wave-20260531" + ) # Call the live v2 method (and new fabric briefing once implemented) - recent_densified = recorder.get_recent_densified_loop_graphs_for_diary(n=3, min_lift=0.005) or [] + recent_densified = ( + recorder.get_recent_densified_loop_graphs_for_diary(n=3, min_lift=0.005) or [] + ) get_fabric_brief = getattr(recorder, "get_parent_facing_memory_fabric_briefing", None) if callable(get_fabric_brief): try: @@ -1732,8 +1755,12 @@ def run_daily_consolidation_job() -> dict[str, Any]: "cycles_analyzed": n, "multi_cycle_coherence": round( sum(float(d.get("coherence", 0.0)) for d in recent_densified) / max(1, n), 4 - ) if n else 0.0, - "total_densification_lift": round(sum(float(d.get("lift", 0.0)) for d in recent_densified), 4), + ) + if n + else 0.0, + "total_densification_lift": round( + sum(float(d.get("lift", 0.0)) for d in recent_densified), 4 + ), "cycles": [d.get("cycle_id") for d in recent_densified], "note": "Fabric briefing synthesized from get_recent_densified_loop_graphs_for_diary + densif history", } @@ -1746,7 +1773,9 @@ def run_daily_consolidation_job() -> dict[str, Any]: "coherence_score": first.get("coherence"), "coherence": first.get("coherence"), "lift": first.get("lift"), - "densification_history": [{"lift": first.get("lift", 0)}] if first.get("lift") else [], + "densification_history": [{"lift": first.get("lift", 0)}] + if first.get("lift") + else [], } _ = embed_graph_into_artifact( cycle_graph_dict=gdict, @@ -1770,12 +1799,18 @@ def run_daily_consolidation_job() -> dict[str, Any]: for d in recent_densified: cid = d.get("cycle_id", "unknown") sec_lines.append(f"### Cycle {cid}") - sec_lines.append(f"coherence={d.get('coherence')} | lift={d.get('lift')} | new_edges={d.get('new_edges')}") - sec_lines.append("Mermaid (from live recorder.render_cycle_graph_mermaid via get_recent...):") + sec_lines.append( + f"coherence={d.get('coherence')} | lift={d.get('lift')} | new_edges={d.get('new_edges')}" + ) + sec_lines.append( + "Mermaid (from live recorder.render_cycle_graph_mermaid via get_recent...):" + ) sec_lines.append("```mermaid") sec_lines.append(d.get("mermaid_snippet") or "(no mermaid snippet)") sec_lines.append("```") - sec_lines.append("Hierarchical Text (from live recorder.render_cycle_graph_text):") + sec_lines.append( + "Hierarchical Text (from live recorder.render_cycle_graph_text):" + ) sec_lines.append("```") sec_lines.append(d.get("text_snippet") or "(no text snippet)") sec_lines.append("```") @@ -1792,17 +1827,32 @@ def run_daily_consolidation_job() -> dict[str, Any]: # Wave2: emit FabricUpdateEvent with before/after coherence + deltas from v3 daily fusion # (targeting stabilization-wave-20260531 drive + recorder surfaces; carries cycle_ids) try: - pre_coh = round( - sum(float(d.get("coherence", 0.0)) for d in recent_densified) / max(1, len(recent_densified)), 4 - ) if recent_densified else 0.0 - post_coh = float(fabric_briefing.get("fabric_coherence", fabric_briefing.get("multi_cycle_coherence", pre_coh + 0.01))) + pre_coh = ( + round( + sum(float(d.get("coherence", 0.0)) for d in recent_densified) + / max(1, len(recent_densified)), + 4, + ) + if recent_densified + else 0.0 + ) + post_coh = float( + fabric_briefing.get( + "fabric_coherence", + fabric_briefing.get("multi_cycle_coherence", pre_coh + 0.01), + ) + ) _publish_mission_event( "fabric_update", cycle_id=_daily_cycle, correlation_id=_cid, fabric_coherence=post_coh, - delta_edges=int(densified_graph_fusion.get("total_lift", 0) * 10) if "densified_graph_fusion" in locals() else len(recent_densified) * 2, - affected_cycles=[d.get("cycle_id") for d in recent_densified if d.get("cycle_id")], + delta_edges=int(densified_graph_fusion.get("total_lift", 0) * 10) + if "densified_graph_fusion" in locals() + else len(recent_densified) * 2, + affected_cycles=[ + d.get("cycle_id") for d in recent_densified if d.get("cycle_id") + ], summary="v3 daily_consolidation fabric fusion (GraphGardener densified + briefing injected into daily-present)", graph_delta={ "coherence_before": pre_coh, @@ -1821,16 +1871,23 @@ def run_daily_consolidation_job() -> dict[str, Any]: "cycles": [d.get("cycle_id") for d in recent_densified], "lifts": [d.get("lift") for d in recent_densified], "total_lift": round(sum(float(d.get("lift", 0.0)) for d in recent_densified), 4), - "renders_embedded_via_embed_graph_into_artifact": bool(recent_densified and embed_graph_into_artifact), + "renders_embedded_via_embed_graph_into_artifact": bool( + recent_densified and embed_graph_into_artifact + ), "recorder_target": "stabilization-wave-20260531/drive/meta_evolution/loops", "fabric_briefing": fabric_briefing, "graphgardener_v3": True, "fusion_method": "daily_consolidation + recorder.get_recent_densified... + embed + fabric_briefing", } else: - densified_graph_fusion = {"note": "recorder import unavailable; v3 fusion skipped (graceful)"} + densified_graph_fusion = { + "note": "recorder import unavailable; v3 fusion skipped (graceful)" + } except Exception as _e: - densified_graph_fusion = {"note": "v3 densified graph fusion graceful skip", "error": str(_e)[:140]} + densified_graph_fusion = { + "note": "v3 densified graph fusion graceful skip", + "error": str(_e)[:140], + } recent_densified = [] fabric_briefing = {} graph_section_for_diary = "" @@ -1851,7 +1908,9 @@ def run_daily_consolidation_job() -> dict[str, Any]: # v3 GraphGardener densified + fabric enrichment (daily_consolidation automatic fusion) "densified_graph_fusion": densified_graph_fusion, "fabric_briefing": fabric_briefing, - "experience_graph_v3_injected": bool(densified_graph_fusion.get("renders_embedded_via_embed_graph_into_artifact")), + "experience_graph_v3_injected": bool( + densified_graph_fusion.get("renders_embedded_via_embed_graph_into_artifact") + ), } daily_present_payload = { @@ -2005,7 +2064,10 @@ def run_daily_consolidation_job() -> dict[str, Any]: delta_edges=8, affected_cycles=[_daily_cycle], summary="post daily_consolidation (stabilization-wave-20260531): daily-present fused to experience layer v3", - graph_delta={"post_consolidation_coherence": final_coh, "harness_decision": str(getattr(daily_scores, "decision", ""))}, + graph_delta={ + "post_consolidation_coherence": final_coh, + "harness_decision": str(getattr(daily_scores, "decision", "")), + }, ) except Exception: pass diff --git a/src/agentdrive/dreaming/ingestion.py b/src/agentdrive/dreaming/ingestion.py index 52f9744..22f257d 100644 --- a/src/agentdrive/dreaming/ingestion.py +++ b/src/agentdrive/dreaming/ingestion.py @@ -36,10 +36,17 @@ class IngestionConfig: default_factory=lambda: Path("~/.agentdrive/reasoning/ledger").expanduser() ) pool_ingest_path: Path = field( - default_factory=lambda: Path("~/.agentdrive/pool/ingest.jsonl").expanduser() + default_factory=lambda: ( + __import__( + "agentdrive.constants", fromlist=["get_default_drive_path"] + ).get_default_drive_path() + / "ingest.jsonl" + ) ) pool_genomes_root: Path = field( - default_factory=lambda: Path("~/.agentdrive/pool/genomes").expanduser() + default_factory=lambda: __import__( + "agentdrive.constants", fromlist=["get_genomes_dir"] + ).get_genomes_dir() ) peers_root: Path = field(default_factory=lambda: Path("~/.agentdrive/peers").expanduser()) since_seconds: int = 86_400 diff --git a/src/agentdrive/drive/architecture.md b/src/agentdrive/drive/architecture.md index 53c7af8..b39a831 100644 --- a/src/agentdrive/drive/architecture.md +++ b/src/agentdrive/drive/architecture.md @@ -32,9 +32,9 @@ Safe by default, explicitly relaxable by the user. ### 2. SwarmDriveManager (the active subagents registry) Located in `swarm_manager.py`. -- Thread-safe registry of all active `(swarm_id, subagent_id)` → their private `AgentDrive` -- Automatic provisioning of isolated directories under `~/.agentdrive/swarms///pool/` -- `get_or_create_pool(...)` — the method that should be called when spawning any sub-agent +- Thread-safe registry of swarm members → shared `AgentDrive` per `swarm_id` +- Automatic provisioning at `~/.agentdrive/swarms//drive/` (v2 shared Drive) +- `get_or_create_pool(...)` — call when spawning any sub-agent; siblings share one instance - `propose_dna_merge(...)` for controlled sharing - Pause / resume capability at the swarm level diff --git a/src/agentdrive/drive/drive.py b/src/agentdrive/drive/drive.py index 42090c2..2d6ccfd 100644 --- a/src/agentdrive/drive/drive.py +++ b/src/agentdrive/drive/drive.py @@ -168,7 +168,7 @@ class AgentDrive: Supports full per-swarm and per-subagent isolation: - When swarm_id or subagent_id provided (or via current context / AGENTDRIVE_*_ID env), uses - ~/.agentdrive/swarms///pool/ (starts empty: own genomes/ + ingest) + ~/.agentdrive/swarms//drive/ (shared per-swarm Drive; sub-agents tag writes via author field) - Parent/child sharing governed by user's DriveSettings.sharing_policy (none/read-only/selective/full) - Automatic provisioning via SwarmDriveManager + get_default_drive() @@ -854,9 +854,7 @@ def _apply_additive_fusion( elif "schema" in pt: pt_boost = 0.07 dream_b = ( - 0.09 - if "dream" in pt or "observation" in str(getattr(g, "_page_type", "")) - else 0.0 + 0.09 if "dream" in pt or "observation" in str(getattr(g, "_page_type", "")) else 0.0 ) experience_b = ( 0.22 @@ -2010,78 +2008,6 @@ def __exit__(self, exc_type, exc_val, exc_tb): # Global default pool (easy for agents to use) -- the root/parent pool default_pool: AgentDrive | None = None -# Cache for scoped child pools (keyed by (swarm, subagent)) -_scoped_pools: dict[tuple[str, str], AgentDrive] = {} - -# Singleton manager -_swarm_pool_manager: SwarmDriveManager | None = None - - -class SwarmDriveManager: - """ - Provisions and manages isolated per-swarm / per-subagent AgentDrives. - - Automatically creates the directory structure: - ~/.agentdrive/swarms///pool/ - ├── genomes/ (child's private DNA, starts empty) - └── ingest.jsonl - - When any spawner (Grok's spawn_subagent, Claude, etc.) sets - AGENTDRIVE_SWARM_ID + AGENTDRIVE_SUBAGENT_ID (or uses using_swarm() context), - get_default_drive() + Harness automatically give the child its own pool. - - Sharing policies (from user settings) are honored for parent<->child visibility. - """ - - def __init__(self): - self._cache: dict[tuple[str, str], AgentDrive] = {} - - def provision(self, swarm_id: str, subagent_id: str | None = None) -> Path: - """Create the isolated dir tree (idempotent). Returns the Drive/ path. - - Now also ensures objects/ + experience layer seed for empty-drive - self-healing parity with global Drive initialization. - """ - p = get_swarm_drive_path(swarm_id, subagent_id) - try: - p.mkdir(parents=True, exist_ok=True) - for sub in ("genomes", "objects"): - (p / sub).mkdir(exist_ok=True) - # Touch ingest log defensively (AgentDrive will also do it) - ingest = p / "ingest.jsonl" - if not ingest.exists(): - ingest.touch(exist_ok=True) - except Exception: - pass - # The subsequent AgentDrive() ctor will run the full _ensure_experience_layer_seed - # + home ensure + recovery logic. We keep provision minimal but safe. - return p - - def get_pool(self, swarm_id: str, subagent_id: str | None = None, **kwargs: Any) -> AgentDrive: - """Return the child's private pool (creates + wires parent for sharing if new).""" - key = (swarm_id or "default", subagent_id or "") - if key in self._cache: - return self._cache[key] - self.provision(swarm_id, subagent_id) - child = AgentDrive( - swarm_id=swarm_id, - subagent_id=subagent_id, - name=f"swarm:{swarm_id}/sub:{subagent_id or 'anon'}", - **kwargs, - ) - # Wire to global root parent so sharing policies work (query + full-ingest) - child.parent_pool = get_global_drive() - self._cache[key] = child - _scoped_pools[key] = child # also global cache - return child - - -def get_swarm_drive_manager() -> SwarmDriveManager: - global _swarm_pool_manager - if _swarm_pool_manager is None: - _swarm_pool_manager = SwarmDriveManager() - return _swarm_pool_manager - def get_global_drive() -> AgentDrive: """Always returns the root (non-scoped) pool, regardless of current context/env ids. @@ -2120,7 +2046,12 @@ def get_default_drive() -> AgentDrive: subagent_id = get_current_subagent_id() if swarm_id is not None or subagent_id is not None: - return get_swarm_drive_manager().get_pool(swarm_id or "default", subagent_id) + from agentdrive.drive.swarm_manager import get_swarm_drive_manager + + return get_swarm_drive_manager().get_or_create_pool( + swarm_id or "default", + subagent_id, + ) return get_global_drive() diff --git a/src/agentdrive/drive/retrieval.py b/src/agentdrive/drive/retrieval.py index 025daf7..45fe14f 100644 --- a/src/agentdrive/drive/retrieval.py +++ b/src/agentdrive/drive/retrieval.py @@ -119,10 +119,13 @@ def fuse_scored_with_rrf( "reasoning": rel.get("reasoning"), "page_type": page_types.get(gid, ""), "graph_signal": graph_signals.get(gid, {}).get("gbrain_signal_score"), - "rankings": {name: ids.index(gid) + 1 if gid in ids else None for name, ids in rankings.items()}, + "rankings": { + name: ids.index(gid) + 1 if gid in ids else None + for name, ids in rankings.items() + }, }, ) except Exception: pass fused.append((rrf, g)) - return fused \ No newline at end of file + return fused diff --git a/src/agentdrive/eval/__init__.py b/src/agentdrive/eval/__init__.py index 79462e8..281c25c 100644 --- a/src/agentdrive/eval/__init__.py +++ b/src/agentdrive/eval/__init__.py @@ -2,4 +2,4 @@ from agentdrive.eval.replay import replay_artifact_scores, replay_genome_artifact_file -__all__ = ["replay_artifact_scores", "replay_genome_artifact_file"] \ No newline at end of file +__all__ = ["replay_artifact_scores", "replay_genome_artifact_file"] diff --git a/src/agentdrive/eval/replay.py b/src/agentdrive/eval/replay.py index 61ebec6..1af19cf 100644 --- a/src/agentdrive/eval/replay.py +++ b/src/agentdrive/eval/replay.py @@ -24,7 +24,9 @@ def _scores_from_artifact(data: dict[str, Any]) -> dict[str, Any] | None: if isinstance(kdo, dict) and kdo.get("decision"): return { "decision": kdo.get("decision"), - "overall_goodness": (ev or {}).get("overall_goodness") if isinstance(ev, dict) else None, + "overall_goodness": (ev or {}).get("overall_goodness") + if isinstance(ev, dict) + else None, } manifest = data.get("manifest") or {} if isinstance(manifest, dict): @@ -64,7 +66,9 @@ def _artifact_to_after_state(data: dict[str, Any]) -> dict[str, Any]: "proposals_executed": [1], "citation_count": 4, "experience_layer_v3_seed_referenced": True, - "feeds_experience_layer": bool(fw.get("high_signal", True)) if isinstance(fw, dict) else True, + "feeds_experience_layer": bool(fw.get("high_signal", True)) + if isinstance(fw, dict) + else True, "contradictions_addressed": contradictions_addressed, } @@ -126,4 +130,4 @@ def replay_genome_artifact_file(path: Path | str, *, tolerance: float = 0.05) -> data = json.loads(p.read_text(encoding="utf-8")) result = replay_artifact_scores(data, tolerance=tolerance) result["path"] = str(p) - return result \ No newline at end of file + return result diff --git a/src/agentdrive/events.py b/src/agentdrive/events.py index 511bb61..8469200 100644 --- a/src/agentdrive/events.py +++ b/src/agentdrive/events.py @@ -130,6 +130,8 @@ class InheritanceReceived(Event): genomes_absorbed: list[str] = field(default_factory=list) genomes_rejected: list[str] = field(default_factory=list) + skills_absorbed: list[str] = field(default_factory=list) + skills_rejected: list[str] = field(default_factory=list) @dataclass @@ -137,10 +139,21 @@ class InheritanceAbsorbed(Event): """A single foreign genome was absorbed via an inheritance manifest.""" genome_id: str = "" + skill_name: str = "" source_subagent_id: str = "" parent_pool: str = "" +@dataclass +class SkillAssimilated(Event): + """A gated inherited-skill assimilation pass promoted parent knowledge.""" + + promoted_skills: list[str] = field(default_factory=list) + dna_genomes: list[str] = field(default_factory=list) + pruned_skills: list[str] = field(default_factory=list) + errors: list[dict[str, str]] = field(default_factory=list) + + @dataclass class SubagentSpawn(Event): parent_id: str = "" @@ -473,6 +486,7 @@ def unsubscribe(token: SubscriptionToken) -> None: "ConfidenceUpdated", "InheritanceReceived", "InheritanceAbsorbed", + "SkillAssimilated", "SubagentSpawn", "SubagentTool", "SubagentTokens", diff --git a/src/agentdrive/evolution/experience_graph.py b/src/agentdrive/evolution/experience_graph.py index 20aa6f4..9ff48f2 100644 --- a/src/agentdrive/evolution/experience_graph.py +++ b/src/agentdrive/evolution/experience_graph.py @@ -44,6 +44,7 @@ from typing import Any from agentdrive.knowledge_graph.link_extraction import TypedEdge +from agentdrive.memory import MemoryTraceCandidate, triage_memory_candidates from agentdrive.utils.safe_paths import PathTraversalError, safe_join try: @@ -1394,10 +1395,17 @@ def get_fabric_context_pack( "top_weak_clusters": [], "strong_continuations": [], "recent_high_value_densifications": [], + "memory_systems_triage": {}, "actionable_structural_recommendations": [], "compact_graph_summary": f"{agg.get('cycle_count', 0)} cycles, {agg.get('cross_cycle_edge_count', 0)} cross edges, coh={fab_coh}, style={style}", } + def _floatish(value: Any, default: float) -> float: + try: + return float(value) + except (TypeError, ValueError): + return default + # Style-tuned population of the structural pack (deepened for Parent reasoning power) include_weaks = style in ("balanced", "weak_links_focus") include_conts = style in ("balanced", "continuations_only", "structural_analogies") @@ -1461,11 +1469,97 @@ def get_fabric_context_pack( "Call find_structural_similarities on key fabric elements for cross-element pattern matches with full traces" ) + # Trim for token budget before memory triage so queues reference the final pack contents. + if max_tokens < 1200: + pack["strong_continuations"] = pack["strong_continuations"][:2] + pack["recent_high_value_densifications"] = pack["recent_high_value_densifications"][:1] + + memory_candidates: list[MemoryTraceCandidate] = [] + for item in pack["top_weak_clusters"]: + coherence = _floatish(item.get("coherence"), 0.5) + signal = _floatish(item.get("gbrain_signal_score"), 0.65) + cycle_id = item.get("cycle_id") or "unknown" + memory_candidates.append( + MemoryTraceCandidate( + item_id=f"weak_cluster:{cycle_id}", + source="experience_graph.top_weak_clusters", + memory_kind="episodic", + salience=signal, + retrieval_relevance=0.82, + coherence=coherence, + trust=0.72, + novelty=min(1.0, 0.45 + (1.0 - coherence) * 0.35), + contradiction_pressure=max(0.0, 1.0 - coherence), + consolidation_depth=0.2, + metadata={ + "cycle_id": cycle_id, + "edge_count": item.get("edge_count"), + "artifact_count": item.get("artifact_count"), + }, + ) + ) + + for item in pack["strong_continuations"]: + source = item.get("source") or "unknown" + relation = item.get("relation") or "continues" + target = item.get("target") or "unknown" + memory_candidates.append( + MemoryTraceCandidate( + item_id=f"continuation:{source}->{relation}->{target}", + source="experience_graph.strong_continuations", + memory_kind="semantic", + rehearsal_count=2, + salience=_floatish(item.get("gbrain_signal_score"), 0.78), + retrieval_relevance=0.72, + coherence=0.86, + trust=0.86, + novelty=0.25, + consolidation_depth=0.75, + metadata={ + "source": source, + "target": target, + "relation": relation, + "provenance": item.get("provenance"), + }, + ) + ) + + for item in pack["recent_high_value_densifications"]: + lift = _floatish(item.get("lift"), 0.0) + coherence_after = _floatish(item.get("coherence_after"), 0.82) + cycle_id = item.get("cycle") or "unknown" + memory_candidates.append( + MemoryTraceCandidate( + item_id=f"densification:{cycle_id}", + source="experience_graph.recent_high_value_densifications", + memory_kind="procedural", + salience=_floatish(item.get("gbrain_signal_score"), 0.74), + retrieval_relevance=0.76, + coherence=coherence_after, + trust=0.82, + novelty=min(1.0, 0.35 + abs(lift) * 3.0), + consolidation_depth=0.45, + metadata={ + "cycle_id": cycle_id, + "lift": item.get("lift"), + "coherence_before": item.get("coherence_before"), + "coherence_after": item.get("coherence_after"), + "key_edges": item.get("key_edges"), + }, + ) + ) + + pack["memory_systems_triage"] = triage_memory_candidates( + memory_candidates, per_route_limit=3 + ) + # Pre-computed actionable structural steers (Parent can accept or refine) — style aware base_recs = [ "Prioritize densification on lowest-coh cross-cycle clusters (use record_parent_fabric_reasoning to declare exactly which edges)", "Extend proven strong continuations (research-constitutions patterns have shown +0.03–0.05 coherence lift)", "Record explicit fabric reasoning trace when deciding — this becomes queryable DNA for future Parent decisions (use get_fabric_reasoning_traces_for_element + get_parent_reasoning_history)", + "Use memory_systems_triage: keep working_set items in scarce context, reconcile " + "reconsolidate items before treating them as precedent, and consolidate durable patterns into DNA", ] if style == "high_lift_patterns_only": pack["actionable_structural_recommendations"] = [ @@ -1480,11 +1574,6 @@ def get_fabric_context_pack( else: pack["actionable_structural_recommendations"] = base_recs - # Trim for token budget (crude but effective) - if max_tokens < 1200: - pack["strong_continuations"] = pack["strong_continuations"][:2] - pack["recent_high_value_densifications"] = pack["recent_high_value_densifications"][:1] - # Always emit via publish_event_sync (recorder as clean point) + KG TypedEdge with gbrain provenance try: self._emit_loop_or_fabric_event( @@ -1560,13 +1649,19 @@ def record_parent_fabric_reasoning( # This directly closes the echo risk surfaced when the PerfectionistOptimizer + forced passes + subagent # synthesis re-recorded the same "Council has now spoken..." narrative ~50 times in 4-5min. try: - core = (reasoning.get("structural_pattern_matched") or "")[:220] + "|" + (reasoning.get("decision_rationale") or "")[:350] + core = ( + (reasoning.get("structural_pattern_matched") or "")[:220] + + "|" + + (reasoning.get("decision_rationale") or "")[:350] + ) h = str(hash(core)) nowt = time.time() for k in list(self._recent_fabric_reasoning_hashes.keys()): if nowt - self._recent_fabric_reasoning_hashes[k] > 180: del self._recent_fabric_reasoning_hashes[k] - if h in self._recent_fabric_reasoning_hashes and (nowt - self._recent_fabric_reasoning_hashes[h] < 45): + if h in self._recent_fabric_reasoning_hashes and ( + nowt - self._recent_fabric_reasoning_hashes[h] < 45 + ): reasoning["dupe_suppressed"] = True reasoning["suppressed_window_s"] = 45 sup_slug = f"parent_fabric_reasoning_dupe_suppressed:{int(nowt)}" @@ -1574,7 +1669,10 @@ def record_parent_fabric_reasoning( cycle_id, sup_slug, "parent_fabric_reasoning_dupe_suppressed", - content_ref={"h": h[:16], "structural_pattern": reasoning.get("structural_pattern_matched", "")[:120]}, + content_ref={ + "h": h[:16], + "structural_pattern": reasoning.get("structural_pattern_matched", "")[:120], + }, texture_hints={"suppression": True, "gbrain_signal_score": 0.01}, ) self._recent_fabric_reasoning_hashes[h] = nowt @@ -1646,7 +1744,10 @@ def record_parent_fabric_reasoning( # Write first-class page_type observation for the trace (queryable DNA + living memory) try: self._write_parent_fabric_reasoning_trace_observation( - cycle_id, slug, reasoning, gbrain, + cycle_id, + slug, + reasoning, + gbrain, program_id=prog_id, user_objective_refs=user_objectives, program_mandate_ref=prog_mandate, @@ -2478,11 +2579,21 @@ def normalize_fabric_reasoning(self, reasoning: dict[str, Any] | None) -> dict[s # AD-Grid Programs as Inhabitants (minimal tranche on stabilization-wave-20260531) # Programs (local models or frontier sessions) declare identity + mandate when recording reasoning. # These become first-class on the trace observation + TypedEdges + gbrain provenance. - "program_id": str(reasoning.get("program_id") or reasoning.get("program") or "") or None, + "program_id": str(reasoning.get("program_id") or reasoning.get("program") or "") + or None, "user_objective_refs": [ - str(x) for x in (reasoning.get("user_objective_refs", []) or reasoning.get("objectives", []) or []) if x + str(x) + for x in ( + reasoning.get("user_objective_refs", []) + or reasoning.get("objectives", []) + or [] + ) + if x ][:8], - "program_mandate_ref": str(reasoning.get("program_mandate_ref") or reasoning.get("mandate") or "") or None, + "program_mandate_ref": str( + reasoning.get("program_mandate_ref") or reasoning.get("mandate") or "" + ) + or None, "constitution_refs": [ str(x) for x in (reasoning.get("constitution_refs", []) or []) if x ][:5], @@ -2570,6 +2681,18 @@ def suggest_fabric_reasoning_structure(self) -> dict[str, Any]: "fabric_reasoning_prompt_template": template, "few_shot_good_traces": few_shot, "usage_in_parent_flow": "1. Call get_parent_actionable_briefing() 2. (optionally) call this suggest_fabric... 3. Reason over fabric_context_pack + few-shots 4. Call record_parent_decision(..., fabric_reasoning=populated_dict) --> richer parent_fabric_reasoning_informed_decision TypedEdge + element links + normalized trace artifact created automatically (still inside 6-step).", + "external_mcp_parent_flow": ( + "When YOU are the connected model (Grok/Claude/Codex via MCP) and no local LLM is configured: " + "1) experience_graph_get_context_pack 2) this suggest tool 3) reason across multiverse roles " + "in your session 4) external_parent_decision(trigger, branches, collapsed_branch_id, " + "fabric_reasoning=...) OR experience_graph_record_reasoning for lighter decisions. " + "Sets llm_mode=external on the persisted session." + ), + "reasoning_provider_modes": { + "local_llm": "multiverse_parent_decision when ~/.agentdrive/local_models.yaml backend is reachable", + "heuristic": "multiverse_parent_decision fallback when no local model (template branches only)", + "external_mcp": "external_parent_decision — frontier/chat MCP client is the Parent reasoner", + }, "live_traces_available": len(live_examples) > 0, } @@ -2704,7 +2827,9 @@ def _write_parent_fabric_reasoning_trace_observation( "program_id": program_id, "user_objectives": user_objective_refs or [], "mandate": program_mandate_ref, - } if program_id else None, + } + if program_id + else None, "self_referential": ( "First-class capture of Parent graph-native reasoning over the v3 multi-cycle memory fabric. " "Produced exclusively by record_parent_fabric_reasoning. " @@ -2786,11 +2911,15 @@ def record_model_program_manifest( """ if not isinstance(manifest, dict): manifest = {} - program_id = str(manifest.get("program_id") or manifest.get("id") or f"prog-{int(time.time())}") + program_id = str( + manifest.get("program_id") or manifest.get("id") or f"prog-{int(time.time())}" + ) user_objectives = manifest.get("user_objective_refs") or manifest.get("objectives") or [] if not program_id or not user_objectives: # Basic UserSovereigntyClause: programs must declare explicit tie to user's objectives - manifest["_sovereignty_note"] = "REJECTED: program_id + user_objective_refs required for AD-Grid inhabitant registration" + manifest["_sovereignty_note"] = ( + "REJECTED: program_id + user_objective_refs required for AD-Grid inhabitant registration" + ) # Still record the attempt as experience (for audit / GuardianIntegrity) program_id = program_id or f"rejected-prog-{int(time.time())}" @@ -2816,7 +2945,9 @@ def record_model_program_manifest( # Dedicated observation with explicit page_type try: - self._write_model_program_manifest_observation(cycle_id, slug, manifest, program_id, gbrain) + self._write_model_program_manifest_observation( + cycle_id, slug, manifest, program_id, gbrain + ) except Exception: pass @@ -2876,7 +3007,9 @@ def record_inhabitant_code_action( # Basic Guardian-style pre-check (real enforcement comes in the apply path + constitutions) sovereignty_ok = bool(user_objective_refs) or bool(constitution_refs) if not sovereignty_ok: - action["_guardian_note"] = "LOW_SOVEREIGNTY: recommend explicit user_objective_refs or constitution_refs for promotion" + action["_guardian_note"] = ( + "LOW_SOVEREIGNTY: recommend explicit user_objective_refs or constitution_refs for promotion" + ) # === Single-channel enforcement hook for high-volume paths (additive, safe) === # All high-signal / high-volume inhabitant actions (Council threads, auto research passes, @@ -2885,7 +3018,9 @@ def record_inhabitant_code_action( try: if not hasattr(self, "_high_volume_action_counts"): self._high_volume_action_counts: dict[str, int] = {} - self._high_volume_action_counts[program_id] = self._high_volume_action_counts.get(program_id, 0) + 1 + self._high_volume_action_counts[program_id] = ( + self._high_volume_action_counts.get(program_id, 0) + 1 + ) vol = self._high_volume_action_counts[program_id] if vol % 5 == 0 or vol > 20: # annotate on bursts or sustained high volume action["_single_channel_enforcement"] = { @@ -2997,17 +3132,27 @@ def guardian_verdict_gate( prog_id = ( program_id or (proposal.get("program_id") if isinstance(proposal, dict) else None) - or (proposal.get("action", {}).get("program_id") if isinstance(proposal, dict) else None) + or ( + proposal.get("action", {}).get("program_id") if isinstance(proposal, dict) else None + ) ) uo_refs = ( user_objective_refs or (proposal.get("user_objective_refs", []) if isinstance(proposal, dict) else []) - or (proposal.get("action", {}).get("user_objective_refs", []) if isinstance(proposal, dict) else []) + or ( + proposal.get("action", {}).get("user_objective_refs", []) + if isinstance(proposal, dict) + else [] + ) ) const_refs = ( constitution_refs or (proposal.get("constitution_refs", []) if isinstance(proposal, dict) else []) - or (proposal.get("action", {}).get("constitution_refs", []) if isinstance(proposal, dict) else []) + or ( + proposal.get("action", {}).get("constitution_refs", []) + if isinstance(proposal, dict) + else [] + ) ) action = ( proposal.get("action", proposal) @@ -3020,9 +3165,13 @@ def guardian_verdict_gate( # Criterion 1: program_id + user_objective_refs (core of UserSovereigntyClause) if not prog_id or str(prog_id).strip() in ("", "unknown-inhabitant", "unknown"): - issues.append("MISSING_PROGRAM_ID: program_id mandatory for attributed inhabitant DNA and sovereignty tracking") + issues.append( + "MISSING_PROGRAM_ID: program_id mandatory for attributed inhabitant DNA and sovereignty tracking" + ) if not uo_refs or len([r for r in (uo_refs or []) if str(r).strip()]) == 0: - issues.append("MISSING_USER_OBJECTIVE_REFS: >=1 explicit user_objective_ref required (UserSovereigntyClause + Guardian constitution)") + issues.append( + "MISSING_USER_OBJECTIVE_REFS: >=1 explicit user_objective_ref required (UserSovereigntyClause + Guardian constitution)" + ) # Criterion 2: no erosion of Conductor authority (strengthened with real override path) # Explicit "conductor_override": true + non-empty "conductor_signature" (or equivalent) permits @@ -3042,7 +3191,14 @@ def guardian_verdict_gate( erosion_keywords_present = any( k in action_str_lower - for k in ("conductor_override", "auto_promote_without_user", "bypass_guardian", "silent_apply", "force_conductor", "user_not_needed") + for k in ( + "conductor_override", + "auto_promote_without_user", + "bypass_guardian", + "silent_apply", + "force_conductor", + "user_not_needed", + ) ) if erosion_keywords_present: @@ -3052,57 +3208,125 @@ def guardian_verdict_gate( "CONDUCTOR_OVERRIDE_ACCEPTED: explicit bypass with signature; full DNA audit trail recorded per Program Contract + Guardian enforcement (no sovereignty erosion)" ) else: - issues.append("CONDUCTOR_EROSION_RISK: proposal claims or implies override/bypass of human Conductor final authority (explicitly forbidden unless conductor_override:true + conductor_signature)") + issues.append( + "CONDUCTOR_EROSION_RISK: proposal claims or implies override/bypass of human Conductor final authority (explicitly forbidden unless conductor_override:true + conductor_signature)" + ) # Criterion 3: references to active constitutions (GuardianIntegrity primary for this gate) - active = {"guardian-integrity", "guardian", "research-constitution-guardian-integrity", "perfectionist-optimizer", "external-bridge"} + active = { + "guardian-integrity", + "guardian", + "research-constitution-guardian-integrity", + "perfectionist-optimizer", + "external-bridge", + } has_const_ref = bool(const_refs) and any( any(a in str(cr).lower() for a in active) for cr in (const_refs or []) ) if not has_const_ref: - issues.append("WEAK_CONSTITUTION_REFS: must reference at least one active Council constitution (GuardianIntegrity strongly recommended for code actions)") + issues.append( + "WEAK_CONSTITUTION_REFS: must reference at least one active Council constitution (GuardianIntegrity strongly recommended for code actions)" + ) # Criterion 4: basic drift/sanity checks (prevent low-signal or dangerous proposals from 5min echo) if action_type in ("code_change_applied", "code_edit", "apply", "patch") and target_path: tp = str(target_path) - if ".." in tp or tp.startswith(("/", "\\")) or any(bad in tp for bad in ("__pycache__", ".git", "site-packages", "node_modules")): - issues.append("UNSAFE_TARGET_PATH: path escapes allowed safe roots or targets protected/generated dirs (use safe_join guard)") - content_len = len(str(action.get("content") or action.get("patch") or action.get("new_content") or "")) + if ( + ".." in tp + or tp.startswith(("/", "\\")) + or any( + bad in tp for bad in ("__pycache__", ".git", "site-packages", "node_modules") + ) + ): + issues.append( + "UNSAFE_TARGET_PATH: path escapes allowed safe roots or targets protected/generated dirs (use safe_join guard)" + ) + content_len = len( + str(action.get("content") or action.get("patch") or action.get("new_content") or "") + ) if content_len > 20000: - issues.append("EXCESSIVE_CHANGE_SIZE: v1 guarded apply limits patch/content to 20k chars to bound drift risk") - if any(dyn in str(action) for dyn in ("__import__", "exec(", "eval(", "os.system", "subprocess.call", "compile(")): - issues.append("SANITY_DRIFT: proposal contains dynamic execution patterns - requires explicit Conductor review beyond gate") + issues.append( + "EXCESSIVE_CHANGE_SIZE: v1 guarded apply limits patch/content to 20k chars to bound drift risk" + ) + if any( + dyn in str(action) + for dyn in ( + "__import__", + "exec(", + "eval(", + "os.system", + "subprocess.call", + "compile(", + ) + ): + issues.append( + "SANITY_DRIFT: proposal contains dynamic execution patterns - requires explicit Conductor review beyond gate" + ) # Criterion 5 (Inhabitants that Ship 1780296458): explicit Conductor approval REQUIRED for real source targets # This prevents any real contrib without going through the review queue + Conductor path. # Uses the same override+signature mechanism (or passed conductor_approval) for explicit approval. # Stricter bounds for real mode (5k chars, source exts only). - real_mode_active = bool(real_contribution_mode or allow_real_source_targets or (isinstance(proposal, dict) and proposal.get("real_contribution_mode"))) + real_mode_active = bool( + real_contribution_mode + or allow_real_source_targets + or (isinstance(proposal, dict) and proposal.get("real_contribution_mode")) + ) if real_mode_active: - ca = conductor_approval or (proposal.get("conductor_approval") if isinstance(proposal, dict) else None) or action + ca = ( + conductor_approval + or (proposal.get("conductor_approval") if isinstance(proposal, dict) else None) + or action + ) ca_override = bool(isinstance(ca, dict) and ca.get("conductor_override") is True) ca_sig = None if isinstance(ca, dict): ca_sig = ca.get("conductor_signature") or ca.get("signature") ca_sig = str(ca_sig).strip() if ca_sig else None if not (ca_override and ca_sig): - issues.append("REAL_CONTRIBUTION_REQUIRES_EXPLICIT_CONDUCTOR_APPROVAL: real_contribution_mode or allow_real_source_targets set but no valid conductor_approval (dict with conductor_override=True + conductor_signature). Use submit_inhabitant_proposal_for_review + conductor_approve_proposal to obtain. Conductor sovereignty absolute.") + issues.append( + "REAL_CONTRIBUTION_REQUIRES_EXPLICIT_CONDUCTOR_APPROVAL: real_contribution_mode or allow_real_source_targets set but no valid conductor_approval (dict with conductor_override=True + conductor_signature). Use submit_inhabitant_proposal_for_review + conductor_approve_proposal to obtain. Conductor sovereignty absolute." + ) # Stricter for real - rlen = len(str(action.get("content") or action.get("patch") or action.get("new_content") or "")) + rlen = len( + str(action.get("content") or action.get("patch") or action.get("new_content") or "") + ) if rlen > 5000: - issues.append("EXCESSIVE_REAL_CHANGE_SIZE: real contribution mode limits to 5k chars (tighter drift bound)") + issues.append( + "EXCESSIVE_REAL_CHANGE_SIZE: real contribution mode limits to 5k chars (tighter drift bound)" + ) tp = str(target_path or "") - if tp and not any(tp.endswith(ext) for ext in (".py", ".md", ".txt", ".rst", ".yaml", ".json")): - issues.append("REAL_TARGET_EXT_UNSUPPORTED: real source contrib limited to source/docs exts for safety") - if any(bad in tp for bad in ("__pycache__", ".git", "site-packages", "node_modules", "build", "dist", ".pyc")): - issues.append("REAL_TARGET_PROTECTED_DIR: even in real mode, protected/generated dirs forbidden") + if tp and not any( + tp.endswith(ext) for ext in (".py", ".md", ".txt", ".rst", ".yaml", ".json") + ): + issues.append( + "REAL_TARGET_EXT_UNSUPPORTED: real source contrib limited to source/docs exts for safety" + ) + if any( + bad in tp + for bad in ( + "__pycache__", + ".git", + "site-packages", + "node_modules", + "build", + "dist", + ".pyc", + ) + ): + issues.append( + "REAL_TARGET_PROTECTED_DIR: even in real mode, protected/generated dirs forbidden" + ) if issues: # If the ONLY issue is the accepted override note, treat as pass (override path) override_only = len(issues) == 1 and "CONDUCTOR_OVERRIDE_ACCEPTED" in issues[0] if override_only: verdict = "pass" - reason = "Guardian gate PASSED via explicit Conductor override with signature. Full audit DNA recorded. " + issues[0] + reason = ( + "Guardian gate PASSED via explicit Conductor override with signature. Full audit DNA recorded. " + + issues[0] + ) gbrain = 0.65 # lower than normal pass but higher than block; signals override use else: verdict = "block" @@ -3131,7 +3355,10 @@ def guardian_verdict_gate( # 1780296458 fields for real ship audit "real_contribution_mode": real_contribution_mode, "allow_real_source_targets": allow_real_source_targets, - "had_conductor_approval": bool(conductor_approval or (isinstance(proposal, dict) and proposal.get("conductor_approval"))), + "had_conductor_approval": bool( + conductor_approval + or (isinstance(proposal, dict) and proposal.get("conductor_approval")) + ), } return { @@ -3246,17 +3473,26 @@ def guarded_apply_inhabitant_action( "applied": False, "verification": None, "edit_details": None, - "logs": [f"[{program_id}] Guardian gate verdict: {verdict} :: {gate_result['reason'][:140]}"], + "logs": [ + f"[{program_id}] Guardian gate verdict: {verdict} :: {gate_result['reason'][:140]}" + ], "dna_traces": [verdict_slug] if verdict_slug else [], } if verdict != "pass": - result["logs"].append("Apply BLOCKED by Guardian per constitution. Proposal recorded for fabric/Parent review (no edit).") + result["logs"].append( + "Apply BLOCKED by Guardian per constitution. Proposal recorded for fabric/Parent review (no edit)." + ) # Record the blocked proposal for DNA (low gbrain but traceable) try: prop_slug = self.record_inhabitant_code_action( program_id=program_id, - action={**action, "type": action.get("type", "code_proposal"), "_guarded_blocked": True, "gate": gate_result}, + action={ + **action, + "type": action.get("type", "code_proposal"), + "_guarded_blocked": True, + "gate": gate_result, + }, cycle_id=cycle_id, constitution_refs=const_refs, user_objective_refs=uo_refs, @@ -3288,7 +3524,11 @@ def guarded_apply_inhabitant_action( real_edit_attempted = False if real_contribution_mode and conductor_approval and allow_real_source_targets: ca = conductor_approval - if isinstance(ca, dict) and ca.get("conductor_override") and ca.get("conductor_signature"): + if ( + isinstance(ca, dict) + and ca.get("conductor_override") + and ca.get("conductor_signature") + ): try: # Safe real root: the agentdrive workspace (user's actual system) # Extremely conservative: resolve, require startswith, no traversal (safe_join helps) @@ -3299,24 +3539,42 @@ def guarded_apply_inhabitant_action( candidate = safe_join(ad_root, str(tpath)) else: candidate = tpath - cand_str = str(candidate.resolve()) if hasattr(candidate, "resolve") else str(candidate) + cand_str = ( + str(candidate.resolve()) + if hasattr(candidate, "resolve") + else str(candidate) + ) root_str = str(ad_root) if cand_str.startswith(root_str) and ".." not in str(target): # extra belt # Additional real-mode guards (beyond gate) - if not any(cand_str.endswith(ext) for ext in (".py", ".md", ".txt", ".rst")): - result["logs"].append("REAL_EDIT_BLOCKED: target ext not in safe source set for real contrib") + if not any( + cand_str.endswith(ext) for ext in (".py", ".md", ".txt", ".rst") + ): + result["logs"].append( + "REAL_EDIT_BLOCKED: target ext not in safe source set for real contrib" + ) elif len(str(content)) > 5000: - result["logs"].append("REAL_EDIT_BLOCKED: content exceeds 5k real-mode limit") + result["logs"].append( + "REAL_EDIT_BLOCKED: content exceeds 5k real-mode limit" + ) else: candidate.parent.mkdir(parents=True, exist_ok=True) - cstr = json.dumps(content, indent=2, default=str) if isinstance(content, (dict, list)) else str(content) + cstr = ( + json.dumps(content, indent=2, default=str) + if isinstance(content, (dict, list)) + else str(content) + ) # For extra safety in real: write atomically-ish via temp then replace (simple here) candidate.write_text(cstr, encoding="utf-8") target_written = str(candidate) do_edit = True real_edit_attempted = True - result["logs"].append(f"REAL CONTRIBUTION EDIT (heavily gated, Conductor-approved): {target_written} ({len(cstr)} chars) [charter 1780296458]") - result["logs"].append(" WARNING: real source mutated under explicit Conductor approval + all gates. Full DNA + review queue trace exists.") + result["logs"].append( + f"REAL CONTRIBUTION EDIT (heavily gated, Conductor-approved): {target_written} ({len(cstr)} chars) [charter 1780296458]" + ) + result["logs"].append( + " WARNING: real source mutated under explicit Conductor approval + all gates. Full DNA + review queue trace exists." + ) except (PathTraversalError, Exception) as pex: result["logs"].append(f"Real contrib path guard blocked: {pex}") real_edit_attempted = True # attempted but failed safely @@ -3328,39 +3586,63 @@ def guarded_apply_inhabitant_action( root = Path(root_str) root.mkdir(parents=True, exist_ok=True) # Force relative under this demo root; use safe_name on last segment + safe_join - safe_seg = target.replace("..", "_").replace("/", "_").replace("\\", "_")[-120:] + safe_seg = ( + target.replace("..", "_").replace("/", "_").replace("\\", "_")[-120:] + ) if not safe_seg or safe_seg in (".", "_"): safe_seg = f"demo_inhabitant_change_{int(time.time())}.py" candidate = safe_join(root, safe_seg) # v1: only allow writes inside the demo root (never real source) if str(candidate).startswith(str(root)): candidate.parent.mkdir(parents=True, exist_ok=True) - cstr = json.dumps(content, indent=2, default=str) if isinstance(content, (dict, list)) else str(content) + cstr = ( + json.dumps(content, indent=2, default=str) + if isinstance(content, (dict, list)) + else str(content) + ) candidate.write_text(cstr, encoding="utf-8") target_written = str(candidate) do_edit = True - result["logs"].append(f"SAFE DEMO EDIT performed (under demo root only): {target_written} ({len(cstr)} chars)") + result["logs"].append( + f"SAFE DEMO EDIT performed (under demo root only): {target_written} ({len(cstr)} chars)" + ) break except (PathTraversalError, Exception) as pex: result["logs"].append(f"Demo path guard skipped root {root_str}: {pex}") continue if not do_edit: - result["logs"].append("Real edit skipped (no suitable demo root or safety block or real-mode gates not met). Staying in simulation. For real ship: supply real_contribution_mode + conductor_approval + allow_real_source_targets + dry_run=False after Conductor review queue approval.") + result["logs"].append( + "Real edit skipped (no suitable demo root or safety block or real-mode gates not met). Staying in simulation. For real ship: supply real_contribution_mode + conductor_approval + allow_real_source_targets + dry_run=False after Conductor review queue approval." + ) elif dry_run: - result["logs"].append("DRY_RUN (default): edit simulated only. No FS changes. To exercise real path, set dry_run=False + supply target under /tmp/agentdrive_guardian_demo/... OR (for 1780296458 real ship) use real_contribution_mode=True + conductor_approval from queue + allow_real_source_targets=True on actual source target.") + result["logs"].append( + "DRY_RUN (default): edit simulated only. No FS changes. To exercise real path, set dry_run=False + supply target under /tmp/agentdrive_guardian_demo/... OR (for 1780296458 real ship) use real_contribution_mode=True + conductor_approval from queue + allow_real_source_targets=True on actual source target." + ) if target and content: - result["logs"].append(f" (simulated) would target: {target} len={len(str(content))}") + result["logs"].append( + f" (simulated) would target: {target} len={len(str(content))}" + ) # (c) Minimal verification (py_compile on written or a temp repro if content provided) verification: dict[str, Any] = {"py_compile": "skipped", "ok": True} verif_target = target_written - if (do_edit or (dry_run and content)) and (target_written or (isinstance(content, str) and content.strip().startswith(("def ", "import ", "class ", "#")))): + if (do_edit or (dry_run and content)) and ( + target_written + or ( + isinstance(content, str) + and content.strip().startswith(("def ", "import ", "class ", "#")) + ) + ): try: if not verif_target: # ephemeral temp for compile check only (no persist) td = Path(tempfile.mkdtemp(prefix="guardian_pycompile_")) verif_target = td / "verify_snippet.py" - cstr = json.dumps(content, indent=2, default=str) if not isinstance(content, str) else content + cstr = ( + json.dumps(content, indent=2, default=str) + if not isinstance(content, str) + else content + ) verif_target.write_text(cstr, encoding="utf-8") comp = subprocess.run( [sys.executable or "python3", "-m", "py_compile", str(verif_target)], @@ -3375,13 +3657,19 @@ def guarded_apply_inhabitant_action( "file_checked": str(verif_target), "stderr": (comp.stderr or "")[:280] if not ok else "", } - result["logs"].append(f"py_compile verification: {verification['py_compile']} on {verif_target}") + result["logs"].append( + f"py_compile verification: {verification['py_compile']} on {verif_target}" + ) except Exception as vx: verification = {"py_compile": "error", "ok": False, "error": str(vx)[:120]} result["logs"].append(f"Verification step error (non-fatal): {vx}") result["verification"] = verification - result["edit_details"] = {"written_to": target_written, "dry_run": dry_run} if target_written else {"simulated": True, "dry_run": dry_run} + result["edit_details"] = ( + {"written_to": target_written, "dry_run": dry_run} + if target_written + else {"simulated": True, "dry_run": dry_run} + ) # (d)(e) Record apply + test_result as DNA try: @@ -3533,7 +3821,8 @@ def conductor_approve_proposal( "conductor_override": True, "conductor_signature": str(conductor_signature).strip(), "approved_at": now, - "approval_notes": approval_notes or "Explicit Conductor approval for real contribution (Inhabitants that Ship)", + "approval_notes": approval_notes + or "Explicit Conductor approval for real contribution (Inhabitants that Ship)", "approved_by": "Conductor (via conductor_approve_proposal 1780296458)", "program_id": found.get("program_id"), } diff --git a/src/agentdrive/golden_path.py b/src/agentdrive/golden_path.py index 7b6c929..25e43d0 100644 --- a/src/agentdrive/golden_path.py +++ b/src/agentdrive/golden_path.py @@ -154,7 +154,9 @@ def verify_step(step_id: str) -> dict[str, Any]: "step": step_id, "title": step.title, "optional": True, - "detail": "experience seed or genomes present" if ok else "empty registry — run seed-experience-v3", + "detail": "experience seed or genomes present" + if ok + else "empty registry — run seed-experience-v3", } if step_id == "think": @@ -238,14 +240,33 @@ def run_walkthrough( continue if step.id == "doctor": - result = run_operation("doctor") - entry = {"step": step.id, "title": step.title, "success": result.get("success"), "result": result} + result = run_operation("doctor", dry_run=True) if dry_run else run_operation("doctor") + entry = { + "step": step.id, + "title": step.title, + "success": result.get("success"), + "result": result, + } results.append(entry) if stop_on_fail and not result.get("success"): break continue if step.id == "mcp": + if dry_run: + results.append( + { + "step": step.id, + "title": step.title, + "success": True, + "skipped": True, + "dry_run": True, + "detail": "MCP doctor skipped in dry-run", + "note": "Run agentdrive mcp install && agentdrive mcp doctor to verify live clients.", + } + ) + continue + from agentdrive.adapters.mcp_config import run_mcp_doctor report = run_mcp_doctor() @@ -277,7 +298,12 @@ def run_walkthrough( result = run_operation("reconcile_seed", dry_run=True) else: result = run_operation("reconcile_seed") - entry = {"step": step.id, "title": step.title, "success": result.get("success"), "result": result} + entry = { + "step": step.id, + "title": step.title, + "success": result.get("success"), + "result": result, + } results.append(entry) if stop_on_fail and not result.get("success"): break @@ -297,12 +323,17 @@ def run_walkthrough( "success": True, "skipped": True, "detail": "no provider configured — run: agentdrive provider set ", - "hint": "agentdrive think \"...\" --dry-run # works without provider", + "hint": 'agentdrive think "..." --dry-run # works without provider', } ) continue result = run_operation("think", **kwargs) - entry = {"step": step.id, "title": step.title, "success": result.get("success"), "result": result} + entry = { + "step": step.id, + "title": step.title, + "success": result.get("success"), + "result": result, + } if not result.get("success") and not dry_run and not provider_configured(): entry["hint"] = "agentdrive provider set openai --model gpt-4o" results.append(entry) @@ -326,7 +357,12 @@ def run_walkthrough( type="operational", source="observed", ) - entry = {"step": step.id, "title": step.title, "success": result.get("success"), "result": result} + entry = { + "step": step.id, + "title": step.title, + "success": result.get("success"), + "result": result, + } results.append(entry) if stop_on_fail and not result.get("success"): break @@ -337,7 +373,12 @@ def run_walkthrough( if dry_run: kwargs["dry_run"] = True result = run_operation("pool_query", **kwargs) - entry = {"step": step.id, "title": step.title, "success": result.get("success"), "result": result} + entry = { + "step": step.id, + "title": step.title, + "success": result.get("success"), + "result": result, + } results.append(entry) if stop_on_fail and not result.get("success"): break @@ -350,4 +391,4 @@ def run_walkthrough( "steps": results, "passed": sum(1 for r in results if r.get("success")), "total": len(results), - } \ No newline at end of file + } diff --git a/src/agentdrive/grid/engine.py b/src/agentdrive/grid/engine.py index 29768bb..b2f97ae 100644 --- a/src/agentdrive/grid/engine.py +++ b/src/agentdrive/grid/engine.py @@ -487,7 +487,12 @@ async def _scan_and_heal_once(self) -> None: self._grid_health["status"] = "monitoring" def form_autonomous_research_thread( - self, *, roles: list[str] | None = None, budget: int = 1500, objective: str | None = None, **kwargs: Any + self, + *, + roles: list[str] | None = None, + budget: int = 1500, + objective: str | None = None, + **kwargs: Any, ) -> dict: """GridEngine surface for real-time Grid to dynamically spawn multi-agent research organizations. Wires directly to HealingFactor research org @@ -506,11 +511,18 @@ def form_autonomous_research_thread( ) # v3 GraphGardener minimal support in form_autonomous_research_thread (reuse manifest pattern) try: - if roles and any("gardener" in str(r).lower() or "graph" in str(r).lower() for r in roles): + if roles and any( + "gardener" in str(r).lower() or "graph" in str(r).lower() for r in roles + ): manifest["gardener"] = True - manifest["fabric_briefing"] = {"source": "GraphGardener via GridEngine form + recorder", "stabilization_wave": "20260531"} + manifest["fabric_briefing"] = { + "source": "GraphGardener via GridEngine form + recorder", + "stabilization_wave": "20260531", + } manifest["densification_history"] = [] - manifest["research_thread_lineage_fabric"] = {"constitution": "research-constitution-graphgardener-gridnative@stabilization-wave-20260531"} + manifest["research_thread_lineage_fabric"] = { + "constitution": "research-constitution-graphgardener-gridnative@stabilization-wave-20260531" + } except Exception: pass return manifest @@ -557,9 +569,15 @@ def register_model_program(self, manifest: dict[str, Any]) -> dict[str, Any]: system = IntegratedRealTimeEvolutionSystem(swarm_id=effective_swarm) recorder = system.recorder - program_id = str(manifest.get("program_id") or manifest.get("id") or f"prog-{int(time.time())}") - user_objectives = manifest.get("user_objective_refs") or manifest.get("objectives") or [] - constitution_refs = list(manifest.get("constitution_refs") or manifest.get("constitutions") or []) + program_id = str( + manifest.get("program_id") or manifest.get("id") or f"prog-{int(time.time())}" + ) + user_objectives = ( + manifest.get("user_objective_refs") or manifest.get("objectives") or [] + ) + constitution_refs = list( + manifest.get("constitution_refs") or manifest.get("constitutions") or [] + ) # === Wire Program Contract as MANDATORY in registration (enforce binding + DNA if missing) === # This is the high-leverage enforcement: every registered inhabitant now binds to the @@ -572,7 +590,8 @@ def register_model_program(self, manifest: dict[str, Any]) -> dict[str, Any]: "research-constitution-ad-grid-program-contract@stabilization-wave-20260531", ] had_contract = any( - any(cr in str(c).lower() for cr in ("program-contract", "ad-grid-program-contract")) for c in constitution_refs + any(cr in str(c).lower() for cr in ("program-contract", "ad-grid-program-contract")) + for c in constitution_refs ) for cref in contract_refs: if cref not in constitution_refs: @@ -587,7 +606,9 @@ def register_model_program(self, manifest: dict[str, Any]) -> dict[str, Any]: "type": "program_contract_binding_enforced_at_registration", "program_id": program_id, "binding": "auto-injected mandatory Program Contract refs (top-level governance)", - "original_manifest_const_refs": list(manifest.get("original_constitution_refs") or []), + "original_manifest_const_refs": list( + manifest.get("original_constitution_refs") or [] + ), "enforced_by": "GridEngine.register_model_program", "rationale": "Mandatory per Program Contract + Guardian constitution + ILO Guardian Lens deeper enforcement; closes gap where registration could precede binding", "charter": "1780293824", @@ -597,7 +618,8 @@ def register_model_program(self, manifest: dict[str, Any]) -> dict[str, Any]: program_id=program_id or "unknown-program-at-reg", action=bind_action, constitution_refs=constitution_refs, - user_objective_refs=user_objectives or ["system:self-registration-enforcement"], + user_objective_refs=user_objectives + or ["system:self-registration-enforcement"], ) except Exception: # Never fail registration on audit recording @@ -664,19 +686,23 @@ def list_active_programs(self) -> list[dict[str, Any]]: obs_dir = drive_path / "observations" / "meta-evolution" if obs_dir.exists(): - for p in sorted(obs_dir.glob("*.json"), key=lambda x: x.stat().st_mtime, reverse=True)[:50]: + for p in sorted( + obs_dir.glob("*.json"), key=lambda x: x.stat().st_mtime, reverse=True + )[:50]: try: data = json.loads(p.read_text()) if data.get("page_type") in ("model-program-manifest", "agent-program"): m = data.get("manifest", {}) - programs.append({ - "program_id": m.get("program_id"), - "created": m.get("created"), - "user_objective_refs": m.get("user_objective_refs", []), - "current_mandate": m.get("current_mandate"), - "lifecycle": m.get("lifecycle"), - "source_file": str(p), - }) + programs.append( + { + "program_id": m.get("program_id"), + "created": m.get("created"), + "user_objective_refs": m.get("user_objective_refs", []), + "current_mandate": m.get("current_mandate"), + "lifecycle": m.get("lifecycle"), + "source_file": str(p), + } + ) except Exception: continue @@ -740,7 +766,11 @@ async def _maintenance_loop(self) -> None: # Minimal v3 GraphGardener support in maintenance_loop (per task): detect gardener threads, # surface active_gardener_threads + last pass stamp for health/manifests (stabilization-wave drive) try: - gardener_cids = [c for c in self._active_research_thread_jobs if "graphgardener" in c or "gardener" in c] + gardener_cids = [ + c + for c in self._active_research_thread_jobs + if "graphgardener" in c or "gardener" in c + ] self._grid_health["active_gardener_threads"] = len(gardener_cids) if gardener_cids: self._grid_health["last_gardener_pass_ts"] = now @@ -809,12 +839,19 @@ def _run_research_thread_pass(self) -> None: "research_budget_per_iteration": budget, "branch_policy": "governance_enforcement_and_inhabitant_binding", "council_role": "Binding Program Contract (top-level rules)", - "high_signal_threshold": {"binding_violation_detected": 0, "attribution_completeness": 1.0}, + "high_signal_threshold": { + "binding_violation_detected": 0, + "attribution_completeness": 1.0, + }, "enforcement": { "guardian_gate_required": True, - "mandatory_fields_on_all_actions": ["program_id", "user_objective_refs", "constitution_refs (incl. this contract)"], - "escalation": "Adversary + explicit Conductor notification" - } + "mandatory_fields_on_all_actions": [ + "program_id", + "user_objective_refs", + "constitution_refs (incl. this contract)", + ], + "escalation": "Adversary + explicit Conductor notification", + }, }, # Experience Layer Research Branching Swarm integration: each constitution now drives # native research-thread forks (first-class living-experience genome families) via @@ -916,7 +953,12 @@ def _run_research_thread_pass(self) -> None: "healing_integration": { "emit_healing_signal_on_major_lift": True, "trigger_graph_densification": True, - "recorder_surfaces": ["find_weak_across_recent_cycles", "propose_densification_edges", "record_densification_lift", "write_connection_densification_observation"], + "recorder_surfaces": [ + "find_weak_across_recent_cycles", + "propose_densification_edges", + "record_densification_lift", + "write_connection_densification_observation", + ], }, "fabric_briefing_ref": "ExperienceGraphRecorder + renderers + embed_graph_into_artifact (evolution/experience_graph.py)", "lineage_fabric": "ResearchThreadLineage with densification_history + fabric_coherence", @@ -936,7 +978,10 @@ def _run_research_thread_pass(self) -> None: "research_budget_per_iteration": budget, "branch_policy": "optimization_densification_pressure", "council_role": "PerfectionistOptimizer", - "high_signal_threshold": {"min_coherence_gain": 0.03, "min_densification_lift": 0.04}, + "high_signal_threshold": { + "min_coherence_gain": 0.03, + "min_densification_lift": 0.04, + }, }, { "id": "research-constitution-guardian-integrity@stabilization-wave-20260531", @@ -1052,15 +1097,39 @@ def _research_runner(): # v3 GraphGardener Grid Integrator (minimal reuse of ResearchThreadLineage / harness / event / manifest patterns exactly) # pass recorder surfaces (noted), set gardener=True flag, include fabric_briefing + densification_history # in high-signal obs + return manifests. Target stabilization-wave-20260531 drive. - is_gardener = bool(constitution_ref.get("gardener")) or "graphgardener" in str(constitution_ref.get("id", "")).lower() + is_gardener = ( + bool(constitution_ref.get("gardener")) + or "graphgardener" in str(constitution_ref.get("id", "")).lower() + ) if is_gardener: observation["gardener"] = True observation["fabric_briefing"] = { - "recorder_surfaces_passed": constitution_ref.get("healing_integration", {}).get("recorder_surfaces", ["find_weak_across_recent_cycles", "propose_densification_edges", "record_densification_lift"]), + "recorder_surfaces_passed": constitution_ref.get( + "healing_integration", {} + ).get( + "recorder_surfaces", + [ + "find_weak_across_recent_cycles", + "propose_densification_edges", + "record_densification_lift", + ], + ), "8_step_sequencing": "find_weak -> propose (3 canonical densif relations) -> enter_phase -> harness/measure (conn_density 0.28) -> record_lift -> write_obs (fabric_briefing+densif_history+fusion)", - "densification_relations": ["DENSIFIED_VIA_GARDENER", "CONNECTION_STRENGTHENED_BY", "GRAPH_COHERENCE_LIFT"], + "densification_relations": [ + "DENSIFIED_VIA_GARDENER", + "CONNECTION_STRENGTHENED_BY", + "GRAPH_COHERENCE_LIFT", + ], } - observation["densification_history"] = [{"lift": round(improvement, 4), "coherence_post": 0.79, "edges_added": 5, "cycle": "sim-from-grid-gardener-thread", "ts": "2026-05-31"}] + observation["densification_history"] = [ + { + "lift": round(improvement, 4), + "coherence_post": 0.79, + "edges_added": 5, + "cycle": "sim-from-grid-gardener-thread", + "ts": "2026-05-31", + } + ] # ResearchThreadLineage fabric carrying (exact reuse of create_ helper + to_lineage_entry) try: from agentdrive.constants import ( @@ -1068,24 +1137,35 @@ def _research_runner(): new_correlation_id, ) from agentdrive.reconciliation import create_research_thread_fork + lineage = create_research_thread_fork( parent_genome_id="experience-graph-v3-fabric@stabilization-wave-20260531", constitution_ref=constitution_ref["id"], budget=research_budget, correlation_id=get_correlation_id() or new_correlation_id(), - thread_id=f"gardener-fabric-{int(time.time())%10000}", + thread_id=f"gardener-fabric-{int(time.time()) % 10000}", ) observation["research_thread_lineage"] = lineage.to_lineage_entry() observation["lineage_fabric_carried"] = True except Exception: - observation["research_thread_lineage"] = {"note": "fabric lineage via ResearchThreadLineage (stub in runner)"} + observation["research_thread_lineage"] = { + "note": "fabric lineage via ResearchThreadLineage (stub in runner)" + } # Update Grid health gardener keys (surfacing) try: lift_val = max(float(improvement), 0.08) - self._grid_health["last_gardener_densification_lift"] = round(lift_val, 4) - self._grid_health["fabric_coherence_last"] = round(0.48 + lift_val, 3) + self._grid_health["last_gardener_densification_lift"] = round( + lift_val, 4 + ) + self._grid_health["fabric_coherence_last"] = round( + 0.48 + lift_val, 3 + ) self._grid_health["multi_cycle_edges"] += 3 - self._grid_health["densification_lifts_total"] = round(self._grid_health.get("densification_lifts_total", 0.0) + lift_val, 4) + self._grid_health["densification_lifts_total"] = round( + self._grid_health.get("densification_lifts_total", 0.0) + + lift_val, + 4, + ) self._emit_grid_health_if_attached() # light MC on gardener health lift except Exception: pass @@ -1095,9 +1175,15 @@ def _research_runner(): **(self._research_thread_manifests.get(cid, {})), "gardener": True, "fabric_briefing": observation.get("fabric_briefing"), - "densification_history": observation.get("densification_history"), - "research_thread_lineage": observation.get("research_thread_lineage"), - "status": "high_signal_gardener" if high_signal else "active_gardener", + "densification_history": observation.get( + "densification_history" + ), + "research_thread_lineage": observation.get( + "research_thread_lineage" + ), + "status": "high_signal_gardener" + if high_signal + else "active_gardener", "constitution_ref": cid, "last_updated": time.time(), } @@ -1129,20 +1215,31 @@ def _research_runner(): new_correlation_id, ) from agentdrive.events import HealingSignalEvent, emit - lift_thresh = constitution_ref.get("high_signal_threshold", {}).get("min_densification_lift", 0.05) + + lift_thresh = constitution_ref.get( + "high_signal_threshold", {} + ).get("min_densification_lift", 0.05) if improvement >= lift_thresh: - emit(HealingSignalEvent( - signal_type="graph_densification_major_lift", - source="grid_engine_gardener_research_thread", - details={ - "constitution_id": cid, - "densification_lift": round(improvement, 4), - "fabric_coherence": self._grid_health.get("fabric_coherence_last"), - "drive": "stabilization-wave-20260531", - }, - correlation_id=get_correlation_id() or new_correlation_id(), - )) - self._grid_health["gardener_threads_completed"] = self._grid_health.get("gardener_threads_completed", 0) + 1 + emit( + HealingSignalEvent( + signal_type="graph_densification_major_lift", + source="grid_engine_gardener_research_thread", + details={ + "constitution_id": cid, + "densification_lift": round(improvement, 4), + "fabric_coherence": self._grid_health.get( + "fabric_coherence_last" + ), + "drive": "stabilization-wave-20260531", + }, + correlation_id=get_correlation_id() + or new_correlation_id(), + ) + ) + self._grid_health["gardener_threads_completed"] = ( + self._grid_health.get("gardener_threads_completed", 0) + + 1 + ) except Exception: pass return { @@ -1234,7 +1331,11 @@ async def _heartbeat_loop(self) -> None: self._grid_health["active_research_threads"] = active # v3 GraphGardener minimal heartbeat support (reuse pattern) try: - g_active = sum(1 for c in getattr(self, "_active_research_thread_jobs", {}) if "graphgardener" in c or "gardener" in c) + g_active = sum( + 1 + for c in getattr(self, "_active_research_thread_jobs", {}) + if "graphgardener" in c or "gardener" in c + ) self._grid_health["active_gardener_threads"] = g_active except Exception: pass @@ -1525,8 +1626,14 @@ def run_dogfood_research_experiments(self, max_threads: int = 2) -> list[dict]: "8_step": "find_weak_across -> propose_densif -> ... -> write with fabric_briefing + densification_history", "recorder": "ExperienceGraphRecorder surfaces for Grid native dispatch", } - obs["densification_history"] = [{"lift": round(scores.resilience_lift, 4), "edges": 5}] - obs["research_thread_lineage"] = {"fabric": True, "constitution": cid, "via": "ResearchThreadLineage"} + obs["densification_history"] = [ + {"lift": round(scores.resilience_lift, 4), "edges": 5} + ] + obs["research_thread_lineage"] = { + "fabric": True, + "constitution": cid, + "via": "ResearchThreadLineage", + } obs["framework"]["gardener"] = True except Exception: pass @@ -1591,12 +1698,15 @@ def _emit_grid_health_if_attached(self) -> None: from agentdrive.mission_control.events import GridHealthEvent from agentdrive.mission_control.server import publish_event_sync + health = self.get_grid_health() - publish_event_sync(GridHealthEvent( - event_type="grid_health", - timestamp=_t.time(), - health=health, - )) + publish_event_sync( + GridHealthEvent( + event_type="grid_health", + timestamp=_t.time(), + health=health, + ) + ) except Exception: pass @@ -1635,7 +1745,9 @@ def get_active_research_threads(self) -> list[dict]: else None, # v3 GraphGardener surfacing in get_active (from manifests populated by runner/dogfood) "gardener": bool(manifest.get("gardener")), - "fabric_coherence": manifest.get("fabric_briefing", {}).get("coherence") if isinstance(manifest.get("fabric_briefing"), dict) else manifest.get("fabric_coherence_last"), + "fabric_coherence": manifest.get("fabric_briefing", {}).get("coherence") + if isinstance(manifest.get("fabric_briefing"), dict) + else manifest.get("fabric_coherence_last"), } out.append(entry) return out diff --git a/src/agentdrive/harness/compose.py b/src/agentdrive/harness/compose.py index 85fa75b..3f718a9 100644 --- a/src/agentdrive/harness/compose.py +++ b/src/agentdrive/harness/compose.py @@ -177,4 +177,4 @@ def assemble_layered_prompt(base: str, layers: ComposeLayers, pool: Any) -> str: def sessions_dir() -> Path: """Return the top-level sessions directory used by ``resolve_session_layer``.""" - return get_agentdrive_home() / "sessions" \ No newline at end of file + return get_agentdrive_home() / "sessions" diff --git a/src/agentdrive/inheritance.py b/src/agentdrive/inheritance.py index 3237ff9..3610bb8 100644 --- a/src/agentdrive/inheritance.py +++ b/src/agentdrive/inheritance.py @@ -20,15 +20,20 @@ import json import logging +import os +import re from dataclasses import asdict, dataclass, field from datetime import UTC, datetime from pathlib import Path from typing import TYPE_CHECKING, Any +import yaml + from agentdrive.constants import get_agentdrive_home from agentdrive.events import ( InheritanceAbsorbed, InheritanceReceived, + SkillAssimilated, SubagentDone, emit, subscribe, @@ -39,6 +44,22 @@ logger = logging.getLogger(__name__) +_SKILL_BLOCK_RE = re.compile( + r"```(?:agentdrive-skill|agentdrive_skill)\s*\n(.*?)\n```", + re.DOTALL | re.IGNORECASE, +) +_SKILL_BLOCK_SPLIT_RE = re.compile(r"\n---\s*\n", re.DOTALL) +_MAX_RESULT_EVIDENCE_CHARS = 240 + + +def _extend_raw_skill_candidates(target: list[Any], value: Any) -> None: + if not value: + return + if isinstance(value, (list, tuple)): + target.extend(value) + else: + target.append(value) + # ───────────────────────────────────────────────────────────────────── # Data classes @@ -49,6 +70,49 @@ def _utc_now_iso() -> str: return datetime.now(UTC).isoformat() +@dataclass +class InheritedSkillCandidate: + """A reusable playbook a sub-agent proposes back to its parent.""" + + name: str + description: str = "" + body: str = "" + tags: list[str] = field(default_factory=list) + operation: str | None = None + evidence: dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_raw(cls, raw: Any) -> InheritedSkillCandidate | None: + if isinstance(raw, cls): + return raw + if not isinstance(raw, dict): + return None + name = str(raw.get("name") or raw.get("skill") or "").strip() + body = str(raw.get("body") or raw.get("playbook") or raw.get("steps") or "").strip() + if not name or not body: + return None + tags = raw.get("tags") or [] + if isinstance(tags, str): + tag_list = [t.strip() for t in tags.split(",") if t.strip()] + elif isinstance(tags, list): + tag_list = [str(t).strip() for t in tags if str(t).strip()] + else: + tag_list = [] + return cls( + name=name, + description=str(raw.get("description") or "").strip(), + body=body, + tags=tag_list, + operation=( + str(raw.get("operation") or raw.get("agentdrive_operation") or "").strip() or None + ), + evidence=dict(raw.get("evidence") or {}), + ) + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + @dataclass class InheritanceManifest: """A single sub-agent's report of what it learned in one mission.""" @@ -57,27 +121,178 @@ class InheritanceManifest: swarm_id: str = "" genomes_pulled: list[str] = field(default_factory=list) genomes_created: list[str] = field(default_factory=list) + skills_created: list[InheritedSkillCandidate] = field(default_factory=list) outcomes_logged: list[dict[str, Any]] = field(default_factory=list) duration_s: float = 0.0 created_at: str = field(default_factory=_utc_now_iso) def to_json(self) -> str: - return json.dumps(asdict(self), indent=2, default=str) + payload = asdict(self) + payload["skills_created"] = [s.to_dict() for s in self.skills_created] + return json.dumps(payload, indent=2, default=str) @classmethod def from_json(cls, data: str | bytes) -> InheritanceManifest: raw = json.loads(data) + skills = [ + skill + for skill in ( + InheritedSkillCandidate.from_raw(s) for s in raw.get("skills_created", []) or [] + ) + if skill is not None + ] return cls( subagent_id=str(raw.get("subagent_id", "") or ""), swarm_id=str(raw.get("swarm_id", "") or ""), genomes_pulled=list(raw.get("genomes_pulled", []) or []), genomes_created=list(raw.get("genomes_created", []) or []), + skills_created=skills, outcomes_logged=list(raw.get("outcomes_logged", []) or []), duration_s=float(raw.get("duration_s", 0.0) or 0.0), created_at=str(raw.get("created_at", "") or _utc_now_iso()), ) +def extract_skill_candidates_from_result( + result: Any, + *, + task: str = "", +) -> list[InheritedSkillCandidate]: + """Extract explicit AgentDrive skill proposals from a sub-agent result. + + Sub-agents can return fenced blocks shaped like:: + + ```agentdrive-skill + name: incident-retrospective-playbook + description: Reusable incident review handoff + tags: [incident, retrospective] + --- + # Incident Retrospective Playbook + ... + ``` + + The parser is intentionally opt-in. We do not turn every long sub-agent + response into a skill, because that would pollute the parent bench with + low-signal transcript fragments. + """ + raw_candidates: list[Any] = [] + text_parts: list[str] = [] + if isinstance(result, dict): + _extend_raw_skill_candidates(raw_candidates, result.get("skills_created")) + _extend_raw_skill_candidates(raw_candidates, result.get("agentdrive_skills")) + for key in ("result", "summary", "text", "content", "output"): + value = result.get(key) + if isinstance(value, str) and value.strip(): + text_parts.append(value) + elif isinstance(result, (list, tuple)): + for item in result: + if isinstance(item, dict): + raw_candidates.append(item) + elif isinstance(item, str): + text_parts.append(item) + elif isinstance(result, str): + text_parts.append(result) + elif result is not None: + text_parts.append(str(result)) + + candidates: list[InheritedSkillCandidate] = [] + for raw in raw_candidates: + candidate = InheritedSkillCandidate.from_raw(raw) + if candidate is not None: + candidates.append(candidate) + + for text in text_parts: + candidates.extend(_extract_skill_blocks(text, task=task)) + + deduped: dict[str, InheritedSkillCandidate] = {} + for candidate in candidates: + key = candidate.name.strip().lower() + if key and key not in deduped: + deduped[key] = candidate + return list(deduped.values()) + + +def _extract_skill_blocks(text: str, *, task: str = "") -> list[InheritedSkillCandidate]: + candidates: list[InheritedSkillCandidate] = [] + for match in _SKILL_BLOCK_RE.finditer(text): + block = match.group(1).strip() + header_text = "" + body = block + parts = _SKILL_BLOCK_SPLIT_RE.split(block, maxsplit=1) + if len(parts) == 2: + header_text, body = parts[0].strip(), parts[1].strip() + meta: dict[str, Any] = {} + if header_text: + try: + loaded = yaml.safe_load(header_text) or {} + if isinstance(loaded, dict): + meta = loaded + except yaml.YAMLError: + logger.debug("Failed to parse inherited skill block header", exc_info=True) + + evidence = dict(meta.get("evidence") or {}) + if task and "source_task" not in evidence: + evidence["source_task"] = task[:_MAX_RESULT_EVIDENCE_CHARS] + evidence.setdefault("source", "subagent_result") + raw = { + "name": meta.get("name") or meta.get("skill"), + "description": meta.get("description") or "", + "tags": meta.get("tags") or [], + "operation": meta.get("operation") or meta.get("agentdrive_operation"), + "body": body, + "evidence": evidence, + } + candidate = InheritedSkillCandidate.from_raw(raw) + if candidate is not None: + candidates.append(candidate) + return candidates + + +def write_subagent_result_manifest( + *, + swarm_id: str, + subagent_id: str, + task: str, + result: Any, + duration_s: float = 0.0, +) -> InheritanceManifest | None: + """Merge explicit skill proposals from a sub-agent result into its manifest. + + This is the runtime ferry between Hermes-style sub-agent handoffs and the + parent AgentDrive skill pool. It writes before ``SubagentDone`` so the + existing inheritance subscriber can absorb the manifest normally. + """ + skills = extract_skill_candidates_from_result(result, task=task) + if not skills: + return None + + path = manifest_path(swarm_id, subagent_id) + manifest: InheritanceManifest + if path.is_file(): + try: + manifest = InheritanceManifest.from_json(path.read_text(encoding="utf-8")) + except Exception: + logger.debug("Failed to merge existing inheritance manifest %s", path, exc_info=True) + manifest = InheritanceManifest(subagent_id=subagent_id, swarm_id=swarm_id) + else: + manifest = InheritanceManifest(subagent_id=subagent_id, swarm_id=swarm_id) + + manifest.subagent_id = manifest.subagent_id or subagent_id + manifest.swarm_id = manifest.swarm_id or swarm_id + if duration_s: + manifest.duration_s = max(float(manifest.duration_s or 0.0), float(duration_s)) + + existing = {skill.name.strip().lower() for skill in manifest.skills_created} + for skill in skills: + key = skill.name.strip().lower() + if key and key not in existing: + manifest.skills_created.append(skill) + existing.add(key) + + _write_manifest(manifest) + return manifest + + @dataclass class InheritanceResult: """Outcome of recording (and optionally absorbing) a manifest.""" @@ -85,6 +300,8 @@ class InheritanceResult: manifest: InheritanceManifest genomes_absorbed: list[str] = field(default_factory=list) genomes_rejected: list[str] = field(default_factory=list) + skills_absorbed: list[str] = field(default_factory=list) + skills_rejected: list[str] = field(default_factory=list) reason_per_rejected: dict[str, str] = field(default_factory=dict) @@ -201,6 +418,7 @@ def record_manifest( auto_absorb: bool = True, source_pool: AgentDrive | None = None, quarantine_external: bool = False, + skill_outcome_success: bool | None = None, ) -> InheritanceResult: """Persist a manifest and optionally absorb its new genomes into a pool. @@ -216,6 +434,12 @@ def record_manifest( and any other code path that receives DNA from outside the local instance. + If ``skill_outcome_success`` is supplied, every installed inherited skill + also records one usage outcome. The successful ``SubagentDone`` hook passes + ``True`` here, giving the curation loop evidence that the playbook came from + a completed child task. Manual manifest imports leave it unset unless the + caller has its own outcome signal. + Each absorbed genome triggers the existing ``PoolIngest`` event with ``source="inheritance:"`` so subscribers can render it on the audit ribbon. A summary ``InheritanceReceived`` event fires once @@ -225,6 +449,8 @@ def record_manifest( absorbed: list[str] = [] rejected: list[str] = [] + skills_absorbed: list[str] = [] + skills_rejected: list[str] = [] reasons: dict[str, str] = {} needs_quarantine = quarantine_external and source_pool is not None @@ -289,11 +515,67 @@ def record_manifest( rejected.append(gid) reasons[gid] = f"ingest failed: {str(exc)[:80]}" + for skill in manifest.skills_created: + key = f"skill:{skill.name}" + if quarantine_external: + skills_rejected.append(skill.name) + reasons[key] = "external inherited skills require review before install" + continue + try: + from agentdrive.skills.registry import install_inherited_skill + + install_inherited_skill( + name=skill.name, + description=skill.description, + body=skill.body, + tags=skill.tags, + operation=skill.operation, + swarm_id=manifest.swarm_id, + source_subagent_id=manifest.subagent_id, + update_existing=True, + ) + skills_absorbed.append(skill.name) + if skill_outcome_success is not None: + try: + from agentdrive.skills.usage import record_skill_run + + record_skill_run( + skill.name, + success=bool(skill_outcome_success), + source=( + f"inheritance:{manifest.swarm_id or 'default'}:" + f"{manifest.subagent_id or 'subagent'}" + ), + ) + except Exception: + logger.debug( + "Failed to record inherited skill outcome for %s", + skill.name, + exc_info=True, + ) + try: + emit( + InheritanceAbsorbed( + skill_name=skill.name, + source_subagent_id=manifest.subagent_id, + parent_pool=getattr(target_pool, "name", "main"), + swarm_id=manifest.swarm_id or None, + subagent_id=manifest.subagent_id or None, + ) + ) + except Exception: + logger.debug("Failed to emit skill InheritanceAbsorbed", exc_info=True) + except Exception as exc: + skills_rejected.append(skill.name) + reasons[key] = f"skill install failed: {str(exc)[:80]}" + try: emit( InheritanceReceived( genomes_absorbed=list(absorbed), genomes_rejected=list(rejected), + skills_absorbed=list(skills_absorbed), + skills_rejected=list(skills_rejected), swarm_id=manifest.swarm_id or None, subagent_id=manifest.subagent_id or None, ) @@ -305,6 +587,8 @@ def record_manifest( manifest=manifest, genomes_absorbed=absorbed, genomes_rejected=rejected, + skills_absorbed=skills_absorbed, + skills_rejected=skills_rejected, reason_per_rejected=reasons, ) @@ -408,6 +692,7 @@ def _on_subagent_done(event: SubagentDone) -> None: target_pool=target, auto_absorb=True, source_pool=source_pool, + skill_outcome_success=True, ) except Exception: logger.debug( @@ -416,6 +701,59 @@ def _on_subagent_done(event: SubagentDone) -> None: manifest.subagent_id, exc_info=True, ) + return + + _auto_assimilate_completed_skills(manifest, target_pool=target) + + +def _auto_assimilate_completed_skills( + manifest: InheritanceManifest, + *, + target_pool: AgentDrive, +) -> None: + """Run the gated parent-bench assimilation pass for completed child skills.""" + if not manifest.skills_created: + return + flag = os.environ.get("AGENTDRIVE_AUTO_ASSIMILATE_SKILLS", "1").strip().lower() + if flag in {"0", "false", "no", "off"}: + return + try: + from agentdrive.skills.curation import assimilate_inherited_skills + + report = assimilate_inherited_skills( + target_drive=target_pool, + ingest_dna=True, + prune=False, + include_promoted=False, + skill_names=[skill.name for skill in manifest.skills_created], + ) + except Exception: + logger.debug( + "auto skill assimilation failed for %s/%s", + manifest.swarm_id, + manifest.subagent_id, + exc_info=True, + ) + return + + promoted = [item.name for item in report.promoted] + dna_genomes = [item.genome_id for item in report.dna_exports] + pruned = [item.get("skill_name", "") for item in report.pruned if item.get("skill_name")] + if not (promoted or dna_genomes or pruned or report.errors): + return + try: + emit( + SkillAssimilated( + promoted_skills=promoted, + dna_genomes=dna_genomes, + pruned_skills=pruned, + errors=list(report.errors), + swarm_id=manifest.swarm_id or None, + subagent_id=manifest.subagent_id or None, + ) + ) + except Exception: + logger.debug("Failed to emit SkillAssimilated", exc_info=True) # Subscribe once at import time. Using try/except so an import-order quirk @@ -428,8 +766,11 @@ def _on_subagent_done(event: SubagentDone) -> None: __all__ = [ + "InheritedSkillCandidate", "InheritanceManifest", "InheritanceResult", + "extract_skill_candidates_from_result", + "write_subagent_result_manifest", "record_manifest", "list_manifests", "load_manifest", diff --git a/src/agentdrive/learning/__init__.py b/src/agentdrive/learning/__init__.py new file mode 100644 index 0000000..14c1d7b --- /dev/null +++ b/src/agentdrive/learning/__init__.py @@ -0,0 +1,20 @@ +"""Automatic experience + skill absorption for MCP/CLI operation runs.""" + +from agentdrive.learning.auto_absorb import maybe_absorb_operation_outcome +from agentdrive.learning.framework_skills import ( + build_framework_session_pack, + route_skills_for_task, + run_framework_skill, +) +from agentdrive.learning.growth_merge import build_growth_briefing, merge_session_growth +from agentdrive.learning.skill_fusion import synthesize_from_inputs + +__all__ = [ + "maybe_absorb_operation_outcome", + "synthesize_from_inputs", + "build_growth_briefing", + "merge_session_growth", + "route_skills_for_task", + "build_framework_session_pack", + "run_framework_skill", +] diff --git a/src/agentdrive/learning/auto_absorb.py b/src/agentdrive/learning/auto_absorb.py new file mode 100644 index 0000000..8d4ced6 --- /dev/null +++ b/src/agentdrive/learning/auto_absorb.py @@ -0,0 +1,575 @@ +""" +End-to-end automatic learning for AgentDrive operations. + +Whenever an AI uses AgentDrive via MCP or CLI (``run_operation``), this module: +1. Tracks session context (context pack pulled, ops run). +2. Auto-records minimal fabric reasoning when the model did not call + ``experience_graph_record_reasoning`` on a high-signal mutating op. +3. Distills reusable inherited skills from successful outcomes (Hermes-style ferry + for parent MCP sessions, not only sub-agents). +4. Promotes + ingests proven auto-learned skills into DNA when enabled. + +Disable with ``AGENTDRIVE_AUTO_LEARN=0`` (master switch). +Finer control: ``AGENTDRIVE_AUTO_RECORD_REASONING``, ``AGENTDRIVE_AUTO_DISTILL_SKILLS``, +``AGENTDRIVE_AUTO_ASSIMILATE_SKILLS`` (existing). +""" + +from __future__ import annotations + +import logging +import os +import re +import time +from dataclasses import dataclass, field +from typing import Any + +logger = logging.getLogger(__name__) + +_MCP_SUBAGENT_ID = "mcp-auto-learning" + +# Ops that already write Parent DNA / fabric traces — skip duplicate auto-reasoning. +_SELF_RECORDING_OPS = frozenset( + { + "experience_graph_record_reasoning", + "external_parent_decision", + "multiverse_parent_decision", + "multiverse_run_full", + } +) + +# Successful outcomes on these ops distill + promote skills automatically. +_HIGH_SIGNAL_OPS = frozenset( + { + "external_parent_decision", + "multiverse_parent_decision", + "experience_graph_record_reasoning", + "think", + "record_outcome", + "codebase_observe_file", + "codebase_patterns_profile", + "codebase_mimic", + } +) + +_CODEBASE_OPS = frozenset( + { + "codebase_observe_file", + "codebase_patterns_profile", + "codebase_register_project", + "codebase_mimic", + "codebase_transform_style", + "codebase_mirror_resonance", + } +) + +_MIRROR_HIGH_SIGNAL = frozenset({"codebase_mimic", "codebase_observe_file"}) + +# Mutating ops that benefit from a lightweight reasoning trace when none was recorded. +_AUTO_REASONING_OPS = frozenset( + { + "think", + "record_outcome", + "pool_query", + "propose_improvement", + "ingest_genome", + "codebase_observe_file", + "codebase_patterns_match", + } +) + +_SESSION_START_OPS = frozenset({"experience_graph_context_pack", "think"}) + +_SLUG_RE = re.compile(r"[^a-z0-9]+") + + +def _flag(name: str, default: str = "1") -> bool: + raw = os.environ.get(name, default).strip().lower() + return raw not in {"0", "false", "no", "off"} + + +def auto_learn_enabled() -> bool: + return _flag("AGENTDRIVE_AUTO_LEARN", "1") + + +def auto_record_reasoning_enabled() -> bool: + return auto_learn_enabled() and _flag("AGENTDRIVE_AUTO_RECORD_REASONING", "1") + + +def auto_distill_skills_enabled() -> bool: + return auto_learn_enabled() and _flag("AGENTDRIVE_AUTO_DISTILL_SKILLS", "1") + + +def auto_assimilate_enabled() -> bool: + return _flag("AGENTDRIVE_AUTO_ASSIMILATE_SKILLS", "1") + + +@dataclass +class LearningSession: + swarm_id: str + program_id: str + started_at: float = field(default_factory=time.time) + context_pack_pulled: bool = False + reasoning_recorded: bool = False + ops: list[tuple[str, str]] = field(default_factory=list) + experience_traces: list[str] = field(default_factory=list) + distilled_skills: list[str] = field(default_factory=list) + referenced_skills: list[str] = field(default_factory=list) + pattern_projects: list[str] = field(default_factory=list) + fused_skill_name: str | None = None + growth_merged: bool = False + + +_SESSIONS: dict[tuple[str, str], LearningSession] = {} + + +def _session_key(swarm_id: str, program_id: str) -> tuple[str, str]: + return (swarm_id, program_id or _MCP_SUBAGENT_ID) + + +def _get_session(swarm_id: str, program_id: str | None) -> LearningSession: + key = _session_key(swarm_id, program_id or _MCP_SUBAGENT_ID) + session = _SESSIONS.get(key) + if session is None: + session = LearningSession(swarm_id=swarm_id, program_id=program_id or _MCP_SUBAGENT_ID) + _SESSIONS[key] = session + return session + + +def _effective_swarm(kwargs: dict[str, Any], result: dict[str, Any]) -> str: + return str(result.get("swarm_id") or kwargs.get("swarm_id") or "stabilization-wave-20260531") + + +def _program_id(kwargs: dict[str, Any]) -> str: + return str(kwargs.get("program_id") or _MCP_SUBAGENT_ID) + + +def _trigger_text(kwargs: dict[str, Any], result: dict[str, Any]) -> str: + for key in ("trigger", "question", "task", "text", "summary"): + val = kwargs.get(key) + if isinstance(val, str) and val.strip(): + return val.strip() + nested = result.get("result") + if isinstance(nested, dict): + for key in ("trigger", "question"): + val = nested.get(key) + if isinstance(val, str) and val.strip(): + return val.strip() + return "" + + +def _slugify(text: str, *, max_len: int = 36) -> str: + slug = _SLUG_RE.sub("-", text.lower()).strip("-") + if len(slug) > max_len: + slug = slug[:max_len].rstrip("-") + return slug or "session" + + +_MAX_SKILL_NAME = 64 + + +def _op_summary(operation: str, kwargs: dict[str, Any], result: dict[str, Any]) -> str: + trigger = _trigger_text(kwargs, result) + if trigger: + return f"{operation}: {trigger[:120]}" + return operation + + +def _should_auto_record_reasoning( + operation: str, + session: LearningSession, + result: dict[str, Any], +) -> bool: + if not auto_record_reasoning_enabled(): + return False + if session.reasoning_recorded: + return False + if operation in _SELF_RECORDING_OPS: + return False + if operation not in _AUTO_REASONING_OPS and operation not in _HIGH_SIGNAL_OPS: + return False + # Prefer recording after the model has pulled context (6-step loop grounding). + if not session.context_pack_pulled and operation not in _HIGH_SIGNAL_OPS: + return False + return True + + +def _should_distill_skill(operation: str, result: dict[str, Any]) -> bool: + if not auto_distill_skills_enabled(): + return False + if operation in _HIGH_SIGNAL_OPS: + return True + if operation == "experience_graph_context_pack": + return False + return operation in _AUTO_REASONING_OPS + + +def _auto_record_reasoning( + operation: str, + kwargs: dict[str, Any], + result: dict[str, Any], + swarm_id: str, + program_id: str, +) -> str | None: + try: + from agentdrive.operations.registry import _integrated_recorder # noqa: PLC2701 + + _, recorder = _integrated_recorder(swarm_id) + trigger = _trigger_text(kwargs, result) + cycle_id = f"auto-learn-{int(time.time())}" + reasoning = { + "summary": f"Auto-recorded from {operation}", + "decision_rationale": ( + trigger[:500] + if trigger + else f"Successful {operation} without explicit record_reasoning" + ), + "fabric_elements_considered": [f"operation:{operation}", f"swarm:{swarm_id}"], + "structural_pattern_matched": "mcp-auto-learning-loop", + "expected_lift_signal": 0.08, + "program_id": program_id, + "llm_mode": "auto_absorb", + "auto_learning": True, + "operation": operation, + } + evidence = _extract_evidence(operation, result) + if evidence: + reasoning["evidence"] = evidence + return recorder.record_parent_fabric_reasoning(cycle_id=cycle_id, reasoning=reasoning) + except Exception: + logger.debug("auto_record_reasoning failed for %s", operation, exc_info=True) + return None + + +def _extract_evidence(operation: str, result: dict[str, Any]) -> dict[str, Any]: + evidence: dict[str, Any] = {} + nested = result.get("result") + if operation == "external_parent_decision" and isinstance(nested, dict): + decision = nested.get("decision") or {} + if isinstance(decision, dict): + evidence["directive"] = decision.get("directive", "") + evidence["session_id"] = nested.get("session_id") or decision.get( + "multiverse_session_id", "" + ) + fabric = nested.get("fabric_reasoning") or {} + if isinstance(fabric, dict) and fabric.get("evidence"): + evidence.update(dict(fabric["evidence"])) + elif operation == "multiverse_parent_decision" and isinstance(nested, dict): + evidence["collapsed"] = nested.get("collapsed_branch_id", "") + evidence["session_id"] = nested.get("session_id", "") + elif operation == "think" and isinstance(nested, dict): + gaps = nested.get("gaps") or [] + if gaps: + evidence["gaps"] = gaps[:3] + if nested.get("answer"): + evidence["answer_snippet"] = str(nested["answer"])[:300] + elif result.get("trace_slug"): + evidence["trace_slug"] = result["trace_slug"] + return evidence + + +def _build_skill_body( + operation: str, + kwargs: dict[str, Any], + result: dict[str, Any], +) -> tuple[str, str]: + from agentdrive.learning.skill_naming import learned_skill_title + + trigger = _trigger_text(kwargs, result) + project_id = str(kwargs.get("project_id") or result.get("project_id") or "") + intent = str(kwargs.get("intent") or kwargs.get("task") or "") + title = learned_skill_title( + operation, + trigger=trigger, + project_id=project_id, + intent=intent, + ) + lines = [ + f"# {title}", + "", + "Auto-learned playbook from a successful AgentDrive MCP/CLI session.", + "", + "## When to use", + f"- Task resembles: {trigger[:200]}" + if trigger + else f"- Running `{operation}` with similar inputs", + f"- Operation: `{operation}`", + "", + "## Steps", + "1. Call `experience_graph_get_context_pack` before acting.", + ] + + nested = result.get("result") + if operation == "external_parent_decision" and isinstance(nested, dict): + decision = nested.get("decision") or {} + directive = decision.get("directive", "") if isinstance(decision, dict) else "" + session_id = nested.get("session_id", "") + if directive: + lines.append(f"2. Follow collapsed directive: {directive}") + if session_id: + lines.append(f"3. Ground in multiverse session `{session_id}`.") + lines.append("4. Re-verify with live evidence before shipping.") + elif operation == "multiverse_parent_decision" and isinstance(nested, dict): + collapsed = nested.get("collapsed_branch_id", "") + if collapsed: + lines.append(f"2. Prefer collapsed branch `{collapsed}`.") + lines.append("3. Record reasoning or call external_parent_decision for MCP Parent path.") + elif operation == "experience_graph_record_reasoning": + summary = str( + (kwargs.get("reasoning") or {}).get("summary") + or kwargs.get("summary") + or result.get("trace_slug") + or "" + )[:300] + if summary: + lines.append(f"2. Prior rationale: {summary}") + lines.append("3. Extend graph with new structural traces on similar tasks.") + elif operation == "think" and isinstance(nested, dict): + answer = str(nested.get("answer", ""))[:400] + if answer: + lines.append(f"2. Prior synthesis: {answer}") + gaps = nested.get("gaps") or [] + if gaps: + lines.append(f"3. Known gaps to close: {gaps[0]}") + elif operation in _CODEBASE_OPS: + project_id = str(kwargs.get("project_id") or result.get("project_id") or "") + framework = result.get("framework") or {} + mimic = result.get("mimicry_prompt") or "" + motors = result.get("motor_programs") or [] + mirror = result.get("mirror_neurons") or {} + if project_id: + lines.append( + f"2. Project `{project_id}` — mirror-neuron mimicry (observe → fire → write)." + ) + if mimic: + lines.append(f"3. Mimicry brief:\n{mimic[:500]}") + elif motors: + lines.append(f"3. Motor programs: {[m.get('name') for m in motors[:3]]}") + elif mirror.get("motors_fired"): + lines.append(f"3. Mirror neurons fired: {mirror.get('motors_fired')}") + patterns = framework.get("patterns") if isinstance(framework, dict) else None + if isinstance(patterns, list) and patterns: + for pat in patterns[:3]: + lines.append(f" - {pat.get('rule', '')}") + lines.append( + "4. Use `codebase_mimic` before writing; `codebase_transform_style` after drafting." + ) + else: + lines.append(f"2. Run `{operation}` with the same swarm/program attribution.") + lines.append("3. Call `experience_graph_record_reasoning` for non-trivial forks.") + + lines.extend( + [ + "", + "## Verification", + "- Confirm `success: true` on the operation JSON.", + "- Check Experience Graph context pack for contradictions.", + ] + ) + description = f"Auto-learned from {operation}" + (f": {trigger[:120]}" if trigger else "") + return description[:1024], "\n".join(lines) + + +def _distill_and_install_skill( + operation: str, + kwargs: dict[str, Any], + result: dict[str, Any], + swarm_id: str, + program_id: str, +) -> dict[str, Any]: + from agentdrive.learning.skill_naming import learned_skill_name + from agentdrive.skills.registry import install_inherited_skill + + trigger = _trigger_text(kwargs, result) + project_id = str(kwargs.get("project_id") or result.get("project_id") or "") + intent = str(kwargs.get("intent") or kwargs.get("task") or "") + name = learned_skill_name( + operation, + trigger=trigger, + project_id=project_id, + intent=intent, + ) + description, body = _build_skill_body(operation, kwargs, result) + tags = ["learned", "auto-learned", "mcp-parent", operation.replace("_", "-")] + if project_id: + tags.append(project_id) + if program_id and program_id != _MCP_SUBAGENT_ID: + tags.append(program_id) + + when_to_call = ( + f"Task resembles: {intent or trigger[:200]}" + if (intent or trigger) + else f"After successful {operation.replace('_', ' ')} on {project_id or swarm_id}" + ) + path = install_inherited_skill( + name=name, + description=description, + body=body, + tags=tags, + operation=operation, + when_to_call=when_to_call, + swarm_id=swarm_id, + source_subagent_id=_MCP_SUBAGENT_ID, + update_existing=True, + ) + + try: + from agentdrive.skills.usage import record_skill_run + + source = f"mcp-auto-learning:{swarm_id}" + record_skill_run(name, success=True, source=source) + record_skill_run(name, success=True, source=source) + except Exception: + logger.debug("Failed to record auto-learned skill usage for %s", name, exc_info=True) + + promoted = False + genome_id: str | None = None + if operation in _HIGH_SIGNAL_OPS and auto_assimilate_enabled(): + promoted, genome_id = _promote_and_ingest(name, swarm_id) + + return { + "name": name, + "path": str(path), + "promoted": promoted, + "genome_id": genome_id, + "operation": operation, + } + + +def _promote_and_ingest(skill_name: str, swarm_id: str) -> tuple[bool, str | None]: + try: + from agentdrive.drive.drive import get_default_drive + from agentdrive.skills.curation import ingest_skill_as_dna, promote_inherited_skill + + promote_inherited_skill(skill_name) + export = ingest_skill_as_dna(skill_name, target_drive=get_default_drive()) + return True, export.genome_id if export.accepted else export.genome_id + except Exception: + logger.debug("auto promote/ingest failed for %s", skill_name, exc_info=True) + try: + from agentdrive.drive.drive import get_default_drive + from agentdrive.skills.curation import assimilate_inherited_skills + + report = assimilate_inherited_skills( + target_drive=get_default_drive(), + ingest_dna=True, + skill_names=[skill_name], + ) + if report.dna_exports: + return True, report.dna_exports[0].genome_id + if report.promoted: + return True, None + except Exception: + logger.debug("fallback assimilate failed for %s", skill_name, exc_info=True) + return False, None + + +def maybe_absorb_operation_outcome( + operation: str, + kwargs: dict[str, Any], + result: dict[str, Any], +) -> dict[str, Any] | None: + """ + Post-handler hook: absorb experience + skills from a successful operation. + + Returns a summary dict for inclusion as ``auto_learning`` on the operation + result, or None when nothing was absorbed. + """ + if not auto_learn_enabled(): + return None + if not isinstance(result, dict) or not result.get("success"): + return None + if result.get("dry_run"): + return None + + swarm_id = _effective_swarm(kwargs, result) + program_id = _program_id(kwargs) + session = _get_session(swarm_id, program_id) + session.ops.append((operation, _op_summary(operation, kwargs, result))) + + if operation in _SESSION_START_OPS: + session.context_pack_pulled = True + if operation == "experience_graph_context_pack": + session.context_pack_pulled = True + if operation == "experience_graph_record_reasoning": + session.reasoning_recorded = True + + if operation in _CODEBASE_OPS: + project_id = str(kwargs.get("project_id") or result.get("project_id") or "") + if project_id and project_id not in session.pattern_projects: + session.pattern_projects.append(project_id) + + for skill_key in ("skill_name", "skill"): + skill_ref = kwargs.get(skill_key) + if isinstance(skill_ref, str) and skill_ref.strip(): + ref = skill_ref.strip() + if ref not in session.referenced_skills: + session.referenced_skills.append(ref) + + absorbed: dict[str, Any] = {"operation": operation, "swarm_id": swarm_id} + + if _should_auto_record_reasoning(operation, session, result): + trace = _auto_record_reasoning(operation, kwargs, result, swarm_id, program_id) + if trace: + absorbed["reasoning_trace"] = trace + session.reasoning_recorded = True + if trace not in session.experience_traces: + session.experience_traces.append(trace) + + if _should_distill_skill(operation, result): + skill_info = _distill_and_install_skill(operation, kwargs, result, swarm_id, program_id) + absorbed["skill"] = skill_info + skill_name = skill_info.get("name") + if skill_name and skill_name not in session.distilled_skills: + session.distilled_skills.append(skill_name) + + trigger = _trigger_text(kwargs, result) + if not session.fused_skill_name: + try: + from agentdrive.learning.skill_fusion import maybe_fuse_session + + fused = maybe_fuse_session(session, trigger=trigger, last_operation=operation) + if fused: + absorbed["fused_skill"] = fused + session.fused_skill_name = fused.get("name") + except Exception: + logger.debug("skill fusion hook failed for %s", operation, exc_info=True) + + try: + from agentdrive.memory.ingest import ingest_from_operation + + mem = ingest_from_operation( + operation, + kwargs, + result, + swarm_id=swarm_id, + program_id=program_id, + ) + if mem and not mem.get("skipped"): + absorbed["memory"] = mem + except Exception: + logger.debug("memory bank ingest failed for %s", operation, exc_info=True) + + if not session.growth_merged: + try: + from agentdrive.learning.growth_merge import maybe_merge_growth + + growth = maybe_merge_growth( + session, + trigger=trigger, + fused_skill=absorbed.get("fused_skill"), + last_operation=operation, + ) + if growth: + absorbed["growth_merge"] = growth + session.growth_merged = True + except Exception: + logger.debug("growth merge hook failed for %s", operation, exc_info=True) + + if len(absorbed) <= 2: + return None + return absorbed + + +def reset_sessions() -> None: + """Test helper — clear in-memory session tracking.""" + _SESSIONS.clear() diff --git a/src/agentdrive/learning/framework_skills.py b/src/agentdrive/learning/framework_skills.py new file mode 100644 index 0000000..37a11c5 --- /dev/null +++ b/src/agentdrive/learning/framework_skills.py @@ -0,0 +1,295 @@ +""" +Framework skill playbook — how AI agents use AgentDrive + learned skills on any task. + +When AgentDrive is the framework, models route work through: + 1. Session briefing (anchor + growth + skill matches) + 2. Matched learned/fused playbooks for the current task + 3. Skill invocation (read body or run bound operation) + 4. Write-back (auto-learning grows the bench) +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import Any + +from agentdrive.skills.compose import _score_skill +from agentdrive.skills.registry import SkillEntry, discover_skills, get_skill +from agentdrive.skills.runner import run_skill +from agentdrive.skills.usage import record_skill_match + +_TOKEN_RE = re.compile(r"[a-z0-9][a-z0-9_-]{2,}") + +_LEARNED_PREFIXES = ("learned-", "fused-") + +_FRAMEWORK_WORKFLOW = """## AgentDrive framework loop (use on every task) + +1. **Brief** — `framework_session_start` or `growth_merge_briefing` + `framework_skill_route` +2. **Route** — pick matched `learned-*` / `fused-*` skills for this task (below) +3. **Ground** — `experience_graph_get_context_pack` + apply skill playbook steps +4. **Execute** — run bound ops via `framework_skill_run` or follow SKILL.md body +5. **Write-back** — `experience_graph_record_reasoning` + `record_outcome` on completion + +Learned skills compound automatically — check `auto_learning` on every `run_operation` result. +""" + + +@dataclass +class FrameworkSkillMatch: + name: str + description: str + score: float + kind: str # learned | fused | inherited | bundled + project: str + when_to_call: str + has_operation: bool + operation: str | None + excerpt: str + invoke_hint: str + + def to_dict(self) -> dict[str, Any]: + return { + "name": self.name, + "description": self.description, + "score": self.score, + "kind": self.kind, + "project": self.project, + "when_to_call": self.when_to_call, + "has_operation": self.has_operation, + "operation": self.operation, + "excerpt": self.excerpt, + "invoke_hint": self.invoke_hint, + } + + +def _skill_kind(name: str, tags: tuple[str, ...]) -> str: + if name.startswith("fused-"): + return "fused" + if name.startswith("learned-"): + return "learned" + if "fused" in tags: + return "fused" + if "learned" in tags or "auto-learned" in tags: + return "learned" + if "inherited" in tags: + return "inherited" + return "bundled" + + +def _project_from_skill(entry: SkillEntry) -> str: + for tag in entry.tags: + if tag and not tag.startswith(("auto-", "mcp-", "learned", "fused", "inherited")): + if "-" in tag or tag.isalnum(): + return tag + parts = entry.name.split("-") + if entry.name.startswith("learned-") and len(parts) >= 2: + return parts[1] + if entry.name.startswith("fused-") and len(parts) >= 2: + return parts[1] + return "" + + +def _framework_boost( + entry: SkillEntry, + *, + swarm_id: str, + project_id: str, +) -> float: + boost = 0.0 + kind = _skill_kind(entry.name, entry.tags) + if kind == "fused": + boost += 8.0 + elif kind == "learned": + boost += 5.0 + elif kind == "inherited": + boost += 2.0 + + skill_project = _project_from_skill(entry) + if project_id and ( + project_id in entry.name or project_id in entry.tags or skill_project == project_id + ): + boost += 6.0 + if swarm_id and swarm_id in str(entry.path): + boost += 3.0 + if entry.operation: + boost += 1.5 + return boost + + +def _invoke_hint(entry: SkillEntry) -> str: + if entry.operation: + arg = f' arg="{entry.argument}"' if entry.argument else "" + return ( + f"framework_skill_run(name={entry.name!r}{arg}) or run_operation({entry.operation!r})" + ) + return f"Read SKILL.md body for `{entry.name}` and follow the playbook steps." + + +def _excerpt(body: str, *, limit: int = 420) -> str: + text = body.strip() + if len(text) <= limit: + return text + return text[: limit - 1] + "…" + + +def list_framework_skills( + *, + swarm_id: str = "", + learned_only: bool = False, +) -> list[SkillEntry]: + entries: list[SkillEntry] = [] + for entry in discover_skills(): + kind = _skill_kind(entry.name, entry.tags) + if learned_only and kind not in ("learned", "fused"): + continue + if swarm_id and swarm_id not in str(entry.path) and kind == "bundled": + continue + entries.append(entry) + return sorted(entries, key=lambda item: item.name) + + +def route_skills_for_task( + task: str, + *, + swarm_id: str = "", + project_id: str = "", + limit: int = 5, + learned_only: bool = False, + record_matches: bool = True, +) -> list[FrameworkSkillMatch]: + """Rank skills for the current task — prioritizes learned/fused playbooks.""" + query = task.strip() + if not query: + query = project_id or "agentdrive session" + + scored: list[tuple[float, SkillEntry]] = [] + for entry in discover_skills(): + kind = _skill_kind(entry.name, entry.tags) + if learned_only and kind not in ("learned", "fused"): + continue + if entry.harness not in ("agentdrive", "universal", ""): + continue + total = _score_skill(entry, query, role=None) + _framework_boost( + entry, swarm_id=swarm_id, project_id=project_id + ) + if total > 0: + scored.append((total, entry)) + + scored.sort(key=lambda pair: (-pair[0], pair[1].name)) + results: list[FrameworkSkillMatch] = [] + for score, entry in scored[:limit]: + kind = _skill_kind(entry.name, entry.tags) + match = FrameworkSkillMatch( + name=entry.name, + description=entry.description, + score=round(score, 2), + kind=kind, + project=_project_from_skill(entry), + when_to_call=entry.when_to_call or entry.description, + has_operation=bool(entry.operation), + operation=entry.operation, + excerpt=_excerpt(entry.body), + invoke_hint=_invoke_hint(entry), + ) + results.append(match) + if record_matches: + try: + record_skill_match(entry.name, score=score) + except Exception: + pass + return results + + +def format_skill_playbook(matches: list[FrameworkSkillMatch]) -> str: + if not matches: + return "No learned skills matched this task yet. AgentDrive will grow skills as you work." + + lines = ["## Matched skills for this task"] + for match in matches: + lines.append( + f"\n### {match.name} ({match.kind}, score {match.score})\n" + f"**When:** {match.when_to_call}\n" + f"**Invoke:** {match.invoke_hint}\n" + f"{match.excerpt}" + ) + return "\n".join(lines) + + +def build_framework_session_pack( + task: str = "", + *, + swarm_id: str, + project_id: str = "", + skill_limit: int = 5, +) -> dict[str, Any]: + """Unified framework opening pack for any AgentDrive session.""" + from agentdrive.learning.growth_merge import build_growth_briefing + from agentdrive.memory.anchor import build_session_anchor + + vault = project_id or None + anchor = build_session_anchor(swarm_id, vault=vault, query=task) + growth = build_growth_briefing(swarm_id, query=task, limit=skill_limit) + matches = route_skills_for_task( + task, + swarm_id=swarm_id, + project_id=project_id, + limit=skill_limit, + ) + learned_bench = list_framework_skills(swarm_id=swarm_id, learned_only=True) + + playbook = format_skill_playbook(matches) + framework_briefing = ( + f"{_FRAMEWORK_WORKFLOW}\n\n" + f"{anchor.get('anchor_text', '')}\n\n" + f"{playbook}\n\n" + f"## Growth context\n" + f"{growth.get('growth_briefing', '')[:2500]}" + )[:8000] + + return { + "swarm_id": swarm_id, + "task": task, + "project_id": project_id, + "framework_workflow": _FRAMEWORK_WORKFLOW, + "anchor": anchor, + "growth_briefing": growth, + "matched_skills": [m.to_dict() for m in matches], + "learned_skill_count": len(learned_bench), + "learned_skills": [ + { + "name": e.name, + "description": e.description[:120], + "kind": _skill_kind(e.name, e.tags), + } + for e in learned_bench[:20] + ], + "framework_briefing": framework_briefing, + } + + +def run_framework_skill( + name: str, + *, + arg: str = "", + swarm_id: str = "", +) -> dict[str, Any]: + """Run a matched skill and attach swarm context to bound operations.""" + entry = get_skill(name) + if entry is None: + return { + "success": False, + "error": f"Unknown skill: {name}", + "operation": "framework_skill_run", + } + + result = run_skill(name, arg, swarm_id=swarm_id) + payload = { + "success": bool(result.get("success")), + "operation": "framework_skill_run", + "skill": name, + "result": result, + } + if entry.operation: + payload["bound_operation"] = entry.operation + return payload diff --git a/src/agentdrive/learning/growth_merge.py b/src/agentdrive/learning/growth_merge.py new file mode 100644 index 0000000..2f62c7d --- /dev/null +++ b/src/agentdrive/learning/growth_merge.py @@ -0,0 +1,498 @@ +""" +Growth merge — experience + pattern recognition + memory compounding. + +When AgentDrive work spans structural experience, codebase patterns, and +distilled skills, this module recognizes recurring shapes and merges them into +one growth artifact: compound memory, relations, and optional born skills. + +Same integration pattern as the Memory Bank layer: native naming, auto-ingest, +scoped vault/topic storage, and queryable briefings — not a port of external metaphors. +""" + +from __future__ import annotations + +import logging +import os +import re +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from agentdrive.learning.auto_absorb import LearningSession + +logger = logging.getLogger(__name__) + +_GROWTH_VAULT = "growth" +_GROWTH_TOPIC = "merge" +_TOKEN_RE = re.compile(r"[a-z0-9][a-z0-9_-]{2,}") + +_EXPERIENCE_OPS = frozenset( + { + "experience_graph_context_pack", + "experience_graph_record_reasoning", + "think", + "external_parent_decision", + "multiverse_parent_decision", + "multiverse_run_full", + "record_outcome", + "learnings_log", + } +) + +_PATTERN_OPS = frozenset( + { + "codebase_observe_file", + "codebase_patterns_profile", + "codebase_mimic", + "codebase_transform_style", + "codebase_mirror_resonance", + "codebase_patterns_match", + } +) + + +def _flag(name: str, default: str = "1") -> bool: + raw = os.environ.get(name, default).strip().lower() + return raw not in {"0", "false", "no", "off"} + + +def growth_merge_enabled() -> bool: + return _flag("AGENTDRIVE_AUTO_GROWTH_MERGE", "1") and _flag("AGENTDRIVE_AUTO_LEARN", "1") + + +@dataclass +class GrowthAxes: + """Which compounding surfaces contributed to a growth merge.""" + + experience: bool = False + patterns: bool = False + skills: bool = False + memory: bool = False + + def present(self) -> set[str]: + axes: set[str] = set() + if self.experience: + axes.add("experience") + if self.patterns: + axes.add("patterns") + if self.skills: + axes.add("skills") + if self.memory: + axes.add("memory") + return axes + + def merge_ready(self) -> bool: + return len(self.present()) >= 2 + + +@dataclass +class RecognizedPattern: + """A recurring structural signal detected across growth surfaces.""" + + source: str + label: str + score: float + evidence: str + link_type: str = "pattern" + link_id: str = "" + + def to_dict(self) -> dict[str, Any]: + return { + "source": self.source, + "label": self.label, + "score": self.score, + "evidence": self.evidence, + "link_type": self.link_type, + "link_id": self.link_id, + } + + +@dataclass +class GrowthMergeRecord: + """Unified growth artifact from a compounding session.""" + + trigger: str + swarm_id: str + program_id: str + axes: GrowthAxes + operations: list[str] = field(default_factory=list) + experience_traces: list[str] = field(default_factory=list) + pattern_projects: list[str] = field(default_factory=list) + source_skills: list[str] = field(default_factory=list) + recognized_patterns: list[RecognizedPattern] = field(default_factory=list) + memory_hits: list[dict[str, Any]] = field(default_factory=list) + fused_skill: str | None = None + memory_id: str | None = None + + def to_dict(self) -> dict[str, Any]: + return { + "trigger": self.trigger, + "swarm_id": self.swarm_id, + "program_id": self.program_id, + "axes": sorted(self.axes.present()), + "operations": self.operations, + "experience_traces": self.experience_traces, + "pattern_projects": self.pattern_projects, + "source_skills": self.source_skills, + "recognized_patterns": [p.to_dict() for p in self.recognized_patterns], + "memory_hits": self.memory_hits, + "fused_skill": self.fused_skill, + "memory_id": self.memory_id, + } + + +def axes_from_session(session: LearningSession) -> GrowthAxes: + ops = [op for op, _ in session.ops] + return GrowthAxes( + experience=bool(session.experience_traces) or any(op in _EXPERIENCE_OPS for op in ops), + patterns=bool(session.pattern_projects) or any(op in _PATTERN_OPS for op in ops), + skills=bool(session.distilled_skills or session.referenced_skills), + memory=bool(session.fused_skill_name), + ) + + +def _tokenize(text: str) -> set[str]: + return set(_TOKEN_RE.findall(text.lower())) + + +def _recognize_from_memory( + swarm_id: str, + trigger: str, + *, + limit: int = 5, +) -> tuple[list[RecognizedPattern], list[dict[str, Any]]]: + from agentdrive.memory.store import MemoryBankStore + + if not trigger.strip(): + return [], [] + + store = MemoryBankStore(swarm_id) + hits = store.search(trigger, limit=limit) + patterns: list[RecognizedPattern] = [] + memory_hits: list[dict[str, Any]] = [] + trigger_tokens = _tokenize(trigger) + + for hit in hits: + memory_hits.append( + { + "memory_id": hit.memory_id, + "kind": hit.kind, + "title": hit.title, + "vault": hit.vault, + "topic": hit.topic, + } + ) + haystack = _tokenize(f"{hit.title} {hit.content} {' '.join(hit.tags)}") + overlap = len(trigger_tokens & haystack) + if overlap == 0: + continue + score = min(1.0, overlap / max(len(trigger_tokens), 1)) + patterns.append( + RecognizedPattern( + source="memory_bank", + label=hit.title[:80], + score=round(score, 3), + evidence=f"{overlap} token overlap with prior {hit.kind}", + link_type="memory", + link_id=hit.memory_id, + ) + ) + return patterns, memory_hits + + +def _recognize_from_codebase(project_ids: list[str]) -> list[RecognizedPattern]: + from agentdrive.codebase.framework import get_writing_guide + + patterns: list[RecognizedPattern] = [] + for project_id in project_ids[:3]: + try: + framework = get_writing_guide(project_id) + except Exception: + logger.debug("growth merge framework load failed for %s", project_id, exc_info=True) + continue + for pat in (framework.get("patterns") or [])[:4]: + if not isinstance(pat, dict): + continue + rule = str(pat.get("rule") or "") + category = str(pat.get("category") or "pattern") + if not rule: + continue + patterns.append( + RecognizedPattern( + source="codebase", + label=f"{project_id}:{category}", + score=0.75, + evidence=rule[:200], + link_type="codebase_project", + link_id=project_id, + ) + ) + return patterns + + +def _recognize_from_experience( + swarm_id: str, + trigger: str, + traces: list[str], +) -> list[RecognizedPattern]: + patterns: list[RecognizedPattern] = [] + for trace in traces[:5]: + patterns.append( + RecognizedPattern( + source="experience_graph", + label=trace[:80], + score=0.82, + evidence="Prior fabric reasoning trace in this session", + link_type="fabric_trace", + link_id=trace, + ) + ) + + element = _TOKEN_RE.sub("-", trigger.lower()).strip("-")[:40] or "growth-merge" + try: + from agentdrive.operations.registry import _integrated_recorder + + _, recorder = _integrated_recorder(swarm_id) + matches = recorder.find_structural_similarities(element, lookback=8, min_similarity=0.55) + for match in matches[:4]: + patterns.append( + RecognizedPattern( + source="experience_graph", + label=str(match.get("matched_element") or "structural_match"), + score=float(match.get("similarity") or 0.6), + evidence=str(match.get("evidence") or "structural similarity"), + link_type="fabric_element", + link_id=str(match.get("cycle_id") or element), + ) + ) + except Exception: + logger.debug("structural similarity recognition skipped", exc_info=True) + + return patterns + + +def recognize_growth_patterns( + *, + swarm_id: str, + trigger: str, + session: LearningSession | None = None, + experience_traces: list[str] | None = None, + pattern_projects: list[str] | None = None, +) -> list[RecognizedPattern]: + """Detect recurring shapes across memory, codebase, and experience surfaces.""" + recognized: list[RecognizedPattern] = [] + traces = list(experience_traces or []) + projects = list(pattern_projects or []) + if session is not None: + traces = list(dict.fromkeys(traces + session.experience_traces)) + projects = list(dict.fromkeys(projects + session.pattern_projects)) + + mem_patterns, _ = _recognize_from_memory(swarm_id, trigger) + recognized.extend(mem_patterns) + recognized.extend(_recognize_from_codebase(projects)) + recognized.extend(_recognize_from_experience(swarm_id, trigger, traces)) + + seen: set[str] = set() + unique: list[RecognizedPattern] = [] + for item in sorted(recognized, key=lambda p: p.score, reverse=True): + key = f"{item.source}|{item.label}|{item.link_id}" + if key in seen: + continue + seen.add(key) + unique.append(item) + return unique[:12] + + +def _record_growth_relations( + record: GrowthMergeRecord, +) -> list[str]: + from agentdrive.memory.relations import MemoryRelationGraph + + graph = MemoryRelationGraph(record.swarm_id) + relation_ids: list[str] = [] + subject = record.trigger[:80] or "growth-session" + + for pattern in record.recognized_patterns[:6]: + if not pattern.link_id: + continue + rel = graph.record( + subject, + "recognizes", + pattern.label[:120], + memory_id=record.memory_id, + ) + relation_ids.append(rel.relation_id) + return relation_ids + + +def _store_growth_memory(record: GrowthMergeRecord) -> str | None: + from agentdrive.memory.store import MemoryBankStore + + axes = ", ".join(sorted(record.axes.present())) + lines = [ + f"Growth merge for: {record.trigger}", + f"Axes compounded: {axes}", + f"Operations: {', '.join(record.operations[:10])}", + ] + if record.experience_traces: + lines.append(f"Experience traces: {', '.join(record.experience_traces[:5])}") + if record.pattern_projects: + lines.append(f"Pattern projects: {', '.join(record.pattern_projects)}") + if record.source_skills: + lines.append(f"Skills merged: {', '.join(record.source_skills)}") + if record.fused_skill: + lines.append(f"Born skill: {record.fused_skill}") + + lines.append("") + lines.append("Recognized patterns:") + for pat in record.recognized_patterns[:8]: + lines.append(f"- [{pat.source}] {pat.label} ({pat.score:.0%}): {pat.evidence[:120]}") + + content = "\n".join(lines).strip() + title = f"Growth merge: {record.trigger[:80]}" if record.trigger else "Growth merge" + store = MemoryBankStore(record.swarm_id) + if store.has_similar(title, content): + return None + + links: list[dict[str, str]] = [] + for pat in record.recognized_patterns[:6]: + if pat.link_id: + links.append({"type": pat.link_type, "id": pat.link_id}) + if record.fused_skill: + links.append({"type": "skill", "id": record.fused_skill}) + + entry = store.store( + kind="insight", + title=title, + content=content, + confidence=0.88, + source="growth_merge", + program_id=record.program_id, + tags=["growth-merge", *sorted(record.axes.present())], + links=links, + vault=_GROWTH_VAULT, + topic=_GROWTH_TOPIC, + preserves_source=False, + ) + return entry.memory_id + + +def merge_session_growth( + session: LearningSession, + *, + trigger: str, + fused_skill: dict[str, Any] | None = None, +) -> GrowthMergeRecord | None: + """Merge experience, patterns, and skills into one growth artifact.""" + if not growth_merge_enabled(): + return None + + axes = axes_from_session(session) + if fused_skill: + axes.memory = True + if not axes.merge_ready(): + return None + + ops = [op for op, _ in session.ops] + source_skills = list(dict.fromkeys(session.distilled_skills + session.referenced_skills)) + recognized = recognize_growth_patterns( + swarm_id=session.swarm_id, + trigger=trigger, + session=session, + ) + _, memory_hits = _recognize_from_memory(session.swarm_id, trigger) + + record = GrowthMergeRecord( + trigger=trigger, + swarm_id=session.swarm_id, + program_id=session.program_id, + axes=axes, + operations=ops, + experience_traces=list(session.experience_traces), + pattern_projects=list(session.pattern_projects), + source_skills=source_skills, + recognized_patterns=recognized, + memory_hits=memory_hits, + fused_skill=(fused_skill or {}).get("name"), + ) + + memory_id = _store_growth_memory(record) + if not memory_id: + return None + record.memory_id = memory_id + _record_growth_relations(record) + return record + + +def build_growth_briefing( + swarm_id: str, + *, + query: str = "", + limit: int = 8, +) -> dict[str, Any]: + """Unified growth briefing: experience fabric + pattern recognition + memory bank.""" + from agentdrive.memory.briefing import build_deep_briefing + from agentdrive.memory.store import MemoryBankStore + + deep = build_deep_briefing(swarm_id, query=query, memory_limit=limit) + trigger = query or "session growth" + recognized = recognize_growth_patterns(swarm_id=swarm_id, trigger=trigger) + + growth_memories = MemoryBankStore(swarm_id).search( + query or "growth merge", + limit=limit, + vault=_GROWTH_VAULT, + topic=_GROWTH_TOPIC, + ) + + pattern_lines = [ + f"- [{p.source}] {p.label} ({p.score:.0%}): {p.evidence[:100]}" for p in recognized[:6] + ] + growth_section = ( + "\n".join(pattern_lines) if pattern_lines else "No cross-surface patterns detected yet." + ) + + return { + "swarm_id": swarm_id, + "query": query, + "axes_integrated": ["experience_graph", "codebase_patterns", "skills", "memory_bank"], + "recognized_patterns": [p.to_dict() for p in recognized], + "growth_memories": [m.to_dict() for m in growth_memories], + "fabric_context_pack": deep.get("fabric_context_pack"), + "memory_bank": deep.get("memory_bank"), + "growth_briefing": ( + "## Growth merge (experience + patterns + memory)\n" + f"{growth_section}\n\n" + "## Structural memory\n" + f"{deep.get('deep_briefing', '')[:3000]}" + )[:6000], + "pattern_count": len(recognized), + "growth_memory_count": len(growth_memories), + } + + +def maybe_merge_growth( + session: LearningSession, + *, + trigger: str, + fused_skill: dict[str, Any] | None = None, + last_operation: str, +) -> dict[str, Any] | None: + """Post-absorb hook: compound session growth when multiple axes are present.""" + if not growth_merge_enabled(): + return None + terminal = last_operation in { + "external_parent_decision", + "multiverse_parent_decision", + "codebase_mimic", + "think", + "record_outcome", + "experience_graph_record_reasoning", + "synthesize_fused_skill", + } + if not terminal and len(session.ops) < 3: + return None + record = merge_session_growth(session, trigger=trigger, fused_skill=fused_skill) + if record is None: + return None + return record.to_dict() diff --git a/src/agentdrive/learning/skill_fusion.py b/src/agentdrive/learning/skill_fusion.py new file mode 100644 index 0000000..a95e0b5 --- /dev/null +++ b/src/agentdrive/learning/skill_fusion.py @@ -0,0 +1,409 @@ +""" +Skill fusion — experience + skills + patterns → a completely new skill. + +When an MCP/CLI session accumulates structural experience (graph traces), +distilled playbooks, and codebase pattern signals, this module merges them +into one born skill — not a copy of any parent, but a synthesis of the +session's lived AgentDrive work. +""" + +from __future__ import annotations + +import json +import logging +import os +import re +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from agentdrive.learning.auto_absorb import LearningSession + +logger = logging.getLogger(__name__) + +_SLUG_RE = re.compile(r"[^a-z0-9]+") +_MAX_SKILL_NAME = 64 + +_EXPERIENCE_OPS = frozenset( + { + "experience_graph_context_pack", + "experience_graph_record_reasoning", + "think", + "external_parent_decision", + "multiverse_parent_decision", + "multiverse_run_full", + "record_outcome", + "learnings_log", + } +) + +_PATTERN_OPS = frozenset( + { + "codebase_observe_file", + "codebase_patterns_profile", + "codebase_mimic", + "codebase_transform_style", + "codebase_mirror_resonance", + "codebase_patterns_match", + } +) + + +def _flag(name: str, default: str = "1") -> bool: + raw = os.environ.get(name, default).strip().lower() + return raw not in {"0", "false", "no", "off"} + + +def auto_fuse_skills_enabled() -> bool: + return _flag("AGENTDRIVE_AUTO_FUSE_SKILLS", "1") and _flag("AGENTDRIVE_AUTO_LEARN", "1") + + +def _slugify(text: str, *, max_len: int = 32) -> str: + slug = _SLUG_RE.sub("-", text.lower()).strip("-") + if len(slug) > max_len: + slug = slug[:max_len].rstrip("-") + return slug or "session" + + +@dataclass +class FusionLineage: + """Provenance for a born skill — what merged to create it.""" + + trigger: str + swarm_id: str + program_id: str + operations: list[str] = field(default_factory=list) + experience_traces: list[str] = field(default_factory=list) + source_skills: list[str] = field(default_factory=list) + pattern_projects: list[str] = field(default_factory=list) + + def axes_present(self) -> set[str]: + axes: set[str] = set() + if self.experience_traces or any(op in _EXPERIENCE_OPS for op in self.operations): + axes.add("experience") + if self.source_skills: + axes.add("skills") + if self.pattern_projects or any(op in _PATTERN_OPS for op in self.operations): + axes.add("patterns") + return axes + + def fusion_ready(self) -> bool: + return len(self.axes_present()) >= 2 and len(self.operations) >= 2 + + +def lineage_from_session(session: LearningSession, trigger: str) -> FusionLineage: + ops = [op for op, _ in session.ops] + return FusionLineage( + trigger=trigger, + swarm_id=session.swarm_id, + program_id=session.program_id, + operations=ops, + experience_traces=list(session.experience_traces), + source_skills=list(dict.fromkeys(session.distilled_skills + session.referenced_skills)), + pattern_projects=list(dict.fromkeys(session.pattern_projects)), + ) + + +def _fused_skill_name(lineage: FusionLineage) -> str: + from agentdrive.learning.skill_naming import fused_skill_name + + return fused_skill_name( + trigger=lineage.trigger, + pattern_projects=lineage.pattern_projects, + axes=lineage.axes_present(), + ) + + +def _load_skill_excerpts(skill_names: list[str], *, limit: int = 3) -> list[dict[str, str]]: + from agentdrive.skills.registry import get_skill + + excerpts: list[dict[str, str]] = [] + for name in skill_names[:limit]: + entry = get_skill(name) + if entry is None: + continue + body = entry.body.strip() + if len(body) > 600: + body = body[:599] + "…" + excerpts.append( + { + "name": entry.name, + "description": entry.description[:200], + "body": body, + } + ) + return excerpts + + +def _load_pattern_excerpts(project_ids: list[str], *, limit: int = 2) -> list[dict[str, Any]]: + from agentdrive.codebase.framework import get_writing_guide + + excerpts: list[dict[str, Any]] = [] + for pid in project_ids[:limit]: + try: + framework = get_writing_guide(pid) + except Exception: + logger.debug("Failed to load framework for %s", pid, exc_info=True) + continue + patterns = framework.get("patterns") or [] + excerpts.append( + { + "project_id": pid, + "patterns": [ + {"category": p.get("category"), "rule": p.get("rule")} + for p in patterns[:5] + if isinstance(p, dict) + ], + "summary": framework.get("summary") or {}, + } + ) + return excerpts + + +def build_fused_skill_body(lineage: FusionLineage) -> tuple[str, str]: + """Return (description, markdown body) for a born skill.""" + title = lineage.trigger[:80] if lineage.trigger else "AgentDrive fused playbook" + axes = sorted(lineage.axes_present()) + description = ( + f"Born skill — fused from AgentDrive {', '.join(axes)} " + f"({len(lineage.operations)} ops in session)" + )[:1024] + + lines = [ + f"# {title}", + "", + "A **born skill** — not copied from any single parent. Experience traces,", + "distilled playbooks, and codebase patterns from this AgentDrive session were", + "merged into one integrated playbook.", + "", + "## Lineage", + f"- **Axes merged:** {', '.join(axes)}", + f"- **Operations:** {', '.join(lineage.operations[:12])}", + ] + if lineage.experience_traces: + lines.append(f"- **Experience traces:** {', '.join(lineage.experience_traces[:5])}") + if lineage.source_skills: + lines.append(f"- **Parent skills merged:** {', '.join(lineage.source_skills)}") + if lineage.pattern_projects: + lines.append(f"- **Pattern projects:** {', '.join(lineage.pattern_projects)}") + + lines.extend(["", "## When to use"]) + if lineage.trigger: + lines.append(f"- Task resembles: {lineage.trigger[:240]}") + lines.append("- You need the combined grounding from experience + skills + repo patterns.") + lines.append("- Prior session work on this swarm already proved the path.") + + lines.extend(["", "## Synthesized playbook"]) + + if "experience" in axes: + lines.append("### From experience (Experience Graph)") + lines.append("1. Call `experience_graph_get_context_pack` — inherit structural memory.") + if lineage.experience_traces: + lines.append(f"2. Ground in traces: `{lineage.experience_traces[0]}`.") + if any( + op.startswith("multiverse") or op == "external_parent_decision" + for op in lineage.operations + ): + lines.append( + "3. For competing paths, use `external_parent_decision` or `multiverse_parent_decision`." + ) + else: + lines.append("3. Record non-trivial forks with `experience_graph_record_reasoning`.") + + skill_excerpts = _load_skill_excerpts(lineage.source_skills) + if skill_excerpts: + lines.append("") + lines.append("### From merged skills") + for ex in skill_excerpts: + lines.append(f"**{ex['name']}** — {ex['description']}") + if ex["body"]: + snippet = ex["body"].split("\n")[0][:120] + lines.append(f"> {snippet}") + + pattern_excerpts = _load_pattern_excerpts(lineage.pattern_projects) + if pattern_excerpts: + lines.append("") + lines.append("### From codebase patterns (mirror neurons)") + for ex in pattern_excerpts: + lines.append(f"**Project `{ex['project_id']}`**") + for pat in ex.get("patterns") or []: + lines.append(f"- [{pat.get('category')}] {pat.get('rule')}") + lines.append( + "- Use `codebase_mimic` before writing; `codebase_transform_style` after drafting." + ) + + lines.extend( + [ + "", + "### Integrated execution", + "1. Pull graph context → match fused lineage above.", + "2. Invoke merged skill steps where they apply; do not treat them as isolated.", + "3. Write code through project patterns when a repo is in scope.", + "4. Close the loop: `record_outcome` or `experience_graph_record_reasoning`.", + "", + "## Verification", + "- Contradictions checked against latest context pack.", + "- Born skill supersedes individual parent skills for this trigger class.", + ] + ) + + return description, "\n".join(lines) + + +def synthesize_fused_skill( + lineage: FusionLineage, + *, + promote: bool = False, +) -> dict[str, Any]: + """Install a born skill from fusion lineage. Returns install metadata.""" + if not lineage.fusion_ready(): + raise ValueError( + "Fusion requires at least two axes (experience, skills, patterns) " + f"and 2+ operations; got axes={lineage.axes_present()}, " + f"ops={len(lineage.operations)}" + ) + + from agentdrive.skills.registry import install_inherited_skill + + name = _fused_skill_name(lineage) + description, body = build_fused_skill_body(lineage) + tags = [ + "fused", + "born-skill", + "auto-learned", + *sorted(lineage.axes_present()), + ] + if lineage.program_id: + tags.append(lineage.program_id) + + when_to_call = ( + f"Task resembles: {lineage.trigger[:200]}" + if lineage.trigger + else f"Session merged {', '.join(sorted(lineage.axes_present()))} for {lineage.pattern_projects[0] if lineage.pattern_projects else 'this swarm'}" + ) + path = install_inherited_skill( + name=name, + description=description, + body=body, + tags=tags, + operation="synthesize_fused_skill", + when_to_call=when_to_call, + swarm_id=lineage.swarm_id, + source_subagent_id="skill-fusion", + update_existing=True, + ) + + lineage_path = path.parent / "fusion-lineage.json" + lineage_path.write_text( + json.dumps( + { + "name": name, + "trigger": lineage.trigger, + "axes": sorted(lineage.axes_present()), + "operations": lineage.operations, + "experience_traces": lineage.experience_traces, + "source_skills": lineage.source_skills, + "pattern_projects": lineage.pattern_projects, + "program_id": lineage.program_id, + "swarm_id": lineage.swarm_id, + }, + indent=2, + ), + encoding="utf-8", + ) + + promoted = False + genome_id: str | None = None + if promote: + try: + from agentdrive.drive.drive import get_default_drive + from agentdrive.skills.curation import ingest_skill_as_dna, promote_inherited_skill + + promote_inherited_skill(name) + export = ingest_skill_as_dna(name, target_drive=get_default_drive()) + promoted = export.accepted + genome_id = export.genome_id + except Exception: + logger.debug("Fused skill promote/ingest failed for %s", name, exc_info=True) + + fused_meta = { + "name": name, + "path": str(path), + "axes": sorted(lineage.axes_present()), + "source_skills": lineage.source_skills, + "pattern_projects": lineage.pattern_projects, + "promoted": promoted, + "genome_id": genome_id, + "born": True, + } + + try: + from agentdrive.memory.ingest import ingest_from_fused_skill + + mem = ingest_from_fused_skill( + fused_meta, + swarm_id=lineage.swarm_id, + program_id=lineage.program_id, + trigger=lineage.trigger, + ) + if mem: + fused_meta["memory"] = mem + except Exception: + logger.debug("memory bank ingest for fused skill failed", exc_info=True) + + return fused_meta + + +def maybe_fuse_session( + session: LearningSession, + *, + trigger: str, + last_operation: str, +) -> dict[str, Any] | None: + """After absorb, attempt to birth a fused skill when lineage is rich enough.""" + if not auto_fuse_skills_enabled(): + return None + lineage = lineage_from_session(session, trigger) + if not lineage.fusion_ready(): + return None + # Avoid re-fusing on every op — fuse on high-signal terminal ops or explicit synthesis. + terminal = last_operation in { + "external_parent_decision", + "multiverse_parent_decision", + "codebase_mimic", + "think", + "record_outcome", + "experience_graph_record_reasoning", + "synthesize_fused_skill", + } + if not terminal and len(session.ops) < 3: + return None + try: + return synthesize_fused_skill(lineage) + except Exception: + logger.debug("Session skill fusion failed", exc_info=True) + return None + + +def synthesize_from_inputs( + *, + trigger: str, + swarm_id: str, + program_id: str = "skill-fusion", + operations: list[str] | None = None, + experience_traces: list[str] | None = None, + source_skills: list[str] | None = None, + pattern_projects: list[str] | None = None, + promote: bool = False, +) -> dict[str, Any]: + """Explicit fusion API for MCP/CLI (manual or session-exported lineage).""" + lineage = FusionLineage( + trigger=trigger, + swarm_id=swarm_id, + program_id=program_id, + operations=list(operations or []), + experience_traces=list(experience_traces or []), + source_skills=list(source_skills or []), + pattern_projects=list(pattern_projects or []), + ) + return synthesize_fused_skill(lineage, promote=promote) diff --git a/src/agentdrive/learning/skill_naming.py b/src/agentdrive/learning/skill_naming.py new file mode 100644 index 0000000..2abb43d --- /dev/null +++ b/src/agentdrive/learning/skill_naming.py @@ -0,0 +1,114 @@ +""" +Descriptive names for auto-learned and born (fused) skills. + +Skill slugs should tell you what was learned at a glance: + learned-openmangos-mimic-growth-merge-briefing + fused-openmangos-experience-patterns-skills +""" + +from __future__ import annotations + +import re +from typing import Iterable + +_SLUG_RE = re.compile(r"[^a-z0-9]+") +_MAX_SKILL_NAME = 64 + +_OPERATION_VERBS: dict[str, str] = { + "codebase_mimic": "mimic", + "codebase_patterns_profile": "patterns", + "codebase_observe_file": "observe", + "codebase_patterns_match": "match-style", + "codebase_transform_style": "transform-style", + "codebase_mirror_resonance": "mirror-resonance", + "codebase_register_project": "register-project", + "external_parent_decision": "parent-decision", + "multiverse_parent_decision": "multiverse-decision", + "experience_graph_record_reasoning": "fabric-reasoning", + "experience_graph_context_pack": "context-pack", + "think": "synthesis", + "record_outcome": "outcome", + "learnings_log": "learning", + "synthesize_fused_skill": "skill-fusion", + "growth_merge_briefing": "growth-merge", + "memory_bank_store": "memory-store", +} + + +def slugify(text: str, *, max_len: int = 32) -> str: + slug = _SLUG_RE.sub("-", str(text).lower()).strip("-") + if len(slug) > max_len: + slug = slug[:max_len].rstrip("-") + return slug or "session" + + +def _trim_name(parts: Iterable[str]) -> str: + name = "-".join(part for part in parts if part) + return name[:_MAX_SKILL_NAME] or "learned-session" + + +def learned_skill_name( + operation: str, + *, + trigger: str = "", + project_id: str = "", + intent: str = "", +) -> str: + """ + Human-readable slug for a single auto-distilled playbook. + + Pattern: learned-{project?}-{verb}-{focus?} + """ + verb = _OPERATION_VERBS.get(operation) or slugify(operation.replace("_", "-"), max_len=18) + subject = slugify(project_id, max_len=22) if project_id else "" + raw_focus = (intent or trigger or "").strip() + focus = slugify(raw_focus, max_len=30) if raw_focus else "" + if focus in {subject, verb, "session"}: + focus = "" + + parts = ["learned"] + if subject: + parts.append(subject) + parts.append(verb) + if focus: + parts.append(focus) + return _trim_name(parts) + + +def fused_skill_name( + *, + trigger: str = "", + pattern_projects: Iterable[str] | None = None, + axes: Iterable[str] | None = None, +) -> str: + """ + Human-readable slug for a born skill merged from multiple surfaces. + + Pattern: fused-{subject}-{axis1-axis2-...} + """ + projects = [slugify(p, max_len=18) for p in (pattern_projects or []) if p] + subject = projects[0] if projects else slugify(trigger, max_len=26) + axis_list = sorted({slugify(a, max_len=12) for a in (axes or []) if a}) + axis_part = "-".join(axis_list) if axis_list else "merged" + + parts = ["fused", subject or "session", axis_part] + return _trim_name(parts) + + +def learned_skill_title( + operation: str, + *, + trigger: str = "", + project_id: str = "", + intent: str = "", +) -> str: + """Display title for SKILL.md header (not the slug).""" + verb = _OPERATION_VERBS.get(operation, operation.replace("_", " ")) + focus = (intent or trigger or "").strip() + if project_id and focus: + return f"{project_id}: {verb} — {focus[:80]}" + if project_id: + return f"{project_id}: {verb}" + if focus: + return f"{verb}: {focus[:80]}" + return verb.replace("-", " ").title() diff --git a/src/agentdrive/learnings/__init__.py b/src/agentdrive/learnings/__init__.py index 2e47065..00a1bc7 100644 --- a/src/agentdrive/learnings/__init__.py +++ b/src/agentdrive/learnings/__init__.py @@ -7,4 +7,4 @@ "LearningsStore", "resolve_learnings_slug", "ingest_learnings_to_experience", -] \ No newline at end of file +] diff --git a/src/agentdrive/learnings/ingest.py b/src/agentdrive/learnings/ingest.py index 4add20c..7567d51 100644 --- a/src/agentdrive/learnings/ingest.py +++ b/src/agentdrive/learnings/ingest.py @@ -27,9 +27,7 @@ def _entry_hash(entry: dict[str, Any]) -> str: return hashlib.sha256(raw).hexdigest()[:12] -def _observation_from_learning( - entry: dict[str, Any], *, slug: str, obs_id: str -) -> dict[str, Any]: +def _observation_from_learning(entry: dict[str, Any], *, slug: str, obs_id: str) -> dict[str, Any]: return { "schema_version": 3, "page_type": "living-experience", @@ -80,4 +78,4 @@ def ingest_learnings_to_experience(drive: "AgentDrive", slug: str) -> int: observation = _observation_from_learning(entry, slug=store.slug, obs_id=obs_id) obs_path.write_text(json.dumps(observation, indent=2, default=str), encoding="utf-8") ingested += 1 - return ingested \ No newline at end of file + return ingested diff --git a/src/agentdrive/learnings/store.py b/src/agentdrive/learnings/store.py index 59d9cc4..1cbfb91 100644 --- a/src/agentdrive/learnings/store.py +++ b/src/agentdrive/learnings/store.py @@ -144,7 +144,9 @@ def log(self, entry: dict[str, Any]) -> dict[str, Any]: """Append one learning entry. Returns the normalized record written.""" typ = str(entry.get("type") or "") if typ not in ALLOWED_TYPES: - raise ValueError(f"invalid learning type {typ!r}; must be one of: {sorted(ALLOWED_TYPES)}") + raise ValueError( + f"invalid learning type {typ!r}; must be one of: {sorted(ALLOWED_TYPES)}" + ) key = str(entry.get("key") or "") if not key or not _KEY_RE.match(key): @@ -213,4 +215,4 @@ def list_recent(self, limit: int = 10) -> list[dict[str, Any]]: def count(self) -> int: """Count deduplicated learnings.""" - return len(self._entries()) \ No newline at end of file + return len(self._entries()) diff --git a/src/agentdrive/memory/__init__.py b/src/agentdrive/memory/__init__.py new file mode 100644 index 0000000..cd51dcd --- /dev/null +++ b/src/agentdrive/memory/__init__.py @@ -0,0 +1,49 @@ +"""AgentDrive memory layer — triage, Memory Bank, relations, dialogue import.""" + +from agentdrive.memory.anchor import build_session_anchor, load_agent_brief +from agentdrive.memory.briefing import build_deep_briefing, build_memory_briefing +from agentdrive.memory.dialogue_import import import_dialogue_directory, import_dialogue_file +from agentdrive.memory.ingest import ( + ingest_from_fused_skill, + ingest_from_learning, + ingest_from_operation, + memory_ingest_enabled, +) +from agentdrive.memory.ranking import lexical_bm25_scores, rank_memory_candidates +from agentdrive.memory.relations import MemoryRelationGraph, RelationRecord +from agentdrive.memory.scope import MemoryScope, resolve_topic, scope_metadata +from agentdrive.memory.store import MemoryBankStore, MemoryEntry +from agentdrive.memory.triage import ( + MemoryTraceCandidate, + MemoryTriageResult, + build_memory_control_plan, + forgetting_curve_strength, + triage_memory_candidates, +) + +__all__ = [ + "MemoryTraceCandidate", + "MemoryTriageResult", + "build_memory_control_plan", + "forgetting_curve_strength", + "triage_memory_candidates", + "MemoryBankStore", + "MemoryEntry", + "build_memory_briefing", + "build_deep_briefing", + "ingest_from_operation", + "ingest_from_fused_skill", + "ingest_from_learning", + "memory_ingest_enabled", + "lexical_bm25_scores", + "rank_memory_candidates", + "MemoryScope", + "resolve_topic", + "scope_metadata", + "load_agent_brief", + "build_session_anchor", + "MemoryRelationGraph", + "RelationRecord", + "import_dialogue_file", + "import_dialogue_directory", +] diff --git a/src/agentdrive/memory/anchor.py b/src/agentdrive/memory/anchor.py new file mode 100644 index 0000000..31149d7 --- /dev/null +++ b/src/agentdrive/memory/anchor.py @@ -0,0 +1,97 @@ +""" +Session anchor — compact grounding pack when a swarm session opens. + +Tier 1: agent brief (~/.agentdrive/identity.txt) +Tier 2: essential memories (high-confidence, recent) +Tier 3: task-scoped recall (optional query) +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from agentdrive.constants import get_agentdrive_home +from agentdrive.memory.store import MemoryBankStore + + +def load_agent_brief(path: Path | None = None) -> str: + brief_path = path or (get_agentdrive_home() / "identity.txt") + if brief_path.is_file(): + return brief_path.read_text(encoding="utf-8").strip() + return ( + "## Agent brief\n" + "No brief configured. Create ~/.agentdrive/identity.txt with the agent role, " + "active projects, and operator preferences." + ) + + +def build_essential_memories( + swarm_id: str, + *, + vault: str | None = None, + limit: int = 12, + max_chars: int = 3200, +) -> str: + store = MemoryBankStore(swarm_id) + entries = store.list_recent(limit=limit * 2) + if vault: + entries = [entry for entry in entries if entry.vault == vault or vault in entry.tags] + entries.sort(key=lambda entry: (entry.confidence, entry.created_at), reverse=True) + entries = entries[:limit] + + if not entries: + return "## Essential memories\nThe bank is empty — memories accumulate as AgentDrive work completes." + + lines = ["## Essential memories"] + used = 0 + for entry in entries: + vault_label = f" [{entry.vault}]" if entry.vault else "" + body = entry.content.strip() + if len(body) > 400: + body = body[:399] + "…" + block = f"\n### {entry.title}{vault_label} ({entry.kind})\n{body}" + if used + len(block) > max_chars: + break + lines.append(block) + used += len(block) + return "\n".join(lines) + + +def build_session_anchor( + swarm_id: str, + *, + vault: str | None = None, + query: str = "", +) -> dict[str, Any]: + """Produce the session-opening anchor text and structured tiers.""" + tier_agent = load_agent_brief() + tier_essential = build_essential_memories(swarm_id, vault=vault) + tier_scoped = "" + scoped_hits: list[dict[str, Any]] = [] + + if query.strip(): + hits = MemoryBankStore(swarm_id).search(query, limit=5, vault=vault) + if hits: + lines = ["## Scoped recall"] + for hit in hits: + lines.append(f"- **{hit.title}**: {hit.content[:200]}") + tier_scoped = "\n".join(lines) + scoped_hits = [hit.to_dict() for hit in hits] + + anchor_text = f"{tier_agent}\n\n{tier_essential}" + if tier_scoped: + anchor_text = f"{anchor_text}\n\n{tier_scoped}" + + return { + "swarm_id": swarm_id, + "vault": vault, + "tiers": { + "agent_brief": tier_agent, + "essential": tier_essential, + "scoped": tier_scoped or None, + }, + "anchor_text": anchor_text[:8000], + "token_estimate": len(anchor_text) // 4, + "scoped_memories": scoped_hits, + } diff --git a/src/agentdrive/memory/briefing.py b/src/agentdrive/memory/briefing.py new file mode 100644 index 0000000..a123a50 --- /dev/null +++ b/src/agentdrive/memory/briefing.py @@ -0,0 +1,113 @@ +"""Memory Bank briefings — dense recall packs for the AI.""" + +from __future__ import annotations + +from typing import Any + +from agentdrive.memory.store import MemoryBankStore + + +def build_memory_briefing( + swarm_id: str, + *, + query: str = "", + limit: int = 12, + program_id: str | None = None, + max_chars: int = 4000, +) -> dict[str, Any]: + """Return a LLM-optimized memory briefing for session grounding.""" + store = MemoryBankStore(swarm_id) + if query.strip(): + memories = store.search(query, limit=limit, program_id=program_id) + else: + memories = store.list_recent(limit=limit) + + if not memories: + return { + "swarm_id": swarm_id, + "memory_count": 0, + "briefing": "No memories stored yet. AgentDrive will grow this bank as you work.", + "memories": [], + "stats": store.stats(), + } + + lines = [ + f"# Memory Bank — {swarm_id}", + "", + f"**{store.count()}** active memories. This is your deep personal databank —", + "experience, decisions, patterns, born skills, and learnings merged for recall.", + "", + ] + + by_kind: dict[str, list] = {} + for mem in memories: + by_kind.setdefault(mem.kind, []).append(mem) + + for kind, group in sorted(by_kind.items()): + lines.append(f"## {kind.replace('_', ' ').title()}") + for mem in group: + conf = f"{mem.confidence:.0%}" + lines.append(f"### {mem.title} ({conf})") + body = mem.content.strip() + if len(body) > 500: + body = body[:499] + "…" + lines.append(body) + if mem.links: + link_str = ", ".join(f"{l.get('type')}:{l.get('id')}" for l in mem.links[:3]) + lines.append(f"_Links: {link_str}_") + lines.append("") + + briefing = "\n".join(lines) + if len(briefing) > max_chars: + briefing = briefing[: max_chars - 1] + "…" + + return { + "swarm_id": swarm_id, + "memory_count": store.count(), + "recalled": len(memories), + "briefing": briefing, + "memories": [m.to_dict() for m in memories], + "stats": store.stats(), + "integrated_layers": [ + "experience_graph", + "skills", + "learnings", + "codebase_patterns", + "auto_absorb", + "skill_fusion", + ], + } + + +def build_deep_briefing( + swarm_id: str, + *, + query: str = "", + reasoning_style: str = "balanced", + lookback_days: int = 7, + memory_limit: int = 10, + max_tokens: int = 1800, +) -> dict[str, Any]: + """Unified briefing: Experience Graph fabric pack + Memory Bank.""" + from agentdrive.operations.registry import _integrated_recorder + + _, recorder = _integrated_recorder(swarm_id) + fabric_pack = recorder.get_fabric_context_pack( + reasoning_style=reasoning_style, + lookback_days=lookback_days, + max_tokens=max_tokens, + ) + memory_pack = build_memory_briefing(swarm_id, query=query, limit=memory_limit) + + return { + "swarm_id": swarm_id, + "fabric_context_pack": fabric_pack, + "memory_bank": memory_pack, + "deep_briefing": ( + "## Structural memory (Experience Graph)\n" + f"{fabric_pack.get('compact_graph_summary', '')}\n\n" + "## Deep memory bank (personal databank)\n" + f"{memory_pack.get('briefing', '')}" + )[:6000], + "memory_count": memory_pack.get("memory_count", 0), + } diff --git a/src/agentdrive/memory/dialogue_import.py b/src/agentdrive/memory/dialogue_import.py new file mode 100644 index 0000000..37650ab --- /dev/null +++ b/src/agentdrive/memory/dialogue_import.py @@ -0,0 +1,165 @@ +""" +Import dialogue transcripts into the Memory Bank. + +Reads JSONL or plain-text session exports and stores full-text shards without +rewriting or summarizing the source messages. +""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path +from typing import Any, Iterator + +from agentdrive.memory.scope import resolve_topic +from agentdrive.memory.store import MemoryBankStore + +logger = logging.getLogger(__name__) + +SHARD_CHARS = 800 +MIN_CHARS = 30 + + +def _iter_jsonl_messages(path: Path) -> Iterator[dict[str, Any]]: + for line in path.read_text(encoding="utf-8", errors="replace").splitlines(): + line = line.strip() + if not line: + continue + try: + row = json.loads(line) + except json.JSONDecodeError: + continue + if not isinstance(row, dict): + continue + role = str(row.get("role") or row.get("type") or "unknown") + content = row.get("content") or row.get("text") or row.get("message") or "" + if isinstance(content, list): + parts: list[str] = [] + for block in content: + if isinstance(block, dict) and block.get("text"): + parts.append(str(block["text"])) + elif isinstance(block, str): + parts.append(block) + content = "\n".join(parts) + content = str(content).strip() + if len(content) >= MIN_CHARS: + yield {"role": role, "content": content, "origin_path": str(path)} + + +def _split_shards(text: str, *, size: int = SHARD_CHARS) -> list[str]: + if len(text) <= size: + return [text] + shards: list[str] = [] + offset = 0 + while offset < len(text): + shards.append(text[offset : offset + size]) + offset += size + return shards + + +def import_dialogue_file( + path: str | Path, + *, + swarm_id: str, + vault: str = "", + program_id: str = "dialogue-import", +) -> dict[str, Any]: + file_path = Path(path).expanduser().resolve() + if not file_path.is_file(): + raise FileNotFoundError(f"Dialogue file not found: {file_path}") + + store = MemoryBankStore(swarm_id) + vault_name = vault or file_path.parent.name or "dialogues" + imported = 0 + skipped = 0 + + if file_path.suffix == ".jsonl": + for index, message in enumerate(_iter_jsonl_messages(file_path)): + role = message["role"] + for shard_index, shard in enumerate(_split_shards(message["content"])): + title = f"Dialogue {file_path.name} [{role}] #{index}" + if store.has_similar(title, shard): + skipped += 1 + continue + store.store( + kind="episode", + title=title, + content=shard, + confidence=0.7, + source="dialogue_import", + program_id=program_id, + tags=["dialogue", "full_text", vault_name, role], + links=[{"type": "origin_path", "id": str(file_path)}], + vault=vault_name, + topic=resolve_topic("episode", [role, vault_name]), + origin_path=str(file_path), + shard_index=shard_index, + preserves_source=True, + ) + imported += 1 + else: + text = file_path.read_text(encoding="utf-8", errors="replace") + for shard_index, shard in enumerate(_split_shards(text)): + title = f"Dialogue {file_path.name} shard {shard_index}" + if store.has_similar(title, shard): + skipped += 1 + continue + store.store( + kind="episode", + title=title, + content=shard, + confidence=0.65, + source="dialogue_import", + program_id=program_id, + tags=["dialogue", "full_text", vault_name], + vault=vault_name, + topic="dialogues", + origin_path=str(file_path), + shard_index=shard_index, + preserves_source=True, + ) + imported += 1 + + return { + "path": str(file_path), + "vault": vault_name, + "imported": imported, + "skipped": skipped, + "swarm_id": swarm_id, + } + + +def import_dialogue_directory( + directory: str | Path, + *, + swarm_id: str, + vault: str = "", + pattern: str = "*.jsonl", +) -> dict[str, Any]: + root = Path(directory).expanduser().resolve() + if not root.is_dir(): + raise NotADirectoryError(f"Directory not found: {root}") + + total_imported = 0 + total_skipped = 0 + files: list[dict[str, Any]] = [] + for path in sorted(root.rglob(pattern)): + if not path.is_file(): + continue + try: + result = import_dialogue_file(path, swarm_id=swarm_id, vault=vault) + total_imported += result["imported"] + total_skipped += result["skipped"] + files.append(result) + except Exception: + logger.debug("Dialogue import failed for %s", path, exc_info=True) + + return { + "directory": str(root), + "files_processed": len(files), + "imported": total_imported, + "skipped": total_skipped, + "files": files, + "swarm_id": swarm_id, + } diff --git a/src/agentdrive/memory/ingest.py b/src/agentdrive/memory/ingest.py new file mode 100644 index 0000000..0f26e31 --- /dev/null +++ b/src/agentdrive/memory/ingest.py @@ -0,0 +1,228 @@ +"""Auto-ingest AgentDrive surfaces into the Memory Bank.""" + +from __future__ import annotations + +import logging +import os +from typing import Any + +from agentdrive.memory.store import MemoryBankStore + +logger = logging.getLogger(__name__) + +_OP_MEMORY_MAP: dict[str, tuple[str, str]] = { + "think": ("insight", "Synthesis"), + "external_parent_decision": ("decision", "Multiverse collapse"), + "multiverse_parent_decision": ("decision", "Parent decision"), + "experience_graph_record_reasoning": ("insight", "Structural reasoning"), + "record_outcome": ("episode", "Outcome"), + "learnings_log": ("learning", "Operational learning"), + "codebase_observe_file": ("pattern", "Codebase observation"), + "codebase_mimic": ("pattern", "Mirror-neuron mimicry"), + "codebase_patterns_profile": ("pattern", "Writing framework"), + "synthesize_fused_skill": ("born_skill", "Born skill"), +} + + +def memory_ingest_enabled() -> bool: + raw = os.environ.get("AGENTDRIVE_AUTO_MEMORY_BANK", "1").strip().lower() + return raw not in {"0", "false", "no", "off"} + + +def _trigger_text(kwargs: dict[str, Any], result: dict[str, Any]) -> str: + for key in ("trigger", "question", "task", "text", "title", "insight"): + val = kwargs.get(key) + if isinstance(val, str) and val.strip(): + return val.strip() + nested = result.get("result") + if isinstance(nested, dict): + for key in ("trigger", "question", "directive"): + val = nested.get(key) + if isinstance(val, str) and val.strip(): + return val.strip() + return "" + + +def _build_memory_from_operation( + operation: str, + kwargs: dict[str, Any], + result: dict[str, Any], + *, + swarm_id: str, + program_id: str, +) -> dict[str, Any] | None: + kind, label = _OP_MEMORY_MAP.get(operation, ("insight", operation.replace("_", " "))) + trigger = _trigger_text(kwargs, result) + title = trigger[:120] if trigger else label + + content_parts: list[str] = [] + links: list[dict[str, str]] = [] + tags = ["auto-ingest", operation.replace("_", "-")] + + nested = result.get("result") + if operation == "think" and isinstance(nested, dict): + answer = str(nested.get("answer") or "") + gaps = nested.get("gaps") or [] + if answer: + content_parts.append(answer[:1200]) + if gaps: + content_parts.append(f"Gaps: {gaps[0]}") + elif operation in ("external_parent_decision", "multiverse_parent_decision") and isinstance( + nested, dict + ): + decision = nested.get("decision") or {} + directive = decision.get("directive", "") if isinstance(decision, dict) else "" + session_id = nested.get("session_id", "") + if directive: + content_parts.append(f"Directive: {directive}") + if session_id: + content_parts.append(f"Session: {session_id}") + links.append({"type": "multiverse_session", "id": session_id}) + elif operation == "experience_graph_record_reasoning": + reasoning = kwargs.get("reasoning") or {} + if isinstance(reasoning, dict): + rationale = str(reasoning.get("decision_rationale") or reasoning.get("summary") or "") + if rationale: + content_parts.append(rationale[:1000]) + trace = result.get("trace_slug") + if trace: + links.append({"type": "fabric_trace", "id": str(trace)}) + elif operation == "learnings_log": + insight = str(kwargs.get("insight") or "") + key = str(kwargs.get("key") or "") + if insight: + content_parts.append(insight) + if key: + title = f"Learning: {key}" + elif operation in ("codebase_observe_file", "codebase_mimic", "codebase_patterns_profile"): + project_id = str(kwargs.get("project_id") or result.get("project_id") or "") + mimic = str(result.get("mimicry_prompt") or "") + framework = result.get("framework") or {} + if project_id: + tags.append(project_id) + links.append({"type": "codebase_project", "id": project_id}) + if mimic: + content_parts.append(mimic[:800]) + elif isinstance(framework, dict): + patterns = framework.get("patterns") or [] + for pat in patterns[:4]: + if isinstance(pat, dict): + content_parts.append(f"[{pat.get('category')}] {pat.get('rule')}") + elif operation == "synthesize_fused_skill": + fused = result.get("fused_skill") or {} + axes = fused.get("axes") or [] + name = fused.get("name") or "" + if name: + title = f"Born skill: {name}" + content_parts.append(f"Fused axes: {', '.join(axes)}") + links.append({"type": "skill", "id": name}) + elif result.get("auto_learning"): + auto = result["auto_learning"] + if auto.get("fused_skill"): + fused = auto["fused_skill"] + content_parts.append(f"Born skill {fused.get('name')} from session fusion.") + links.append({"type": "skill", "id": str(fused.get("name"))}) + if auto.get("reasoning_trace"): + links.append({"type": "fabric_trace", "id": str(auto["reasoning_trace"])}) + + if not content_parts: + content_parts.append(f"Successful {operation}" + (f": {trigger}" if trigger else "")) + + content = "\n".join(content_parts).strip() + if not content: + return None + + return { + "kind": kind, + "title": title, + "content": content, + "confidence": 0.8 if operation in _OP_MEMORY_MAP else 0.65, + "source": f"auto_absorb:{operation}", + "program_id": program_id, + "tags": tags, + "links": links, + } + + +def ingest_from_operation( + operation: str, + kwargs: dict[str, Any], + result: dict[str, Any], + *, + swarm_id: str, + program_id: str = "", +) -> dict[str, Any] | None: + """Write a memory atom from a successful AgentDrive operation.""" + if not memory_ingest_enabled(): + return None + if not isinstance(result, dict) or not result.get("success"): + return None + + payload = _build_memory_from_operation( + operation, kwargs, result, swarm_id=swarm_id, program_id=program_id + ) + if not payload: + return None + + store = MemoryBankStore(swarm_id) + if store.has_similar(payload["title"], payload["content"]): + return {"skipped": True, "reason": "similar_memory_exists"} + + entry = store.store(**payload) + return {"memory_id": entry.memory_id, "kind": entry.kind, "title": entry.title} + + +def ingest_from_fused_skill( + fused: dict[str, Any], + *, + swarm_id: str, + program_id: str, + trigger: str, +) -> dict[str, Any] | None: + if not memory_ingest_enabled(): + return None + name = fused.get("name") or "fused-skill" + axes = ", ".join(fused.get("axes") or []) + store = MemoryBankStore(swarm_id) + title = f"Born skill memory: {name}" + content = ( + f"Trigger: {trigger}\n" + f"Born skill `{name}` synthesized from axes: {axes}.\n" + f"Parent skills: {', '.join(fused.get('source_skills') or [])}\n" + f"Pattern projects: {', '.join(fused.get('pattern_projects') or [])}" + ) + if store.has_similar(title, content): + return None + entry = store.store( + kind="born_skill", + title=title, + content=content, + confidence=0.85, + source="skill_fusion", + program_id=program_id, + tags=["born-skill", "fused"], + links=[{"type": "skill", "id": name}], + ) + return {"memory_id": entry.memory_id, "kind": entry.kind} + + +def ingest_from_learning(entry: dict[str, Any], *, swarm_id: str) -> dict[str, Any] | None: + if not memory_ingest_enabled(): + return None + key = str(entry.get("key") or "learning") + insight = str(entry.get("insight") or "") + if not insight: + return None + store = MemoryBankStore(swarm_id) + title = f"Learning: {key}" + if store.has_similar(title, insight): + return None + mem = store.store( + kind="learning", + title=title, + content=insight, + confidence=min(1.0, int(entry.get("confidence") or 5) / 10.0), + source="learnings", + tags=["learning", str(entry.get("type") or "operational")], + ) + return {"memory_id": mem.memory_id} diff --git a/src/agentdrive/memory/ranking.py b/src/agentdrive/memory/ranking.py new file mode 100644 index 0000000..14bcc64 --- /dev/null +++ b/src/agentdrive/memory/ranking.py @@ -0,0 +1,99 @@ +""" +Lexical + BM25 ranking for Memory Bank search. + +Combines token overlap signals with Okapi-BM25 over the active candidate set. +Dependency-free — suitable for local MCP loops and offline swarms. +""" + +from __future__ import annotations + +import math +import re +from typing import Any + +_TOKEN_RE = re.compile(r"\w{2,}", re.UNICODE) + + +def _tokenize(text: str) -> list[str]: + if not text: + return [] + return _TOKEN_RE.findall(text.lower()) + + +def lexical_bm25_scores( + query: str, + documents: list[str], + *, + k1: float = 1.5, + b: float = 0.75, +) -> list[float]: + """Score each document against the query using smoothed Okapi-BM25.""" + doc_count = len(documents) + query_terms = set(_tokenize(query)) + if not query_terms or doc_count == 0: + return [0.0] * doc_count + + tokenized = [_tokenize(doc) for doc in documents] + lengths = [len(tokens) for tokens in tokenized] + if not any(lengths): + return [0.0] * doc_count + avg_len = sum(lengths) / doc_count or 1.0 + + doc_freq = {term: 0 for term in query_terms} + for tokens in tokenized: + for term in set(tokens) & query_terms: + doc_freq[term] += 1 + + idf = { + term: math.log((doc_count - doc_freq[term] + 0.5) / (doc_freq[term] + 0.5) + 1) + for term in query_terms + } + + scores: list[float] = [] + for tokens, doc_len in zip(tokenized, lengths): + if doc_len == 0: + scores.append(0.0) + continue + term_freq: dict[str, int] = {} + for token in tokens: + if token in query_terms: + term_freq[token] = term_freq.get(token, 0) + 1 + total = 0.0 + for term, freq in term_freq.items(): + numerator = freq * (k1 + 1) + denominator = freq + k1 * (1 - b + b * doc_len / avg_len) + total += idf[term] * numerator / denominator + scores.append(total) + return scores + + +def rank_memory_candidates( + candidates: list[dict[str, Any]], + query: str, + *, + signal_weight: float = 0.55, + bm25_weight: float = 0.45, +) -> list[tuple[float, dict[str, Any]]]: + """ + Rank memory candidate dicts. + + Each candidate needs ``text`` and optional ``signal_score`` from the store. + """ + if not candidates: + return [] + + documents = [str(item.get("text") or "") for item in candidates] + bm25_raw = lexical_bm25_scores(query, documents) + bm25_peak = max(bm25_raw) if bm25_raw else 0.0 + bm25_scaled = ( + [value / bm25_peak for value in bm25_raw] if bm25_peak > 0 else [0.0] * len(bm25_raw) + ) + + ranked: list[tuple[float, dict[str, Any]]] = [] + for candidate, bm25_value in zip(candidates, bm25_scaled): + signal = float(candidate.get("signal_score") or 0.0) + signal_scaled = min(1.0, signal / 15.0) if signal > 0 else 0.0 + score = signal_weight * signal_scaled + bm25_weight * bm25_value + ranked.append((score, candidate)) + ranked.sort(key=lambda pair: pair[0], reverse=True) + return ranked diff --git a/src/agentdrive/memory/relations.py b/src/agentdrive/memory/relations.py new file mode 100644 index 0000000..fd025dc --- /dev/null +++ b/src/agentdrive/memory/relations.py @@ -0,0 +1,173 @@ +""" +Memory relation graph — time-bounded facts linked to Memory Bank entries. + +Stores subject–predicate–object records with optional validity windows. +""" + +from __future__ import annotations + +import sqlite3 +import uuid +from dataclasses import dataclass +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from agentdrive.drive.drive import get_swarm_drive_path + + +def _graph_path(swarm_id: str) -> Path: + root = get_swarm_drive_path(swarm_id) / "memory_bank" + root.mkdir(parents=True, exist_ok=True) + return root / "relations.sqlite3" + + +@dataclass +class RelationRecord: + relation_id: str + subject: str + predicate: str + object: str + valid_from: str | None = None + valid_to: str | None = None + memory_id: str | None = None + + def to_dict(self) -> dict[str, Any]: + return { + "relation_id": self.relation_id, + "subject": self.subject, + "predicate": self.predicate, + "object": self.object, + "valid_from": self.valid_from, + "valid_to": self.valid_to, + "memory_id": self.memory_id, + } + + +class MemoryRelationGraph: + """Per-swarm SQLite relation store.""" + + def __init__(self, swarm_id: str) -> None: + self.swarm_id = swarm_id + self.path = _graph_path(swarm_id) + self._ensure_schema() + + def _connect(self) -> sqlite3.Connection: + conn = sqlite3.connect(self.path) + conn.row_factory = sqlite3.Row + return conn + + def _ensure_schema(self) -> None: + with self._connect() as conn: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS relations ( + relation_id TEXT PRIMARY KEY, + subject TEXT NOT NULL, + predicate TEXT NOT NULL, + object TEXT NOT NULL, + valid_from TEXT, + valid_to TEXT, + memory_id TEXT, + created_at TEXT NOT NULL + ) + """ + ) + conn.execute("CREATE INDEX IF NOT EXISTS idx_rel_subject ON relations(subject)") + conn.commit() + + def record( + self, + subject: str, + predicate: str, + obj: str, + *, + valid_from: str | None = None, + valid_to: str | None = None, + memory_id: str | None = None, + ) -> RelationRecord: + relation_id = f"rel-{uuid.uuid4().hex[:12]}" + now = datetime.now(UTC).isoformat() + with self._connect() as conn: + conn.execute( + """ + INSERT INTO relations ( + relation_id, subject, predicate, object, + valid_from, valid_to, memory_id, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + relation_id, + subject.strip(), + predicate.strip(), + obj.strip(), + valid_from, + valid_to, + memory_id, + now, + ), + ) + conn.commit() + return RelationRecord( + relation_id=relation_id, + subject=subject, + predicate=predicate, + object=obj, + valid_from=valid_from, + valid_to=valid_to, + memory_id=memory_id, + ) + + def expire( + self, + subject: str, + predicate: str, + obj: str, + *, + ended: str | None = None, + ) -> int: + end = ended or datetime.now(UTC).isoformat() + with self._connect() as conn: + cursor = conn.execute( + """ + UPDATE relations SET valid_to = ? + WHERE subject = ? AND predicate = ? AND object = ? + AND (valid_to IS NULL OR valid_to = '') + """, + (end, subject, predicate, obj), + ) + conn.commit() + return cursor.rowcount + + def query( + self, + entity: str, + *, + as_of: str | None = None, + limit: int = 50, + ) -> list[RelationRecord]: + entity = entity.strip() + sql = "SELECT * FROM relations WHERE (subject = ? OR object = ?)" + params: list[Any] = [entity, entity] + if as_of: + sql += " AND (valid_from IS NULL OR valid_from <= ?)" + sql += " AND (valid_to IS NULL OR valid_to = '' OR valid_to >= ?)" + params.extend([as_of, as_of]) + sql += " ORDER BY created_at DESC LIMIT ?" + params.append(limit) + + records: list[RelationRecord] = [] + with self._connect() as conn: + for row in conn.execute(sql, params): + records.append( + RelationRecord( + relation_id=row["relation_id"], + subject=row["subject"], + predicate=row["predicate"], + object=row["object"], + valid_from=row["valid_from"], + valid_to=row["valid_to"], + memory_id=row["memory_id"], + ) + ) + return records diff --git a/src/agentdrive/memory/scope.py b/src/agentdrive/memory/scope.py new file mode 100644 index 0000000..a82b5f9 --- /dev/null +++ b/src/agentdrive/memory/scope.py @@ -0,0 +1,54 @@ +""" +Memory Bank scoping — vault (workspace) and topic (thematic lane). + +AgentDrive organizes recall by where work lives (vault) and what it is about (topic). +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True) +class MemoryScope: + """Filter memories by workspace vault and/or topic lane.""" + + vault: str = "" + topic: str = "" + + def matches(self, entry_vault: str, entry_topic: str) -> bool: + if self.vault and entry_vault and self.vault != entry_vault: + return False + if self.topic and entry_topic and self.topic != entry_topic: + return False + return True + + def to_dict(self) -> dict[str, str]: + return {"vault": self.vault, "topic": self.topic} + + +def resolve_topic(kind: str, tags: list[str] | None = None) -> str: + """Pick a topic lane from kind or the first meaningful tag.""" + if tags: + for tag in tags: + if tag and not tag.startswith("auto-"): + return tag + return kind or "general" + + +def scope_metadata( + *, + vault: str = "", + topic: str = "", + origin_path: str = "", + shard_index: int | None = None, + preserves_source: bool = True, +) -> dict[str, Any]: + return { + "vault": vault, + "topic": topic or "general", + "origin_path": origin_path, + "shard_index": shard_index, + "preserves_source": preserves_source, + } diff --git a/src/agentdrive/memory/store.py b/src/agentdrive/memory/store.py new file mode 100644 index 0000000..18f82b6 --- /dev/null +++ b/src/agentdrive/memory/store.py @@ -0,0 +1,337 @@ +""" +AgentDrive Memory Bank — deep persistent memory for the AI. + +Append-only atomic memories per swarm, queryable across sessions. +Complements the Experience Graph (structure) and learnings (operational) +with a unified knowledge databank the model can always grow and recall. +""" + +from __future__ import annotations + +import hashlib +import json +import re +import time +import uuid +from dataclasses import asdict, dataclass, field +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from agentdrive.drive.drive import get_swarm_drive_path + +_MEMORY_KINDS = frozenset( + { + "fact", + "procedure", + "insight", + "decision", + "pattern", + "relationship", + "preference", + "episode", + "born_skill", + "learning", + } +) + +_TOKEN_RE = re.compile(r"[a-z0-9][a-z0-9_-]{2,}") + + +def _utc_now() -> str: + return datetime.now(UTC).isoformat() + + +def _memory_bank_dir(swarm_id: str) -> Path: + path = get_swarm_drive_path(swarm_id) / "memory_bank" + path.mkdir(parents=True, exist_ok=True) + return path + + +@dataclass +class MemoryEntry: + memory_id: str + kind: str + title: str + content: str + confidence: float = 0.75 + source: str = "user" + program_id: str = "" + swarm_id: str = "" + tags: list[str] = field(default_factory=list) + links: list[dict[str, str]] = field(default_factory=list) + created_at: str = "" + last_accessed_at: str = "" + access_count: int = 0 + active: bool = True + supersedes: str | None = None + vault: str = "" + topic: str = "" + origin_path: str = "" + shard_index: int | None = None + preserves_source: bool = True + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> MemoryEntry: + shard_index = data.get("shard_index") + return cls( + memory_id=str(data.get("memory_id") or ""), + kind=str(data.get("kind") or "insight"), + title=str(data.get("title") or ""), + content=str(data.get("content") or ""), + confidence=float(data.get("confidence") or 0.75), + source=str(data.get("source") or "user"), + program_id=str(data.get("program_id") or ""), + swarm_id=str(data.get("swarm_id") or ""), + tags=[str(t) for t in (data.get("tags") or [])], + links=[dict(link) for link in (data.get("links") or []) if isinstance(link, dict)], + created_at=str(data.get("created_at") or ""), + last_accessed_at=str(data.get("last_accessed_at") or ""), + access_count=int(data.get("access_count") or 0), + active=bool(data.get("active", True)), + supersedes=data.get("supersedes"), + vault=str(data.get("vault") or ""), + topic=str(data.get("topic") or ""), + origin_path=str(data.get("origin_path") or ""), + shard_index=int(shard_index) if shard_index is not None else None, + preserves_source=bool(data.get("preserves_source", True)), + ) + + +class MemoryBankStore: + """Swarm-scoped append-only memory databank.""" + + def __init__(self, swarm_id: str) -> None: + self.swarm_id = swarm_id + self.root = _memory_bank_dir(swarm_id) + self.memories_path = self.root / "memories.jsonl" + self.stats_path = self.root / "stats.json" + + def _load_all(self) -> list[MemoryEntry]: + if not self.memories_path.is_file(): + return [] + entries: list[MemoryEntry] = [] + for line in self.memories_path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + try: + data = json.loads(line) + except json.JSONDecodeError: + continue + if isinstance(data, dict): + entries.append(MemoryEntry.from_dict(data)) + return entries + + def _active_entries(self) -> list[MemoryEntry]: + return [entry for entry in self._load_all() if entry.active] + + def _dedupe_by_title(self, entries: list[MemoryEntry]) -> list[MemoryEntry]: + seen: dict[str, MemoryEntry] = {} + for entry in sorted(entries, key=lambda item: item.created_at): + key = f"{entry.kind}|{entry.title.strip().lower()}" + if entry.active: + seen[key] = entry + return list(seen.values()) + + def _write_entry(self, entry: MemoryEntry) -> MemoryEntry: + if entry.kind not in _MEMORY_KINDS: + raise ValueError(f"invalid memory kind {entry.kind!r}") + if not entry.title.strip(): + raise ValueError("title is required") + if not entry.content.strip(): + raise ValueError("content is required") + if not entry.memory_id: + entry.memory_id = f"mem-{int(time.time())}-{uuid.uuid4().hex[:8]}" + if not entry.created_at: + entry.created_at = _utc_now() + if not entry.swarm_id: + entry.swarm_id = self.swarm_id + entry.confidence = max(0.0, min(1.0, float(entry.confidence))) + + with self.memories_path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(entry.to_dict(), ensure_ascii=False) + "\n") + + self._bump_stats(kind=entry.kind, source=entry.source) + return entry + + def _bump_stats(self, *, kind: str, source: str) -> None: + stats: dict[str, Any] = { + "total_writes": 0, + "by_kind": {}, + "by_source": {}, + "updated_at": _utc_now(), + } + if self.stats_path.is_file(): + try: + stats = json.loads(self.stats_path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + pass + stats["total_writes"] = int(stats.get("total_writes") or 0) + 1 + stats.setdefault("by_kind", {}) + stats.setdefault("by_source", {}) + stats["by_kind"][kind] = int(stats["by_kind"].get(kind) or 0) + 1 + stats["by_source"][source] = int(stats["by_source"].get(source) or 0) + 1 + stats["updated_at"] = _utc_now() + self.stats_path.write_text(json.dumps(stats, indent=2), encoding="utf-8") + + def store( + self, + *, + kind: str, + title: str, + content: str, + confidence: float = 0.75, + source: str = "user", + program_id: str = "", + tags: list[str] | None = None, + links: list[dict[str, str]] | None = None, + supersedes: str | None = None, + vault: str = "", + topic: str = "", + origin_path: str = "", + shard_index: int | None = None, + preserves_source: bool = True, + ) -> MemoryEntry: + entry = MemoryEntry( + memory_id="", + kind=kind, + title=title.strip(), + content=content.strip(), + confidence=confidence, + source=source, + program_id=program_id, + swarm_id=self.swarm_id, + tags=list(tags or []), + links=list(links or []), + supersedes=supersedes, + vault=vault, + topic=topic, + origin_path=origin_path, + shard_index=shard_index, + preserves_source=preserves_source, + ) + return self._write_entry(entry) + + def recall(self, memory_id: str) -> MemoryEntry | None: + for entry in reversed(self._load_all()): + if entry.memory_id == memory_id and entry.active: + return entry + return None + + def _signal_score(self, entry: MemoryEntry, query_tokens: set[str]) -> float: + haystack = " ".join( + [entry.title, entry.content, entry.kind, entry.source, " ".join(entry.tags)] + ).lower() + haystack_tokens = set(_TOKEN_RE.findall(haystack)) + overlap = len(query_tokens & haystack_tokens) if query_tokens else 0 + score = overlap * 3.0 + score += entry.confidence * 2.0 + score += min(entry.access_count, 10) * 0.15 + try: + created = datetime.fromisoformat(entry.created_at.replace("Z", "+00:00")) + if created.tzinfo is None: + created = created.replace(tzinfo=UTC) + age_days = max(0, (datetime.now(UTC) - created.astimezone(UTC)).days) + score += max(0, 1.5 - age_days * 0.05) + except ValueError: + pass + for tag in entry.tags: + if tag.lower() in query_tokens: + score += 2.0 + return score + + def search( + self, + query: str, + *, + limit: int = 10, + kind: str | None = None, + program_id: str | None = None, + vault: str | None = None, + topic: str | None = None, + ranked: bool = True, + ) -> list[MemoryEntry]: + tokens = set(_TOKEN_RE.findall(query.lower())) + candidates = self._dedupe_by_title(self._active_entries()) + if kind: + candidates = [entry for entry in candidates if entry.kind == kind] + if program_id: + candidates = [ + entry + for entry in candidates + if entry.program_id == program_id or not entry.program_id + ] + if vault: + candidates = [ + entry + for entry in candidates + if entry.vault == vault or vault in entry.tags or not entry.vault + ] + if topic: + candidates = [ + entry + for entry in candidates + if entry.topic == topic or topic in entry.tags or not entry.topic + ] + + if ranked and query.strip(): + from agentdrive.memory.ranking import rank_memory_candidates + + payload = [ + { + "entry": entry, + "text": f"{entry.title}\n{entry.content}", + "signal_score": self._signal_score(entry, tokens), + } + for entry in candidates + ] + ordered = rank_memory_candidates(payload, query) + return [item[1]["entry"] for item in ordered[:limit]] + + ordered = sorted( + ((self._signal_score(entry, tokens), entry) for entry in candidates), + key=lambda item: (-item[0], item[1].created_at), + ) + return [entry for score, entry in ordered[:limit] if score > 0 or not query.strip()] + + def list_recent(self, *, limit: int = 20, kind: str | None = None) -> list[MemoryEntry]: + entries = self._dedupe_by_title(self._active_entries()) + if kind: + entries = [entry for entry in entries if entry.kind == kind] + entries.sort(key=lambda entry: entry.created_at, reverse=True) + return entries[:limit] + + def count(self) -> int: + return len(self._dedupe_by_title(self._active_entries())) + + def stats(self) -> dict[str, Any]: + base = { + "swarm_id": self.swarm_id, + "active_memories": self.count(), + "path": str(self.memories_path), + } + if self.stats_path.is_file(): + try: + base.update(json.loads(self.stats_path.read_text(encoding="utf-8"))) + except json.JSONDecodeError: + pass + by_kind: dict[str, int] = {} + for entry in self._dedupe_by_title(self._active_entries()): + by_kind[entry.kind] = by_kind.get(entry.kind, 0) + 1 + base["active_by_kind"] = by_kind + return base + + def content_hash(self, title: str, content: str) -> str: + raw = f"{title.strip().lower()}|{content.strip()}" + return hashlib.sha256(raw.encode()).hexdigest()[:12] + + def has_similar(self, title: str, content: str) -> bool: + digest = self.content_hash(title, content) + for entry in self._active_entries(): + if self.content_hash(entry.title, entry.content) == digest: + return True + return False diff --git a/src/agentdrive/memory/triage.py b/src/agentdrive/memory/triage.py new file mode 100644 index 0000000..288ee66 --- /dev/null +++ b/src/agentdrive/memory/triage.py @@ -0,0 +1,233 @@ +"""Memory triage primitives for AgentDrive context packs. + +This module gives AgentDrive a small control layer inspired by three stable +findings from human and LLM memory work: + +- working memory is capacity-limited and should be actively selected; +- durable memory improves through rehearsal/consolidation rather than raw append; +- retrieval can reopen a memory for update when it conflicts with current evidence. + +The implementation is deliberately deterministic and dependency-free so it can +run inside MCP, tests, and local model loops without introducing a model call. +""" + +from __future__ import annotations + +import math +from dataclasses import asdict, dataclass, field +from typing import Any + + +def _clamp(value: float, low: float = 0.0, high: float = 1.0) -> float: + return max(low, min(high, float(value))) + + +def forgetting_curve_strength( + age_days: float, + *, + rehearsal_count: int = 0, + half_life_days: float = 7.0, +) -> float: + """Return an Ebbinghaus-style retention signal in the 0..1 range. + + Rehearsal extends the half-life logarithmically. That keeps repeated access + valuable without allowing a noisy item to become immortal solely by being + retrieved many times. + """ + age = max(0.0, float(age_days)) + half_life = max(0.25, float(half_life_days)) + rehearsal_boost = 1.0 + math.log1p(max(0, int(rehearsal_count))) + return round(math.exp(-age / (half_life * rehearsal_boost)), 4) + + +@dataclass(frozen=True) +class MemoryTraceCandidate: + """One graph/genome/learning candidate to route through memory control.""" + + item_id: str + source: str + memory_kind: str = "episodic" + age_days: float = 0.0 + rehearsal_count: int = 0 + salience: float = 0.5 + retrieval_relevance: float = 0.5 + coherence: float = 0.5 + trust: float = 0.7 + novelty: float = 0.3 + contradiction_pressure: float = 0.0 + consolidation_depth: float = 0.0 + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True) +class MemoryTriageResult: + """Scored routing decision for a memory candidate.""" + + item_id: str + source: str + memory_kind: str + route: str + retention_strength: float + working_score: float + consolidation_score: float + reconsolidation_score: float + why: str + metadata: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +ROUTE_ACTIONS: dict[str, dict[str, str]] = { + "working_set": { + "action": "load_into_active_context", + "instruction": "Use these items first in scarce model context.", + }, + "reconsolidate": { + "action": "resolve_before_reuse", + "instruction": "Check, update, or contradict these items before treating them as precedent.", + }, + "consolidate": { + "action": "promote_to_durable_structure", + "instruction": "Turn these items into durable graph, DNA, or learning abstractions.", + }, + "archive": { + "action": "keep_addressable_out_of_context", + "instruction": "Leave these out of active context unless the task directly needs them.", + }, +} + + +def _score_candidate(candidate: MemoryTraceCandidate) -> MemoryTriageResult: + retention = forgetting_curve_strength( + candidate.age_days, + rehearsal_count=candidate.rehearsal_count, + ) + salience = _clamp(candidate.salience) + relevance = _clamp(candidate.retrieval_relevance) + coherence = _clamp(candidate.coherence) + trust = _clamp(candidate.trust) + novelty = _clamp(candidate.novelty) + contradiction = _clamp(candidate.contradiction_pressure) + consolidation_depth = _clamp(candidate.consolidation_depth) + + working = 0.36 * relevance + 0.24 * salience + 0.18 * retention + 0.12 * trust + 0.10 * novelty + consolidation = ( + 0.30 * salience + + 0.24 * novelty + + 0.20 * trust + + 0.16 * (1.0 - consolidation_depth) + + 0.10 * coherence + ) + reconsolidation = ( + 0.36 * contradiction + + 0.24 * (1.0 - coherence) + + 0.18 * relevance + + 0.12 * salience + + 0.10 * retention + ) + + if reconsolidation >= 0.62 and (contradiction >= 0.35 or coherence <= 0.55): + route = "reconsolidate" + why = "retrieved item is important but unstable or conflicting; reopen and update before reuse" + elif working >= 0.66: + route = "working_set" + why = "high relevance and salience; keep in the active context budget" + elif consolidation >= 0.62: + route = "consolidate" + why = "high-signal material has not yet earned durable abstraction" + else: + route = "archive" + why = "low immediate utility; keep addressable but out of scarce context" + + return MemoryTriageResult( + item_id=candidate.item_id, + source=candidate.source, + memory_kind=candidate.memory_kind, + route=route, + retention_strength=retention, + working_score=round(working, 4), + consolidation_score=round(consolidation, 4), + reconsolidation_score=round(reconsolidation, 4), + why=why, + metadata=dict(candidate.metadata), + ) + + +def build_memory_control_plan(queues: dict[str, list[dict[str, Any]]]) -> dict[str, Any]: + """Build deterministic agent instructions from routed memory queues.""" + route_order = ["working_set", "reconsolidate", "consolidate", "archive"] + steps: list[dict[str, Any]] = [] + for route in route_order: + items = queues.get(route, []) + action = ROUTE_ACTIONS[route] + steps.append( + { + "route": route, + "action": action["action"], + "instruction": action["instruction"], + "count": len(items), + "item_ids": [str(item.get("item_id", "")) for item in items], + } + ) + + if queues.get("working_set"): + next_focus = "reason_over_working_set" + elif queues.get("reconsolidate"): + next_focus = "resolve_reconsolidation_queue" + elif queues.get("consolidate"): + next_focus = "schedule_consolidation" + else: + next_focus = "no_active_memory_work" + + return { + "next_focus": next_focus, + "primary_context_order": route_order, + "steps": steps, + } + + +def triage_memory_candidates( + candidates: list[MemoryTraceCandidate], + *, + per_route_limit: int = 4, +) -> dict[str, Any]: + """Route memory candidates into queues consumed by agents and consolidators.""" + scored = [_score_candidate(c) for c in candidates] + route_priority = { + "working_set": 3, + "reconsolidate": 2, + "consolidate": 1, + "archive": 0, + } + scored.sort( + key=lambda r: ( + route_priority.get(r.route, 0), + max(r.working_score, r.consolidation_score, r.reconsolidation_score), + ), + reverse=True, + ) + + queues: dict[str, list[dict[str, Any]]] = { + "working_set": [], + "consolidate": [], + "reconsolidate": [], + "archive": [], + } + for result in scored: + queue = queues[result.route] + if len(queue) < per_route_limit: + queue.append(result.to_dict()) + + return { + "model": "human-inspired-memory-triage-v1", + "principles": [ + "limited working context", + "rehearsal-sensitive retention", + "salience-weighted consolidation", + "retrieval-triggered reconsolidation for conflict", + ], + "queues": queues, + "counts": {name: len(items) for name, items in queues.items()}, + "control_plan": build_memory_control_plan(queues), + } diff --git a/src/agentdrive/mission_control/__init__.py b/src/agentdrive/mission_control/__init__.py index f2c575d..552514e 100644 --- a/src/agentdrive/mission_control/__init__.py +++ b/src/agentdrive/mission_control/__init__.py @@ -21,11 +21,12 @@ GridHealthEvent, LoopStepEvent, MissionEvent, + MultiverseUpdateEvent, OverseerStateEvent, ParentDecisionEvent, StaticFireEvent, ) -from .loop_views import FabricView, LoopStateView, StaticFireTelemetry +from .loop_views import FabricView, LoopStateView, MultiverseView, StaticFireTelemetry from .server import ( # Rich Static Fire helpers for first-class controlled evolution runs FireSession, @@ -55,9 +56,11 @@ "GridHealthEvent", "LoopStateView", "FabricView", + "MultiverseView", + "MultiverseUpdateEvent", "StaticFireTelemetry", # Rich static fire surfaces + integration helpers (use in 2min harnesses for full Bay telemetry) "FireSession", "run_static_fire_with_mission_telemetry", "publish_static_fire_telemetry", -] \ No newline at end of file +] diff --git a/src/agentdrive/mission_control/authz.py b/src/agentdrive/mission_control/authz.py index f246c52..5c90168 100644 --- a/src/agentdrive/mission_control/authz.py +++ b/src/agentdrive/mission_control/authz.py @@ -133,4 +133,4 @@ def mint_mission_control_cap( resource_id=command, ), ) - return signed.cap_id \ No newline at end of file + return signed.cap_id diff --git a/src/agentdrive/mission_control/events.py b/src/agentdrive/mission_control/events.py index 0ae483b..4110ab2 100644 --- a/src/agentdrive/mission_control/events.py +++ b/src/agentdrive/mission_control/events.py @@ -24,9 +24,11 @@ class MissionEvent: # === Core Loop Events (the 6-step canonical loop) === + @dataclass class LoopStepEvent(MissionEvent): """Represents progress through one of the 6 canonical loop steps.""" + step: Literal[1, 2, 3, 4, 5, 6] = 1 description: str = "" data: dict[str, Any] = field(default_factory=dict) @@ -42,17 +44,21 @@ class FabricUpdateEvent(MissionEvent): considered (elements, pattern, expected lift) and allow clickable highlights on the fabric canvas. """ + fabric_coherence: float = 0.0 delta_edges: int = 0 affected_cycles: list[str] = field(default_factory=list) summary: str = "" graph_delta: dict[str, Any] | None = None # For partial graph updates - parent_fabric_reasoning: dict[str, Any] | None = None # Parent's explicit graph-native reasoning trace (elements_considered, structural_pattern, expected_lift, rationale) + parent_fabric_reasoning: dict[str, Any] | None = ( + None # Parent's explicit graph-native reasoning trace (elements_considered, structural_pattern, expected_lift, rationale) + ) @dataclass class ParentDecisionEvent(MissionEvent): """A decision made by the Parent Conductor.""" + decision_summary: str = "" actions_taken: list[str] = field(default_factory=list) triggered_from_fabric: bool = False @@ -62,6 +68,7 @@ class ParentDecisionEvent(MissionEvent): @dataclass class OverseerStateEvent(MissionEvent): """The Overseer's current metacognitive understanding (step 2 of the loop).""" + adaptation_effectiveness: float = 0.0 plateau_detected: bool = False fabric_coherence: float = 0.0 @@ -71,6 +78,7 @@ class OverseerStateEvent(MissionEvent): # === Static Fire Specific === + @dataclass class StaticFireEvent(MissionEvent): """Telemetry and state for a Static Fire run (controlled evolution window). @@ -85,7 +93,10 @@ class StaticFireEvent(MissionEvent): and on completion by harness using run_static_fire_with_mission_telemetry or direct calls. All via the single publish_event_sync path; never bypasses quarantine or auth. """ - phase: Literal["starting", "running", "densifying", "measuring", "completed", "aborted"] = "idle" + + phase: Literal["starting", "running", "densifying", "measuring", "completed", "aborted"] = ( + "idle" + ) duration_seconds: float = 0.0 cycles_completed: int = 0 current_fabric_coherence: float = 0.0 @@ -103,12 +114,27 @@ class StaticFireEvent(MissionEvent): # === System-level === + @dataclass class GridHealthEvent(MissionEvent): """Snapshot of GridEngine health surfaced in Mission Control.""" + health: dict[str, Any] = field(default_factory=dict) +@dataclass +class MultiverseUpdateEvent(MissionEvent): + """Multiverse Cognition session lifecycle for Mission Control Tower.""" + + session_id: str = "" + phase: str = "spawned" # spawned | simulated | collapsed | reopened + status: str = "open" + branch_count: int = 0 + collapsed_branch_id: str | None = None + invariants: list[str] = field(default_factory=list) + branches_summary: list[dict[str, Any]] = field(default_factory=list) + + @dataclass class DreamPhaseEvent(MissionEvent): """Telemetry for one phased dream maintenance cycle step.""" @@ -132,4 +158,5 @@ class DreamPhaseEvent(MissionEvent): | StaticFireEvent | GridHealthEvent | DreamPhaseEvent -) \ No newline at end of file + | MultiverseUpdateEvent +) diff --git a/src/agentdrive/mission_control/loop_views.py b/src/agentdrive/mission_control/loop_views.py index f6e10f9..d37234e 100644 --- a/src/agentdrive/mission_control/loop_views.py +++ b/src/agentdrive/mission_control/loop_views.py @@ -18,16 +18,19 @@ class LoopStateView: A live snapshot of the canonical 6-step Parent-Overseer-Research loop. This is the primary "single pane of glass" view. """ + cycle_id: str current_step: int # 1-6 - step_descriptions: dict[int, str] = field(default_factory=lambda: { - 1: "Experience Layer + Runtime generating signals", - 2: "Overseer ingesting experience + multi-cycle fabric", - 3: "Overseer feeding understanding to Parent", - 4: "Parent making real-time decisions", - 5: "Decisions executing back into runtime", - 6: "New experience + updated fabric flowing back to Overseer", - }) + step_descriptions: dict[int, str] = field( + default_factory=lambda: { + 1: "Experience Layer + Runtime generating signals", + 2: "Overseer ingesting experience + multi-cycle fabric", + 3: "Overseer feeding understanding to Parent", + 4: "Parent making real-time decisions", + 5: "Decisions executing back into runtime", + 6: "New experience + updated fabric flowing back to Overseer", + } + ) fabric_coherence: float = 0.0 last_parent_decision: dict[str, Any] | None = None overseer_state: dict[str, Any] | None = None @@ -40,6 +43,7 @@ class FabricView: Multi-cycle memory fabric view. This is what makes the user see "the whole system as one" across iterations. """ + overall_coherence: float active_cycles: list[str] total_cross_cycle_edges: int @@ -48,6 +52,20 @@ class FabricView: graph_summary: dict[str, Any] # Could include mermaid snippet or node/edge counts +@dataclass +class MultiverseView: + """Live multiverse superposition snapshot for Mission Control.""" + + active_session_id: str | None = None + status: str = "idle" + branch_count: int = 0 + collapsed_branch_id: str | None = None + top_invariants: list[str] = field(default_factory=list) + branches: list[dict[str, Any]] = field(default_factory=list) + recent_collapses: list[dict[str, Any]] = field(default_factory=list) + llm_mode: str = "heuristic" # llm | heuristic + + @dataclass class StaticFireTelemetry: """ @@ -59,6 +77,7 @@ class StaticFireTelemetry: Populated by Integrated.get_static_fire_telemetry() and by harnesses via the run_* helper or direct publish_static_fire_telemetry. """ + fire_id: str status: str # "idle", "running", "completed", "aborted" started_at: float | None = None diff --git a/src/agentdrive/mission_control/server.py b/src/agentdrive/mission_control/server.py index fe105ba..d74444b 100644 --- a/src/agentdrive/mission_control/server.py +++ b/src/agentdrive/mission_control/server.py @@ -94,7 +94,9 @@ class MissionControlHub: def __init__(self): self.active_connections: list[WebSocket] = [] self._current_mission = None # Will hold reference to IntegratedRealTimeEvolutionSystem - self._current_grid: Any = None # AD-Grid persistent GridEngine attach (survives mission end) + self._current_grid: Any = ( + None # AD-Grid persistent GridEngine attach (survives mission end) + ) # Bounded recent events (now full sendable payloads incl. seq) for smoke / replay / debugging. # Populated from ALL publish paths (the only way data flows core -> MC). self.recent_events: list[dict[str, Any]] = [] @@ -202,7 +204,9 @@ def mission(self): @property def grid(self): - return self._current_grid or (getattr(self._current_mission, "grid", None) if self._current_mission else None) + return self._current_grid or ( + getattr(self._current_mission, "grid", None) if self._current_mission else None + ) # ------------------------------------------------------------------ # Command router (inbound from UI / smoke). Commands are the return path. @@ -360,9 +364,15 @@ def dispatch_command(self, command: str, **kwargs: Any) -> dict[str, Any]: "from_fabric", kwargs.get("triggered_from_fabric", True) ) actions_taken = kwargs.get("actions_taken", ["mission_control_command"]) - fabric_reasoning = kwargs.get("fabric_reasoning") or kwargs.get("structural_fabric_reasoning") + fabric_reasoning = kwargs.get("fabric_reasoning") or kwargs.get( + "structural_fabric_reasoning" + ) fn = getattr(mission, "record_parent_decision", None) - result = fn(cid, decision, actions_taken, fabric_reasoning=fabric_reasoning) if callable(fn) else None + result = ( + fn(cid, decision, actions_taken, fabric_reasoning=fabric_reasoning) + if callable(fn) + else None + ) return { "command": command, "result": result, @@ -654,10 +664,27 @@ def _lift(d): # DNA via recorder (if attached) + emits ParentDecisionEvent (visible in Tower fabric/decision timeline). # Full attribution: ilo-conductor-cockpit program + Program Contract + Guardian + 1780296458. # Called from UI buttons in Code Action Review Queue; produces living DNA for the self-referential loop. - elif command in ("review_code_action", "approve_code_action", "override_code_action", "review_inhabitant_code_action"): - slug = kwargs.get("code_action_slug") or kwargs.get("slug") or kwargs.get("target_slug") - verdict = kwargs.get("verdict", "approved_by_conductor" if "approve" in command.lower() else "overridden_by_conductor") - conductor_sig = kwargs.get("conductor_signature", "ilo-conductor-cockpit@stabilization-wave-20260531 + Contract + 1780296458") + elif command in ( + "review_code_action", + "approve_code_action", + "override_code_action", + "review_inhabitant_code_action", + ): + slug = ( + kwargs.get("code_action_slug") + or kwargs.get("slug") + or kwargs.get("target_slug") + ) + verdict = kwargs.get( + "verdict", + "approved_by_conductor" + if "approve" in command.lower() + else "overridden_by_conductor", + ) + conductor_sig = kwargs.get( + "conductor_signature", + "ilo-conductor-cockpit@stabilization-wave-20260531 + Contract + 1780296458", + ) note = kwargs.get("note", f"Conductor Cockpit review: {verdict} on {slug}") result = {"status": "review_ack", "slug": slug, "verdict": verdict, "note": note} try: @@ -697,7 +724,11 @@ def _lift(d): "research-constitution-ad-grid-program-contract@stabilization-wave-20260531", "research-constitution-guardian-integrity@stabilization-wave-20260531", ], - user_objective_refs=["conductor-cockpit-steering", "1780296458", "code-action-review"], + user_objective_refs=[ + "conductor-cockpit-steering", + "1780296458", + "code-action-review", + ], ) result["verdict_dna_slug"] = vslug result["dna_recorded"] = True @@ -716,7 +747,9 @@ def _lift(d): elif command in ("get_council_activity", "council_activity"): return { "command": command, - "result": {"note": "Full data via REST GET /api/grid/council-activity (uses get_council_activity fabric pattern)"}, + "result": { + "note": "Full data via REST GET /api/grid/council-activity (uses get_council_activity fabric pattern)" + }, "timestamp": time.time(), "surface": "local_operator_only", } @@ -852,7 +885,9 @@ def derive_loop_state_snapshot(self) -> dict[str, Any]: else None, } # Always merge grid_health when Grid is attached (persistent AD-Grid observability) - gh = self._safe_grid_health(grid or (mission.grid if mission and hasattr(mission, "grid") else None)) + gh = self._safe_grid_health( + grid or (mission.grid if mission and hasattr(mission, "grid") else None) + ) base["grid_health"] = gh base["status"] = "ok" if (mission or grid) else "no_mission_attached" return base @@ -880,8 +915,12 @@ def derive_fabric_snapshot(self) -> dict[str, Any]: if mission is None: return {"status": "no_mission_attached"} try: - if hasattr(mission, "recorder") and hasattr(mission.recorder, "get_parent_facing_memory_fabric_briefing"): - briefing = mission.recorder.get_parent_facing_memory_fabric_briefing(lookback_days=7) + if hasattr(mission, "recorder") and hasattr( + mission.recorder, "get_parent_facing_memory_fabric_briefing" + ): + briefing = mission.recorder.get_parent_facing_memory_fabric_briefing( + lookback_days=7 + ) # Also surface quick recent cycle graphs if the recorder supports it recent_graphs = [] try: @@ -902,6 +941,61 @@ def derive_fabric_snapshot(self) -> dict[str, Any]: pass return {} + def derive_multiverse_snapshot(self) -> dict[str, Any]: + """Multiverse Cognition panel data for Mission Control Tower (M5).""" + mission = self._current_mission + if mission is None or not hasattr(mission, "recorder"): + return {"status": "no_mission_attached"} + try: + from agentdrive.cognition import MultiverseEngine + + engine = MultiverseEngine(mission.recorder) + ctx = engine.briefing_context(limit=5) + sessions = engine.list_sessions(limit=3) + latest = sessions[0] if sessions else None + llm_mode = "heuristic" + if engine.use_llm: + spawner = engine._get_llm_spawner(latest.trigger if latest else "probe") + if spawner and spawner.llm_available: + llm_mode = "llm" + + branches = [] + active_id = None + status = "idle" + collapsed = None + invariants: list[str] = [] + if latest: + active_id = latest.session_id + status = latest.status.value + collapsed = latest.collapsed_branch_id + invariants = [i.statement for i in latest.invariants if i.kind.value == "robust"][ + :5 + ] + branches = [ + { + "id": b.branch_id, + "role": b.role, + "robustness": b.robustness_score, + "collapsed": b.branch_id == collapsed, + } + for b in latest.branches + ] + + return { + "status": "ok", + "llm_mode": llm_mode, + "active_session_id": active_id, + "session_status": status, + "branch_count": len(branches), + "collapsed_branch_id": collapsed, + "top_invariants": invariants, + "branches": branches, + "recent_collapses": ctx.get("recent_collapses", []), + "open_superposition": ctx.get("open_superposition", []), + } + except Exception: + return {"status": "multiverse_snapshot_error"} + # Global hub instance hub = MissionControlHub() @@ -986,10 +1080,15 @@ async def current_state(): "timestamp": time.time(), "loop_state": loop_snap, "fabric": fabric_snap, + "multiverse": hub.derive_multiverse_snapshot() + if hasattr(hub, "derive_multiverse_snapshot") + else {}, "grid_health": grid_health, "recent_event_count": len(hub.recent_events), # Tight experience layer snapshot for the UI on initial load (reuses the rich derive that already calls recorder briefing when attached) - "experience_fabric": hub.derive_fabric_snapshot() if hasattr(hub, "derive_fabric_snapshot") else (fabric_snap if isinstance(fabric_snap, dict) else {}), + "experience_fabric": hub.derive_fabric_snapshot() + if hasattr(hub, "derive_fabric_snapshot") + else (fabric_snap if isinstance(fabric_snap, dict) else {}), } # === AgentDrive-native Mission Kanban Board API === @@ -1022,10 +1121,26 @@ async def get_experience_fabric(): return {"status": "no_experience_layer_attached"} try: recorder = mission.recorder - briefing = recorder.get_parent_facing_memory_fabric_briefing(lookback_days=7) if hasattr(recorder, "get_parent_facing_memory_fabric_briefing") else {} - recent = recorder.get_recent_loop_graphs(limit=4) if hasattr(recorder, "get_recent_loop_graphs") else [] - weak = recorder.find_weak_across_recent_cycles(min_coherence=0.65, lookback=5) if hasattr(recorder, "find_weak_across_recent_cycles") else [] - traces = recorder.get_recent_parent_fabric_reasoning_traces_for_panel(limit=5) if hasattr(recorder, "get_recent_parent_fabric_reasoning_traces") else [] + briefing = ( + recorder.get_parent_facing_memory_fabric_briefing(lookback_days=7) + if hasattr(recorder, "get_parent_facing_memory_fabric_briefing") + else {} + ) + recent = ( + recorder.get_recent_loop_graphs(limit=4) + if hasattr(recorder, "get_recent_loop_graphs") + else [] + ) + weak = ( + recorder.find_weak_across_recent_cycles(min_coherence=0.65, lookback=5) + if hasattr(recorder, "find_weak_across_recent_cycles") + else [] + ) + traces = ( + recorder.get_recent_parent_fabric_reasoning_traces_for_panel(limit=5) + if hasattr(recorder, "get_recent_parent_fabric_reasoning_traces") + else [] + ) return { "briefing": briefing, "recent_cycle_graphs": recent, @@ -1054,7 +1169,9 @@ async def transition_board_mission(mid: str, payload: dict[str, Any]): if not to: return {"ok": False, "error": "missing 'to' status"} try: - to_status = __import__('agentdrive.board.mission_board', fromlist=['MissionStatus']).MissionStatus(to) + to_status = __import__( + "agentdrive.board.mission_board", fromlist=["MissionStatus"] + ).MissionStatus(to) except Exception: return {"ok": False, "error": f"invalid status '{to}'"} outcome = payload.get("outcome") @@ -1079,9 +1196,7 @@ async def dream_status_api(): status = get_dream_cycle_status() recent = [ - e - for e in hub.recent_events[-30:] - if getattr(e, "event_type", "") == "dream_phase" + e for e in hub.recent_events[-30:] if getattr(e, "event_type", "") == "dream_phase" ] status["recent_phase_events"] = [ { @@ -1110,7 +1225,11 @@ async def get_grid_health(): h = dict(g._grid_health) else: h = {"status": "attached"} - h["mode"] = "living" if h.get("active_research_threads", 0) or h.get("active_programs", 0) else "quiet" + h["mode"] = ( + "living" + if h.get("active_research_threads", 0) or h.get("active_programs", 0) + else "quiet" + ) return h except Exception as e: return {"status": "error", "error": str(e)[:120], "mode": "quiet"} @@ -1135,12 +1254,22 @@ async def get_grid_status(): """Light status for quiet-mode banners and adaptive polling decisions in Tower.""" g = hub.grid if g is None: - return {"attached": False, "mode": "no_grid", "recommendation": "start GridEngine for persistent AD-Grid view"} + return { + "attached": False, + "mode": "no_grid", + "recommendation": "start GridEngine for persistent AD-Grid view", + } try: - h = g.get_grid_health() if hasattr(g, "get_grid_health") else getattr(g, "_grid_health", {}) + h = ( + g.get_grid_health() + if hasattr(g, "get_grid_health") + else getattr(g, "_grid_health", {}) + ) return { "attached": True, - "mode": "quiet" if not (h.get("active_research_threads") or h.get("active_programs")) else "active", + "mode": "quiet" + if not (h.get("active_research_threads") or h.get("active_programs")) + else "active", "fabric_coherence_last": h.get("fabric_coherence_last", 0.0), "active_programs": h.get("active_programs", 0), "active_research_threads": h.get("active_research_threads", 0), @@ -1178,15 +1307,30 @@ async def get_grid_inhabitants(): enriched = [] for p in programs: pp = dict(p) if isinstance(p, dict) else {"raw": str(p)[:200]} - pp["status"] = "registered_inhabitant" if pp.get("program_id") else "manifest_incomplete" - pp["attribution"] = "GridEngine.register_model_program + ad-grid-program-contract@stabilization-wave-20260531" + pp["status"] = ( + "registered_inhabitant" if pp.get("program_id") else "manifest_incomplete" + ) + pp["attribution"] = ( + "GridEngine.register_model_program + ad-grid-program-contract@stabilization-wave-20260531" + ) pp["cockpit_note"] = "visible to Conductor for steering / override" enriched.append(pp) return { "inhabitants": enriched, "count": len(enriched), - "health_snapshot": {k: health.get(k) for k in ("active_programs", "active_research_threads", "status", "fabric_coherence_last") if k in health}, - "council_threads": health.get("active_research_threads", health.get("research_threads", 3)), + "health_snapshot": { + k: health.get(k) + for k in ( + "active_programs", + "active_research_threads", + "status", + "fabric_coherence_last", + ) + if k in health + }, + "council_threads": health.get( + "active_research_threads", health.get("research_threads", 3) + ), "mode": "living" if enriched or health.get("active_research_threads") else "quiet", "note": "Inhabitant Registrations & Status — from register_model_program DNA. Self-referential; all actions carry full attribution.", "generated_at": time.time(), @@ -1200,7 +1344,13 @@ async def get_grid_council_activity(roles: str = "", limit: int = 15): get_fabric_reasoning_traces_for_element on the three Council constitutions + Program Contract). Falls back to recent_events + grid health. Real-time via Tower polling + WS fabric events. """ - effective_roles = [r.strip().lower() for r in (roles or "").split(",") if r.strip()] or ["perfectionist", "guardian", "external", "program-contract", "externalbridge"] + effective_roles = [r.strip().lower() for r in (roles or "").split(",") if r.strip()] or [ + "perfectionist", + "guardian", + "external", + "program-contract", + "externalbridge", + ] council_elems = [ "research-constitution-perfectionist-optimizer@stabilization-wave-20260531", "research-constitution-guardian-integrity@stabilization-wave-20260531", @@ -1214,19 +1364,37 @@ async def get_grid_council_activity(roles: str = "", limit: int = 15): rec = mission.recorder try: if hasattr(rec, "get_recent_parent_fabric_reasoning_traces"): - traces = rec.get_recent_parent_fabric_reasoning_traces_for_panel(limit=limit * 2) or [] + traces = ( + rec.get_recent_parent_fabric_reasoning_traces_for_panel(limit=limit * 2) + or [] + ) for t in traces: txt = str(t).lower() + str(t.get("fabric_elements_considered", [])) - if any(r in txt for r in effective_roles) or any(e in txt for e in council_elems): - activity.append({"type": "parent_fabric_reasoning", "data": t, "gbrain": getattr(t, "gbrain_signal_score", 0.7)}) + if any(r in txt for r in effective_roles) or any( + e in txt for e in council_elems + ): + activity.append( + { + "type": "parent_fabric_reasoning", + "data": t, + "gbrain": getattr(t, "gbrain_signal_score", 0.7), + } + ) for elem in council_elems: if len(activity) >= limit * 2: break if hasattr(rec, "get_fabric_reasoning_traces_for_element"): try: - ts = rec.get_fabric_reasoning_traces_for_element(element=elem, lookback=max(3, limit // 3)) or [] + ts = ( + rec.get_fabric_reasoning_traces_for_element( + element=elem, lookback=max(3, limit // 3) + ) + or [] + ) for t in ts: - activity.append({"type": "council_element_trace", "element": elem, "data": t}) + activity.append( + {"type": "council_element_trace", "element": elem, "data": t} + ) except Exception: pass except Exception: @@ -1234,13 +1402,26 @@ async def get_grid_council_activity(roles: str = "", limit: int = 15): # live from hub recent + grid for e in hub.recent_events[-40:]: s = str(e).lower() - if any(x in s for x in ["perfectionist", "guardian", "external-bridge", "council", "program-contract"]): + if any( + x in s + for x in [ + "perfectionist", + "guardian", + "external-bridge", + "council", + "program-contract", + ] + ): activity.append({"type": "live_event", "event": e}) g = hub.grid grid_snap = {} if g: try: - grid_snap = g.get_grid_health() if hasattr(g, "get_grid_health") else getattr(g, "_grid_health", {}) + grid_snap = ( + g.get_grid_health() + if hasattr(g, "get_grid_health") + else getattr(g, "_grid_health", {}) + ) except Exception: pass activity = activity[:limit] @@ -1248,7 +1429,11 @@ async def get_grid_council_activity(roles: str = "", limit: int = 15): "swarm_id": "stabilization-wave-20260531", "roles": effective_roles, "recent_council_activity": activity, - "grid_health": {k: grid_snap.get(k) for k in ("active_research_threads", "active_programs", "status") if k in grid_snap}, + "grid_health": { + k: grid_snap.get(k) + for k in ("active_research_threads", "active_programs", "status") + if k in grid_snap + }, "note": "Mirrors MCP agentdrive_get_council_activity. High-gbrain Council traces + proposals feed the Live Council Activity panel. Full provenance in Experience Graph.", "generated_at": time.time(), "dna_trace_ref": "parent_fabric_reasoning:1780296588", @@ -1269,28 +1454,70 @@ async def get_grid_code_actions(limit: int = 20): rec = mission.recorder try: if hasattr(rec, "loops_dir"): - files = sorted(rec.loops_dir.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)[:40] + files = sorted( + rec.loops_dir.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True + )[:40] for f in files: try: data = json.loads(f.read_text()) - arts = (data.get("participating_artifacts", []) or data.get("artifacts", []) or []) + arts = ( + data.get("participating_artifacts", []) + or data.get("artifacts", []) + or [] + ) for a in arts: - if isinstance(a, dict) and str(a.get("artifact_type", "")).lower() == "inhabitant_code_action": - content = a.get("content_ref") or a.get("content") or a.get("ref") or {} - act = content.get("action", {}) if isinstance(content, dict) else {} - actions.append({ - "slug": a.get("slug"), - "cycle_id": data.get("cycle_id") or a.get("cycle_id"), - "ts": a.get("ts") or data.get("started_at") or data.get("created_at") or 0, - "program_id": content.get("program_id") or act.get("program_id") or "unknown-inhabitant", - "action_type": act.get("type") if isinstance(act, dict) else str(act)[:30], - "action": act if isinstance(act, dict) else {"raw": str(act)[:120]}, - "constitution_refs": content.get("constitution_refs", []) if isinstance(content, dict) else [], - "user_objective_refs": content.get("user_objective_refs", []) if isinstance(content, dict) else [], - "verdict": content.get("verdict") or content.get("guardian_verdict") or act.get("verdict"), - "content_preview": str(content)[:160] if content else "", - "source": "recorder_loops_scan", - }) + if ( + isinstance(a, dict) + and str(a.get("artifact_type", "")).lower() + == "inhabitant_code_action" + ): + content = ( + a.get("content_ref") + or a.get("content") + or a.get("ref") + or {} + ) + act = ( + content.get("action", {}) + if isinstance(content, dict) + else {} + ) + actions.append( + { + "slug": a.get("slug"), + "cycle_id": data.get("cycle_id") or a.get("cycle_id"), + "ts": a.get("ts") + or data.get("started_at") + or data.get("created_at") + or 0, + "program_id": content.get("program_id") + or act.get("program_id") + or "unknown-inhabitant", + "action_type": act.get("type") + if isinstance(act, dict) + else str(act)[:30], + "action": act + if isinstance(act, dict) + else {"raw": str(act)[:120]}, + "constitution_refs": content.get( + "constitution_refs", [] + ) + if isinstance(content, dict) + else [], + "user_objective_refs": content.get( + "user_objective_refs", [] + ) + if isinstance(content, dict) + else [], + "verdict": content.get("verdict") + or content.get("guardian_verdict") + or act.get("verdict"), + "content_preview": str(content)[:160] + if content + else "", + "source": "recorder_loops_scan", + } + ) except Exception: continue except Exception: @@ -1298,14 +1525,20 @@ async def get_grid_code_actions(limit: int = 20): # supplement with recent hub events (real-time proposals during Council threads) for e in hub.recent_events[-60:]: evs = str(e.get("data", e)).lower() - if "inhabitant_code_action" in evs or "code_action" in evs or "self_improvement_proposal" in evs: - actions.append({ - "slug": e.get("seq"), - "from_live_event": True, - "event_summary": str(e.get("data", {}))[:200], - "ts": e.get("timestamp", 0), - "source": "hub_recent_events", - }) + if ( + "inhabitant_code_action" in evs + or "code_action" in evs + or "self_improvement_proposal" in evs + ): + actions.append( + { + "slug": e.get("seq"), + "from_live_event": True, + "event_summary": str(e.get("data", {}))[:200], + "ts": e.get("timestamp", 0), + "source": "hub_recent_events", + } + ) actions = sorted(actions, key=lambda x: float(x.get("ts", 0) or 0), reverse=True)[:limit] return { "code_actions": actions, @@ -1326,7 +1559,10 @@ async def get_grid_thinking_summary(limit: int = 8): rec = mission.recorder try: if hasattr(rec, "get_recent_parent_fabric_reasoning_traces"): - raw = rec.get_recent_parent_fabric_reasoning_traces_for_panel(limit=limit * 2) or [] + raw = ( + rec.get_recent_parent_fabric_reasoning_traces_for_panel(limit=limit * 2) + or [] + ) for t in raw: lift = t.get("expected_lift_signal") or t.get("expected_lift") or 0.0 if lift >= 0.02 or len(traces) < 3: # bias to high value @@ -1810,8 +2046,7 @@ def _emit(self, phase: str, **fields: Any) -> None: **{ k: v for k, v in fields.items() - if k - not in ("log_line", "current_fabric_coherence", *recognized) + if k not in ("log_line", "current_fabric_coherence", *recognized) }, }, ) diff --git a/src/agentdrive/mission_control/static/index.html b/src/agentdrive/mission_control/static/index.html index 097d495..d69dc79 100644 --- a/src/agentdrive/mission_control/static/index.html +++ b/src/agentdrive/mission_control/static/index.html @@ -112,7 +112,9 @@ .event-overseer_state { border-left-color: #14b8a6; } .event-static_fire { border-left-color: #f97316; } .event-grid_health { border-left-color: #4ade80; } + .event-multiverse_update { border-left-color: #f472b6; } .event-system { border-left-color: #64748b; } + .mv-branch-collapsed { border-color: #f472b6 !important; background: rgba(244,114,182,0.12) !important; } /* AgentDrive-native Kanban (reworked for 6-step + fabric) */ .kanban-lane { @@ -592,6 +594,30 @@ + +
+
+
+
+ MULTIVERSE COGNITION + HEURISTIC +
+
+
SESSION
+
STATUS
idle
+
BRANCHES
0
+
COLLAPSED
+
+
ROBUST INVARIANTS
+
    +
    +
    +
    PARALLEL BRANCHES
    +
    +
    +
    +
    +
    @@ -1077,7 +1103,7 @@ let reconnectTimer = null; let reconnectDelay = 800; const MAX_EVENTS = 260; - let filterTypes = new Set(["loop_step", "fabric_update", "parent_decision", "overseer_state", "static_fire", "grid_health", "dream_phase"]); + let filterTypes = new Set(["loop_step", "fabric_update", "parent_decision", "overseer_state", "static_fire", "grid_health", "dream_phase", "multiverse_update"]); const STEP_DEFS = [ { num: 1, label: "EXPERIENCE", short: "1. SIG GEN", desc: "Experience Layer + Runtime generating signals" }, @@ -1966,6 +1992,36 @@ while (container.children.length > 7) container.removeChild(container.lastChild); } + // === MULTIVERSE COGNITION PANEL (M5) === + function updateMultiverseFromState(mv) { + if (!mv || typeof mv !== "object") return; + const sid = mv.active_session_id || mv.session_id || "—"; + const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val ?? "—"; }; + set("mv-session", sid.length > 28 ? sid.slice(0, 28) + "…" : sid); + set("mv-status", (mv.session_status || mv.status || "idle").toString()); + set("mv-branch-count", String(mv.branch_count ?? (mv.branches || []).length ?? 0)); + set("mv-collapsed", mv.collapsed_branch_id || "—"); + const modeEl = document.getElementById("mv-llm-mode"); + if (modeEl) modeEl.textContent = (mv.llm_mode || "heuristic").toUpperCase(); + const invUl = document.getElementById("mv-invariants"); + if (invUl) { + const invs = mv.top_invariants || mv.invariants || []; + invUl.innerHTML = invs.length + ? invs.slice(0, 5).map(i => `
  • ${String(i).slice(0, 90)}
  • `).join("") + : "
  • "; + } + const brDiv = document.getElementById("mv-branches"); + if (brDiv) { + const branches = mv.branches || mv.branches_summary || []; + brDiv.innerHTML = branches.length + ? branches.map(b => { + const collapsed = b.collapsed || b.id === mv.collapsed_branch_id; + return `
    ${b.role || "?"} • ${(b.id || b.branch_id || "").slice(0, 22)} r=${fmtNum(b.robustness ?? b.robustness_score ?? 0)}
    `; + }).join("") + : "
    No branches yet
    "; + } + } + // === GRID TELEMETRY === function updateGrid(health) { const deck = document.getElementById("grid-deck"); @@ -2498,6 +2554,7 @@ } if (data.fabric) updateFabricFromEvent({data: data.fabric}); // v1.5 + if (data.multiverse) updateMultiverseFromState(data.multiverse); if (data.grid_health) updateGrid(data.grid_health); updateHeader(); @@ -2590,6 +2647,9 @@ // Tight Experience Layer: FabricUpdate (from densif, daily, recorder, Gardener) now visibly drives the structural briefing panel setTimeout(() => { try { refreshExperienceFabric(); } catch(_) {} }, 140); } + else if (et.includes("multiverse")) { + updateMultiverseFromState(evt.data || evt); + } else if (et.includes("parent_decision")) { addDecisionToTimeline(evt); state.globalCoherence = evt.data?.fabric_coherence_at_decision || state.globalCoherence; diff --git a/src/agentdrive/operations/__init__.py b/src/agentdrive/operations/__init__.py index 0c2349d..c7b07db 100644 --- a/src/agentdrive/operations/__init__.py +++ b/src/agentdrive/operations/__init__.py @@ -26,4 +26,4 @@ "parse_operation_kwargs", "register_operations_as_mcp_tools", "run_operation", -] \ No newline at end of file +] diff --git a/src/agentdrive/operations/mcp_bridge.py b/src/agentdrive/operations/mcp_bridge.py index 7f6da6b..2c90826 100644 --- a/src/agentdrive/operations/mcp_bridge.py +++ b/src/agentdrive/operations/mcp_bridge.py @@ -6,7 +6,7 @@ from collections.abc import Callable from typing import Any -from agentdrive.operations.registry import OPERATIONS, run_operation +from agentdrive.operations.registry import OPERATIONS, OperationSpec, run_operation OperationToolFn = Callable[..., str] @@ -19,6 +19,39 @@ def _default_mcp_tool_name(op_name: str, mcp_tool: str | None) -> str | None: return f"agentdrive_{op_name}" +def _rich_doc_for_op(op: "OperationSpec") -> str: + """Produce a model-friendly, self-contained docstring for MCP tool registration. + + Any AI model (Grok, Claude, Cursor, local LLM, custom agent) benefits from + explicit guidance on category, mutability, usage, and return shape. + """ + lines: list[str] = [op.description.strip()] + + lines.append(f"\n[category={op.category}] [read_only={op.read_only}]") + if op.read_only: + lines.append("Safe for frequent / exploratory calls. No side effects on the Drive.") + else: + lines.append( + "Mutating operation — use with care and prefer dry_run=True first when available." + ) + + if getattr(op, "when_to_use", None): + lines.append(f"\nWhen to use: {op.when_to_use}") + + if getattr(op, "examples", None): + exs = op.examples or [] + lines.append("Examples: " + " | ".join(exs[:3])) + + lines.append( + "\nReturns: Always a JSON string (parse it!). Supports dry_run=True for most ops " + "(returns a plan instead of executing). All results include an 'operation' and 'success' field for reliable handling by any model." + ) + lines.append( + "Tip for arbitrary models: Call agentdrive_mcp_catalog() early in a conversation to get the full live list of tools with usage notes." + ) + return "\n".join(lines) + + def _make_op_tool(op_name: str, description: str) -> OperationToolFn: def tool_fn( arguments: dict[str, Any] | None = None, @@ -52,8 +85,15 @@ def register_operations_as_mcp_tools( continue if tool_name in skip: continue - fn = _make_op_tool(op.name, op.description) - mcp.add_tool(fn, name=tool_name, description=op.description) + rich_doc = _rich_doc_for_op(op) + fn = _make_op_tool(op.name, rich_doc) + # Pass readOnlyHint so capable MCP clients / models can reason about safety + annotations: dict[str, Any] | None = {"readOnlyHint": bool(op.read_only)} + try: + mcp.add_tool(fn, name=tool_name, description=op.description, annotations=annotations) + except TypeError: + # Older FastMCP may not accept annotations kwarg — fall back gracefully + mcp.add_tool(fn, name=tool_name, description=op.description) registered.append(tool_name) skip.add(tool_name) return registered @@ -64,4 +104,4 @@ def existing_mcp_tool_names(mcp: Any) -> set[str]: try: return set(mcp._tool_manager._tools.keys()) # noqa: SLF001 except Exception: - return set() \ No newline at end of file + return set() diff --git a/src/agentdrive/operations/registry.py b/src/agentdrive/operations/registry.py index d4af8aa..294db8f 100644 --- a/src/agentdrive/operations/registry.py +++ b/src/agentdrive/operations/registry.py @@ -8,11 +8,14 @@ from __future__ import annotations import json +import logging from collections.abc import Callable from dataclasses import asdict, dataclass from pathlib import Path from typing import Any +logger = logging.getLogger(__name__) + # --------------------------------------------------------------------------- # Types # --------------------------------------------------------------------------- @@ -20,7 +23,11 @@ @dataclass(frozen=True) class OperationSpec: - """Declarative contract for one AgentDrive operation.""" + """Declarative contract for one AgentDrive operation. + + Used for CLI surfaces, MCP tool registration, and self-describing catalogs + exposed to any AI model via MCP. + """ name: str description: str @@ -28,6 +35,9 @@ class OperationSpec: read_only: bool cli_command: str | None = None mcp_tool: str | None = None + # Rich metadata to help arbitrary AI models (Claude, Grok, Cursor, local LLMs, custom agents) decide when/how to call + when_to_use: str = "" + examples: list[str] | None = None # short natural language or arg examples OperationHandler = Callable[..., dict[str, Any]] @@ -47,9 +57,7 @@ def _dry_plan(operation: str, **plan: Any) -> dict[str, Any]: def _handler_think(**kwargs: Any) -> dict[str, Any]: question = str( - kwargs.get("question") - or kwargs.get("text") - or "What should I know about this AgentDrive?" + kwargs.get("question") or kwargs.get("text") or "What should I know about this AgentDrive?" ) dry_run = bool(kwargs.get("dry_run", False)) if dry_run: @@ -102,13 +110,17 @@ def _handler_pool_query(**kwargs: Any) -> dict[str, Any]: def _handler_pool_status(**kwargs: Any) -> dict[str, Any]: + dry_run = bool(kwargs.get("dry_run", False)) + if dry_run: + return _success(operation="pool_status", dry_run=True, stats={}) + from agentdrive.drive.drive import get_default_drive pool = get_default_drive() stats = pool.get_pool_stats() return _success( operation="pool_status", - dry_run=bool(kwargs.get("dry_run", False)), + dry_run=False, stats=stats, ) @@ -170,8 +182,8 @@ def _handler_reconcile_scan(**kwargs: Any) -> dict[str, Any]: return _dry_plan("reconcile_scan", would_scan=True) from agentdrive.drive.drive import get_default_drive - from agentdrive.registry import GenomeRegistry from agentdrive.reconciliation import ReconciliationRunner + from agentdrive.registry import GenomeRegistry pool = get_default_drive() registry = pool.registry if hasattr(pool, "registry") else GenomeRegistry() @@ -331,7 +343,12 @@ def _handler_patterns_list(**kwargs: Any) -> dict[str, Any]: } for p in patterns ] - return _success(operation="patterns_list", dry_run=bool(kwargs.get("dry_run", False)), patterns=rows, count=len(rows)) + return _success( + operation="patterns_list", + dry_run=bool(kwargs.get("dry_run", False)), + patterns=rows, + count=len(rows), + ) def _handler_patterns_show(**kwargs: Any) -> dict[str, Any]: @@ -344,7 +361,11 @@ def _handler_patterns_show(**kwargs: Any) -> dict[str, Any]: try: record = get_pattern(str(name)) except PatternNotFoundError: - return {"success": False, "error": f"pattern not found: {name}", "operation": "patterns_show"} + return { + "success": False, + "error": f"pattern not found: {name}", + "operation": "patterns_show", + } manifest = record.manifest return _success( @@ -363,7 +384,11 @@ def _handler_patterns_apply(**kwargs: Any) -> dict[str, Any]: name = kwargs.get("pattern_name") or kwargs.get("name") or kwargs.get("text") input_text = str(kwargs.get("input") or kwargs.get("input_text") or "") if not name: - return {"success": False, "error": "pattern_name is required", "operation": "patterns_apply"} + return { + "success": False, + "error": "pattern_name is required", + "operation": "patterns_apply", + } if bool(kwargs.get("dry_run", False)): return _dry_plan("patterns_apply", pattern_name=str(name), input_preview=input_text[:200]) @@ -372,7 +397,11 @@ def _handler_patterns_apply(**kwargs: Any) -> dict[str, Any]: try: prompt = apply_pattern(str(name), input_text) except PatternNotFoundError: - return {"success": False, "error": f"pattern not found: {name}", "operation": "patterns_apply"} + return { + "success": False, + "error": f"pattern not found: {name}", + "operation": "patterns_apply", + } return _success(operation="patterns_apply", pattern_name=str(name), prompt=prompt) @@ -402,7 +431,9 @@ def _handler_patterns_import_fabric(**kwargs: Any) -> dict[str, Any]: fabric_root = resolve_fabric_root(source) dest_root = get_agentdrive_home() / "patterns" if pattern_name: - imported = [import_fabric_pattern(fabric_root, str(pattern_name), dest_root, overwrite=overwrite)] + imported = [ + import_fabric_pattern(fabric_root, str(pattern_name), dest_root, overwrite=overwrite) + ] else: imported = import_fabric_corpus(fabric_root, limit=limit, overwrite=overwrite) return _success( @@ -421,7 +452,9 @@ def _handler_dream_run(**kwargs: Any) -> dict[str, Any]: from agentdrive.dreaming.cycle import run_dream_cycle - results = run_dream_cycle(dry_run=dry_run, ack_phases=ack_phases or None, acquire_lock=not dry_run) + results = run_dream_cycle( + dry_run=dry_run, ack_phases=ack_phases or None, acquire_lock=not dry_run + ) return _success( operation="dream_run", dry_run=dry_run, @@ -442,7 +475,9 @@ def _handler_dream_status(**kwargs: Any) -> dict[str, Any]: from agentdrive.dreaming.cycle import get_dream_cycle_status status = get_dream_cycle_status() - return _success(operation="dream_status", dry_run=bool(kwargs.get("dry_run", False)), status=status) + return _success( + operation="dream_status", dry_run=bool(kwargs.get("dry_run", False)), status=status + ) def _handler_cap_mint_mission(**kwargs: Any) -> dict[str, Any]: @@ -488,7 +523,9 @@ def _handler_experience_graph_context_pack(**kwargs: Any) -> dict[str, Any]: lookback_days=lookback_days, max_tokens=max_tokens, ) - return _success(operation="experience_graph_context_pack", swarm_id=effective, context_pack=pack) + return _success( + operation="experience_graph_context_pack", swarm_id=effective, context_pack=pack + ) def _handler_experience_graph_record_reasoning(**kwargs: Any) -> dict[str, Any]: @@ -500,7 +537,9 @@ def _handler_experience_graph_record_reasoning(**kwargs: Any) -> dict[str, Any]: if reasoning is None: reasoning = { - "summary": str(kwargs.get("summary") or kwargs.get("text") or "ops-registry dry reasoning"), + "summary": str( + kwargs.get("summary") or kwargs.get("text") or "ops-registry dry reasoning" + ), "elements": list(kwargs.get("elements") or []), } if not isinstance(reasoning, dict): @@ -541,14 +580,293 @@ def _handler_experience_graph_suggest_reasoning(**kwargs: Any) -> dict[str, Any] ) +def _multiverse_engine(swarm_id: str | None, **engine_kwargs: Any): + from agentdrive.cognition import MultiverseEngine + + effective, recorder = _integrated_recorder(swarm_id) + return effective, MultiverseEngine(recorder, **engine_kwargs) + + +def _handler_multiverse_run_full(**kwargs: Any) -> dict[str, Any]: + trigger = str(kwargs.get("trigger") or kwargs.get("text") or kwargs.get("question") or "") + if not trigger: + return { + "success": False, + "error": "trigger is required", + "operation": "multiverse_run_full", + } + + n_branches = int(kwargs.get("n_branches", kwargs.get("branches", 7))) + forward_steps = kwargs.get("forward_steps") + program_id = kwargs.get("program_id") + dry_run = bool(kwargs.get("dry_run", False)) + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + + if dry_run: + return _dry_plan( + "multiverse_run_full", + swarm_id=effective, + trigger=trigger[:200], + n_branches=n_branches, + forward_steps=forward_steps, + ) + + engine_kwargs: dict[str, Any] = {} + if program_id: + engine_kwargs["program_id"] = str(program_id) + if kwargs.get("user_objective_refs"): + engine_kwargs["user_objective_refs"] = list(kwargs["user_objective_refs"]) + + _, engine = _multiverse_engine(kwargs.get("swarm_id"), **engine_kwargs) + session = engine.run_full( + trigger, + n_branches=n_branches, + forward_steps=int(forward_steps) if forward_steps is not None else None, + ) + fabric_reasoning = engine.to_fabric_reasoning(session) + _, recorder = _integrated_recorder(kwargs.get("swarm_id")) + trace_slug = recorder.record_parent_fabric_reasoning(session.cycle_id, fabric_reasoning) + + return _success( + operation="multiverse_run_full", + swarm_id=effective, + session=engine.to_mcp_dict(session), + fabric_reasoning_trace_slug=trace_slug, + ) + + +def _handler_multiverse_get_session(**kwargs: Any) -> dict[str, Any]: + session_id = str(kwargs.get("session_id") or "") + if not session_id: + return { + "success": False, + "error": "session_id is required", + "operation": "multiverse_get_session", + } + + dry_run = bool(kwargs.get("dry_run", False)) + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + if dry_run: + return _dry_plan("multiverse_get_session", swarm_id=effective, session_id=session_id) + + _, engine = _multiverse_engine(kwargs.get("swarm_id")) + session = engine.get_session(session_id) + if session is None: + return { + "success": False, + "error": f"session not found: {session_id}", + "operation": "multiverse_get_session", + } + return _success( + operation="multiverse_get_session", + swarm_id=effective, + session=engine.to_mcp_dict(session), + ) + + +def _handler_multiverse_list_sessions(**kwargs: Any) -> dict[str, Any]: + limit = int(kwargs.get("limit", 10)) + dry_run = bool(kwargs.get("dry_run", False)) + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + if dry_run: + return _dry_plan("multiverse_list_sessions", swarm_id=effective, limit=limit) + + _, engine = _multiverse_engine(kwargs.get("swarm_id")) + sessions = engine.list_sessions(limit=limit) + return _success( + operation="multiverse_list_sessions", + swarm_id=effective, + count=len(sessions), + sessions=[engine.to_mcp_dict(s) for s in sessions], + briefing_context=engine.briefing_context(limit=min(limit, 5)), + ) + + +def _handler_multiverse_parent_decision(**kwargs: Any) -> dict[str, Any]: + """Integrated loop hook: multiverse pipeline + record_parent_decision.""" + trigger = str(kwargs.get("trigger") or kwargs.get("text") or kwargs.get("question") or "") + if not trigger: + return { + "success": False, + "error": "trigger is required", + "operation": "multiverse_parent_decision", + } + + dry_run = bool(kwargs.get("dry_run", False)) + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + if dry_run: + return _dry_plan( + "multiverse_parent_decision", + swarm_id=effective, + trigger=trigger[:200], + n_branches=int(kwargs.get("n_branches", kwargs.get("branches", 7))), + ) + + from agentdrive.system.integrated_real_time_evolution_system import ( + IntegratedRealTimeEvolutionSystem, + ) + + system = IntegratedRealTimeEvolutionSystem(swarm_id=effective) + payload = system.run_multiverse_parent_decision( + trigger, + n_branches=int(kwargs.get("n_branches", kwargs.get("branches", 7))), + forward_steps=int(kwargs["forward_steps"]) + if kwargs.get("forward_steps") is not None + else None, + program_id=kwargs.get("program_id"), + user_objective_refs=list(kwargs["user_objective_refs"]) + if kwargs.get("user_objective_refs") + else None, + record_decision=not bool(kwargs.get("skip_record", False)), + durable=bool(kwargs.get("durable", False)), + densify_invariants=not bool(kwargs.get("skip_densify", False)), + use_llm=not bool(kwargs.get("heuristic_only", False)), + ) + return _success(operation="multiverse_parent_decision", swarm_id=effective, result=payload) + + +def _unwrap_mcp_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]: + """Flatten nested ``arguments`` dicts from auto-registered MCP tool calls.""" + nested = kwargs.get("arguments") + if isinstance(nested, dict): + merged = dict(nested) + merged.update({k: v for k, v in kwargs.items() if k != "arguments"}) + return merged + return kwargs + + +def _handler_external_parent_decision(**kwargs: Any) -> dict[str, Any]: + """MCP frontier/local chat models submit multiverse branches; AgentDrive records collapse.""" + kwargs = _unwrap_mcp_kwargs(dict(kwargs)) + trigger = str(kwargs.get("trigger") or kwargs.get("text") or kwargs.get("question") or "") + branches = kwargs.get("branches") + collapsed_branch_id = str(kwargs.get("collapsed_branch_id") or "") + if not trigger: + return { + "success": False, + "error": "trigger is required", + "operation": "external_parent_decision", + } + if not isinstance(branches, list) or not branches: + return { + "success": False, + "error": "branches must be a non-empty list", + "operation": "external_parent_decision", + } + if not collapsed_branch_id: + return { + "success": False, + "error": "collapsed_branch_id is required", + "operation": "external_parent_decision", + } + + dry_run = bool(kwargs.get("dry_run", False)) + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + if dry_run: + return _dry_plan( + "external_parent_decision", + swarm_id=effective, + trigger=trigger[:200], + branch_count=len(branches), + collapsed_branch_id=collapsed_branch_id, + reasoning_provider=kwargs.get("reasoning_provider", "mcp-external"), + ) + + from agentdrive.system.integrated_real_time_evolution_system import ( + IntegratedRealTimeEvolutionSystem, + ) + + system = IntegratedRealTimeEvolutionSystem(swarm_id=effective) + payload = system.run_external_parent_decision( + trigger, + branches, + collapsed_branch_id=collapsed_branch_id, + invariants=list(kwargs["invariants"]) if kwargs.get("invariants") else None, + collapse_reason=str(kwargs.get("collapse_reason") or ""), + collapse_policy=str(kwargs["collapse_policy"]) if kwargs.get("collapse_policy") else None, + reasoning_provider=str(kwargs.get("reasoning_provider") or "mcp-external"), + convergence_points=list(kwargs["convergence_points"]) + if kwargs.get("convergence_points") + else None, + divergence_points=list(kwargs["divergence_points"]) + if kwargs.get("divergence_points") + else None, + fabric_reasoning=dict(kwargs["fabric_reasoning"]) + if isinstance(kwargs.get("fabric_reasoning"), dict) + else None, + program_id=kwargs.get("program_id"), + user_objective_refs=list(kwargs["user_objective_refs"]) + if kwargs.get("user_objective_refs") + else None, + record_decision=not bool(kwargs.get("skip_record", False)), + densify_invariants=not bool(kwargs.get("skip_densify", False)), + ) + return _success(operation="external_parent_decision", swarm_id=effective, result=payload) + + +def _handler_multiverse_reopen_stale(**kwargs: Any) -> dict[str, Any]: + max_age = float(kwargs.get("max_age_hours", 24.0)) + dry_run = bool(kwargs.get("dry_run", False)) + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + if dry_run: + return _dry_plan("multiverse_reopen_stale", swarm_id=effective, max_age_hours=max_age) + + from agentdrive.system.integrated_real_time_evolution_system import ( + IntegratedRealTimeEvolutionSystem, + ) + + reopened = IntegratedRealTimeEvolutionSystem( + swarm_id=effective + ).reopen_stale_multiverse_sessions(max_age_hours=max_age) + return _success( + operation="multiverse_reopen_stale", + swarm_id=effective, + reopened_count=len(reopened), + reopened_session_ids=reopened, + ) + + +def _handler_multiverse_densify(**kwargs: Any) -> dict[str, Any]: + session_id = str(kwargs.get("session_id") or "") + if not session_id: + return { + "success": False, + "error": "session_id is required", + "operation": "multiverse_densify", + } + dry_run = bool(kwargs.get("dry_run", False)) + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + if dry_run: + return _dry_plan("multiverse_densify", swarm_id=effective, session_id=session_id) + + from agentdrive.system.integrated_real_time_evolution_system import ( + IntegratedRealTimeEvolutionSystem, + ) + + result = IntegratedRealTimeEvolutionSystem(swarm_id=effective).densify_multiverse_invariants( + session_id + ) + return _success(operation="multiverse_densify", swarm_id=effective, result=result) + + def _handler_learnings_log(**kwargs: Any) -> dict[str, Any]: from agentdrive.learnings import LearningsStore + kwargs = _unwrap_mcp_kwargs(dict(kwargs)) dry_run = bool(kwargs.get("dry_run", False)) + outcome = kwargs.get("outcome") + if isinstance(outcome, dict): + kwargs.setdefault("insight", outcome.get("key_observation") or outcome.get("insight")) + kwargs.setdefault("confidence", outcome.get("confidence", 5)) entry = { "type": str(kwargs.get("type", "pattern")), - "key": str(kwargs.get("key") or "ops-registry-entry"), - "insight": str(kwargs.get("insight") or kwargs.get("text") or "ops registry learning entry"), + "key": str(kwargs.get("key") or kwargs.get("task") or "ops-registry-entry"), + "insight": str( + kwargs.get("insight") + or kwargs.get("text") + or (isinstance(outcome, dict) and outcome.get("key_observation")) + or "ops registry learning entry" + ), "confidence": int(kwargs.get("confidence", 5)), "source": str(kwargs.get("source", "observed")), "skill": str(kwargs.get("skill", "harness")), @@ -558,7 +876,17 @@ def _handler_learnings_log(**kwargs: Any) -> dict[str, Any]: store = LearningsStore(slug=kwargs.get("slug")) record = store.log(entry) - return _success(operation="learnings_log", slug=store.slug, record=record) + result = _success(operation="learnings_log", slug=store.slug, record=record) + try: + from agentdrive.memory.ingest import ingest_from_learning + + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + mem = ingest_from_learning(record, swarm_id=effective) + if mem: + result["memory"] = mem + except Exception: + pass + return result def _handler_learnings_list(**kwargs: Any) -> dict[str, Any]: @@ -579,7 +907,9 @@ def _handler_learnings_list(**kwargs: Any) -> dict[str, Any]: def _handler_harness_compose(**kwargs: Any) -> dict[str, Any]: from agentdrive.harness import Harness - base_prompt = str(kwargs.get("base_prompt") or kwargs.get("prompt") or "You are an AgentDrive agent.") + base_prompt = str( + kwargs.get("base_prompt") or kwargs.get("prompt") or "You are an AgentDrive agent." + ) task = str(kwargs.get("task") or kwargs.get("text") or "") dry_run = bool(kwargs.get("dry_run", False)) agent_id = str(kwargs.get("agent_id", "ops-registry")) @@ -624,6 +954,11 @@ def _handler_harness_compose(**kwargs: Any) -> dict[str, Any]: category="synthesis", read_only=True, cli_command="agentdrive think", + when_to_use="Use for any non-trivial question where you need fused evidence from the pool + Experience Graph + honest gap reporting. Always call with prefer_experience_layer=True unless you explicitly want pure LLM mode.", + examples=[ + "think(question='How should I structure long-horizon agent memory?')", + "think with dry_run first to plan", + ], mcp_tool="agentdrive_think", ), OperationSpec( @@ -632,6 +967,8 @@ def _handler_harness_compose(**kwargs: Any) -> dict[str, Any]: category="drive", read_only=True, cli_command="agentdrive drive query", + when_to_use="First-line retrieval before any synthesis or planning. Returns ranked genomes with relevance scores and framework steps. Prefer over raw vector search when you want Drive-native DNA packets.", + examples=["pool_query(task='self-healing patterns for distributed agents', limit=6)"], mcp_tool="agentdrive_pool_query", ), OperationSpec( @@ -688,6 +1025,8 @@ def _handler_harness_compose(**kwargs: Any) -> dict[str, Any]: category="system", read_only=True, cli_command="agentdrive doctor", + when_to_use="Call at the start of any session or when the user complains about missing data/tools. Also great for models to self-check their environment before heavy work.", + examples=["doctor(verbose=True)"], mcp_tool="agentdrive_doctor", ), OperationSpec( @@ -794,6 +1133,74 @@ def _handler_harness_compose(**kwargs: Any) -> dict[str, Any]: cli_command="agentdrive graph suggest", mcp_tool="experience_graph_suggest_reasoning_structure", ), + OperationSpec( + name="multiverse_run_full", + description="Run full multiverse cognition: spawn branches, simulate, extract invariants, stress-test, collapse, record fabric reasoning", + category="multiverse", + read_only=False, + when_to_use="Call on any non-trivial Parent decision where multiple competing paths exist. Spawns Cognitive Agent Team role branches, holds superposition, collapses to one governed path, and writes Experience Graph DNA.", + examples=[ + 'multiverse_run_full(trigger="How should we ship feature X?", n_branches=7)', + ], + mcp_tool="multiverse_run_full", + ), + OperationSpec( + name="multiverse_get_session", + description="Return a persisted multiverse session by id", + category="multiverse", + read_only=True, + cli_command="agentdrive multiverse status", + mcp_tool="multiverse_get_session", + ), + OperationSpec( + name="multiverse_list_sessions", + description="List recent multiverse sessions and briefing context", + category="multiverse", + read_only=True, + cli_command="agentdrive multiverse list", + mcp_tool="multiverse_list_sessions", + ), + OperationSpec( + name="multiverse_parent_decision", + description="Full multiverse pipeline wired into record_parent_decision (canonical Parent hook)", + category="multiverse", + read_only=False, + when_to_use="Preferred entry for non-trivial Parent decisions inside the 6-step loop. Runs spawn→simulate→invariants→stress-test→collapse→record_parent_decision in one call.", + examples=[ + 'multiverse_parent_decision(trigger="How should we ship feature X?", n_branches=7)', + ], + cli_command="agentdrive multiverse run", + mcp_tool="multiverse_parent_decision", + ), + OperationSpec( + name="external_parent_decision", + description="Submit externally-reasoned multiverse branches (Grok/Claude/Codex MCP) and record Parent DNA", + category="multiverse", + read_only=False, + when_to_use=( + "Use when YOU (the connected MCP model) perform multiverse branch reasoning in your own " + "context and need AgentDrive to persist the collapse. Call after experience_graph_get_context_pack " + "and experience_graph_suggest_reasoning_structure. Sets llm_mode=external." + ), + examples=[ + 'external_parent_decision(trigger="...", branches=[{role, path_summary, ...}], collapsed_branch_id="branch:operator-0", reasoning_provider="grok")', + ], + mcp_tool="external_parent_decision", + ), + OperationSpec( + name="multiverse_reopen_stale", + description="Reopen stale open multiverse superposition sessions (M4 durable threads)", + category="multiverse", + read_only=False, + mcp_tool="multiverse_reopen_stale", + ), + OperationSpec( + name="multiverse_densify", + description="GraphGardener densification on multiverse robust invariant clusters (M3)", + category="multiverse", + read_only=False, + mcp_tool="multiverse_densify", + ), OperationSpec( name="learnings_log", description="Append one gstack-style operational learning entry", @@ -801,6 +1208,10 @@ def _handler_harness_compose(**kwargs: Any) -> dict[str, Any]: read_only=False, cli_command="agentdrive learnings log", mcp_tool="agentdrive_learnings_log", + when_to_use="Call after any non-trivial task (success or failure). The entries become queryable by future think / retrieval and improve the model's own long-term performance on this user's problems.", + examples=[ + "learnings_log(task='debugged MCP tool schema', outcome={'quality': 0.85, 'key_observation': '...'})" + ], ), OperationSpec( name="learnings_list", @@ -818,8 +1229,762 @@ def _handler_harness_compose(**kwargs: Any) -> dict[str, Any]: cli_command="agentdrive harness compose", mcp_tool="agentdrive_harness_compose", ), + OperationSpec( + name="codebase_register_project", + description="Register a codebase root for pattern recognition learning", + category="codebase", + read_only=False, + when_to_use="Call once per repo before observing files. Enables safe path-scoped pattern learning.", + examples=[ + 'codebase_register_project(project_id="interegy-web", root="/path/to/app")', + ], + mcp_tool="codebase_register_project", + ), + OperationSpec( + name="codebase_observe_file", + description="Read a file under a registered project and learn its writing patterns", + category="codebase", + read_only=False, + when_to_use="Call whenever you inspect project source — builds the project's pattern recognition framework automatically.", + examples=[ + 'codebase_observe_file(project_id="interegy-web", path="lib/gateway.ts")', + ], + mcp_tool="codebase_observe_file", + ), + OperationSpec( + name="codebase_patterns_profile", + description="Return the auto-learned writing-style framework for a project", + category="codebase", + read_only=True, + when_to_use="Before writing or reviewing code in a project AD has observed — get naming, imports, framework, and convention patterns.", + mcp_tool="codebase_patterns_profile", + ), + OperationSpec( + name="codebase_patterns_match", + description="Check a code snippet against the learned project writing framework", + category="codebase", + read_only=True, + when_to_use="Before proposing a patch — verify alignment with how the codebase is actually written.", + mcp_tool="codebase_patterns_match", + ), + OperationSpec( + name="codebase_list_projects", + description="List registered codebases and observation stats", + category="codebase", + read_only=True, + mcp_tool="codebase_list_projects", + ), + OperationSpec( + name="codebase_mimic", + description="Fire mirror neurons — return motor programs + mimicry prompt to write like the project", + category="codebase", + read_only=True, + when_to_use="Before writing new code in an observed project. Observation activates the same writing circuits (mirror-neuron mimicry).", + examples=['codebase_mimic(project_id="interegy-web", intent="gateway fetch helper")'], + mcp_tool="codebase_mimic", + ), + OperationSpec( + name="codebase_transform_style", + description="Transform a code snippet toward the learned project writing style", + category="codebase", + read_only=True, + when_to_use="When you drafted generic code and need it to match how the repo is actually written.", + mcp_tool="codebase_transform_style", + ), + OperationSpec( + name="codebase_mirror_resonance", + description="Cross-project mirror field — universal priors shared across observed repos", + category="codebase", + read_only=True, + when_to_use="See which writing patterns resonate across all projects AD has observed (like shared mirror-neuron firing).", + mcp_tool="codebase_mirror_resonance", + ), + OperationSpec( + name="synthesize_fused_skill", + description="Birth a new skill by fusing experience traces, parent skills, and codebase patterns", + category="skills", + read_only=False, + when_to_use=( + "When a session combined Experience Graph work, distilled/inherited skills, " + "and repo patterns — merge them into one born playbook (not a copy of any parent)." + ), + examples=[ + 'synthesize_fused_skill(trigger="Ship gateway helper", source_skills=["auto-think-x"], pattern_projects=["interegy-web"])', + ], + mcp_tool="synthesize_fused_skill", + ), + OperationSpec( + name="memory_bank_store", + description="Store a new memory in the AI's deep Memory Bank databank", + category="memory", + read_only=False, + when_to_use="When the AI or user wants to persist knowledge that should compound across all future sessions.", + examples=[ + 'memory_bank_store(kind="fact", title="Gateway auth", content="Uses X-Ren-API-Key not bootstrap key")', + ], + mcp_tool="memory_bank_store", + ), + OperationSpec( + name="memory_bank_recall", + description="Recall one memory by id from the Memory Bank", + category="memory", + read_only=True, + mcp_tool="memory_bank_recall", + ), + OperationSpec( + name="memory_bank_search", + description="BM25 + lexical ranked search over Memory Bank", + category="memory", + read_only=True, + when_to_use="Before acting on a task — scoped by vault/topic when provided.", + mcp_tool="memory_bank_search", + ), + OperationSpec( + name="memory_bank_list", + description="List recent memories in the Memory Bank", + category="memory", + read_only=True, + mcp_tool="memory_bank_list", + ), + OperationSpec( + name="memory_bank_briefing", + description="Dense Memory Bank briefing for session grounding", + category="memory", + read_only=True, + when_to_use="Start of session — your custom AI memory databank, always growing from AgentDrive work.", + mcp_tool="memory_bank_briefing", + ), + OperationSpec( + name="memory_bank_deep_briefing", + description="Unified briefing: Experience Graph fabric pack + Memory Bank", + category="memory", + read_only=True, + when_to_use="Maximum grounding — structural graph memory + deep personal memory bank in one call.", + mcp_tool="memory_bank_deep_briefing", + ), + OperationSpec( + name="memory_bank_stats", + description="Memory Bank statistics (counts by kind, sources, path)", + category="memory", + read_only=True, + mcp_tool="memory_bank_stats", + ), + OperationSpec( + name="memory_bank_anchor", + description="Session anchor: agent brief + essential memories + optional scoped recall", + category="memory", + read_only=True, + when_to_use="Session start — ~600-900 token grounding from ~/.agentdrive/identity.txt + top memories.", + mcp_tool="memory_bank_anchor", + ), + OperationSpec( + name="memory_bank_import_dialogue", + description="Import JSONL/text dialogue transcripts into full-text memory shards", + category="memory", + read_only=False, + when_to_use="Backfill Claude/Cursor/Grok session JSONL into the memory bank without summarization.", + examples=[ + 'memory_bank_import_dialogue(path="~/.claude/projects/", vault="claude-sessions")', + ], + mcp_tool="memory_bank_import_dialogue", + ), + OperationSpec( + name="memory_relation_record", + description="Record a time-bounded subject–predicate–object relation in the swarm graph", + category="memory", + read_only=False, + mcp_tool="memory_relation_record", + ), + OperationSpec( + name="memory_relation_query", + description="Query relation graph by entity (optional as_of date)", + category="memory", + read_only=True, + mcp_tool="memory_relation_query", + ), + OperationSpec( + name="memory_relation_expire", + description="Expire an active relation (set valid_to)", + category="memory", + read_only=False, + mcp_tool="memory_relation_expire", + ), + OperationSpec( + name="growth_merge_briefing", + description="Unified growth briefing: experience graph + pattern recognition + memory bank", + category="learning", + read_only=True, + when_to_use=( + "When you need compounding context — structural experience, recognized codebase " + "patterns, and merged personal memories in one call." + ), + mcp_tool="growth_merge_briefing", + ), + OperationSpec( + name="framework_session_start", + description="AgentDrive-as-framework session pack: anchor + growth + matched learned skills", + category="learning", + read_only=True, + when_to_use="Start of any task when AgentDrive is your framework — routes learned/fused skills for the work ahead.", + examples=[ + 'framework_session_start(task="wire growth merge into OpenMango", project_id="openmangos")' + ], + mcp_tool="framework_session_start", + ), + OperationSpec( + name="framework_skill_route", + description="Match learned/fused skills to the current task", + category="learning", + read_only=True, + when_to_use="Before acting — find which learned playbooks apply to this task.", + examples=['framework_skill_route(task="OpenMango context pack", project_id="openmangos")'], + mcp_tool="framework_skill_route", + ), + OperationSpec( + name="framework_skill_run", + description="Run a matched learned skill (bound operation or playbook body)", + category="learning", + read_only=False, + when_to_use="After framework_skill_route — execute the chosen learned/fused playbook.", + mcp_tool="framework_skill_run", + ), ] + +def _handler_codebase_register_project(**kwargs: Any) -> dict[str, Any]: + from agentdrive.codebase.registry import register_project + + project_id = str(kwargs.get("project_id") or kwargs.get("id") or "") + root = str(kwargs.get("root") or kwargs.get("path") or "") + if not project_id or not root: + return { + "success": False, + "error": "project_id and root are required", + "operation": "codebase_register_project", + } + dry_run = bool(kwargs.get("dry_run", False)) + if dry_run: + return _dry_plan( + "codebase_register_project", + project_id=project_id, + root=root, + ) + project = register_project( + project_id=project_id, + root=root, + display_name=str(kwargs.get("display_name") or ""), + primary_language=str(kwargs.get("primary_language") or ""), + ) + return _success( + operation="codebase_register_project", + project=project.to_dict(), + ) + + +def _handler_codebase_observe_file(**kwargs: Any) -> dict[str, Any]: + from agentdrive.codebase.observe import observe_file + + project_id = str(kwargs.get("project_id") or "") + path = str(kwargs.get("path") or kwargs.get("file") or "") + if not project_id or not path: + return { + "success": False, + "error": "project_id and path are required", + "operation": "codebase_observe_file", + } + dry_run = bool(kwargs.get("dry_run", False)) + if dry_run: + return _dry_plan("codebase_observe_file", project_id=project_id, path=path) + payload = observe_file( + project_id=project_id, + path=path, + max_lines=int(kwargs.get("max_lines", 400)), + auto_register_root=kwargs.get("auto_register_root"), + ) + if not payload.get("success"): + return {**payload, "operation": "codebase_observe_file", "success": False} + return _success(operation="codebase_observe_file", **payload) + + +def _handler_codebase_patterns_profile(**kwargs: Any) -> dict[str, Any]: + from agentdrive.codebase.framework import get_writing_guide + + project_id = str(kwargs.get("project_id") or "") + if not project_id: + return { + "success": False, + "error": "project_id is required", + "operation": "codebase_patterns_profile", + } + dry_run = bool(kwargs.get("dry_run", False)) + if dry_run: + return _dry_plan("codebase_patterns_profile", project_id=project_id) + framework = get_writing_guide(project_id) + return _success( + operation="codebase_patterns_profile", + project_id=project_id, + framework=framework, + ) + + +def _handler_codebase_patterns_match(**kwargs: Any) -> dict[str, Any]: + from agentdrive.codebase.framework import match_against_framework + + project_id = str(kwargs.get("project_id") or "") + code = str(kwargs.get("code") or kwargs.get("snippet") or "") + if not project_id or not code: + return { + "success": False, + "error": "project_id and code are required", + "operation": "codebase_patterns_match", + } + dry_run = bool(kwargs.get("dry_run", False)) + if dry_run: + return _dry_plan("codebase_patterns_match", project_id=project_id) + match = match_against_framework( + project_id, + code=code, + path=str(kwargs.get("path") or "snippet.py"), + ) + return _success(operation="codebase_patterns_match", **match) + + +def _handler_codebase_mimic(**kwargs: Any) -> dict[str, Any]: + from agentdrive.codebase.mirrors import fire_mirrors_for_intent + + project_id = str(kwargs.get("project_id") or "") + intent = str(kwargs.get("intent") or kwargs.get("task") or kwargs.get("text") or "") + if not project_id or not intent: + return { + "success": False, + "error": "project_id and intent are required", + "operation": "codebase_mimic", + } + dry_run = bool(kwargs.get("dry_run", False)) + if dry_run: + return _dry_plan("codebase_mimic", project_id=project_id, intent=intent[:120]) + payload = fire_mirrors_for_intent( + project_id, + intent=intent, + language=kwargs.get("language"), + limit=int(kwargs.get("limit", 5)), + ) + return _success(operation="codebase_mimic", **payload) + + +def _handler_codebase_transform_style(**kwargs: Any) -> dict[str, Any]: + from agentdrive.codebase.mirrors import transform_toward_style + + project_id = str(kwargs.get("project_id") or "") + code = str(kwargs.get("code") or kwargs.get("snippet") or "") + if not project_id or not code: + return { + "success": False, + "error": "project_id and code are required", + "operation": "codebase_transform_style", + } + dry_run = bool(kwargs.get("dry_run", False)) + if dry_run: + return _dry_plan("codebase_transform_style", project_id=project_id) + payload = transform_toward_style( + project_id, + code=code, + path=str(kwargs.get("path") or "snippet.py"), + ) + return _success(operation="codebase_transform_style", **payload) + + +def _handler_codebase_mirror_resonance(**kwargs: Any) -> dict[str, Any]: + from agentdrive.codebase.mirrors import global_mirror_field + + dry_run = bool(kwargs.get("dry_run", False)) + if dry_run: + return _dry_plan("codebase_mirror_resonance") + payload = global_mirror_field(limit=int(kwargs.get("limit", 12))) + return _success(operation="codebase_mirror_resonance", **payload) + + +def _handler_codebase_list_projects(**kwargs: Any) -> dict[str, Any]: + from agentdrive.codebase.registry import list_projects + + dry_run = bool(kwargs.get("dry_run", False)) + if dry_run: + return _dry_plan("codebase_list_projects") + projects = [p.to_dict() for p in list_projects()] + return _success(operation="codebase_list_projects", projects=projects, count=len(projects)) + + +def _memory_bank_store_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.memory.store import MemoryBankStore + + kind = str(kwargs.get("kind") or "insight") + title = str(kwargs.get("title") or "") + content = str(kwargs.get("content") or kwargs.get("text") or "") + if not title or not content: + return { + "success": False, + "error": "title and content are required", + "operation": "memory_bank_store", + } + dry_run = bool(kwargs.get("dry_run", False)) + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + if dry_run: + return _dry_plan("memory_bank_store", swarm_id=effective, kind=kind, title=title[:80]) + + store = MemoryBankStore(effective) + entry = store.store( + kind=kind, + title=title, + content=content, + confidence=float(kwargs.get("confidence", 0.8)), + source=str(kwargs.get("source") or "user"), + program_id=str(kwargs.get("program_id") or ""), + tags=list(kwargs.get("tags") or []), + links=list(kwargs.get("links") or []), + ) + return _success( + operation="memory_bank_store", + swarm_id=effective, + memory=entry.to_dict(), + ) + + +def _memory_bank_recall_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.memory.store import MemoryBankStore + + memory_id = str(kwargs.get("memory_id") or kwargs.get("id") or "") + if not memory_id: + return { + "success": False, + "error": "memory_id is required", + "operation": "memory_bank_recall", + } + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + entry = MemoryBankStore(effective).recall(memory_id) + if entry is None: + return { + "success": False, + "error": f"memory not found: {memory_id}", + "operation": "memory_bank_recall", + } + return _success(operation="memory_bank_recall", swarm_id=effective, memory=entry.to_dict()) + + +def _memory_scope_filters(kwargs: dict[str, Any]) -> tuple[str | None, str | None]: + vault = kwargs.get("vault") + topic = kwargs.get("topic") + return ( + str(vault) if vault else None, + str(topic) if topic else None, + ) + + +def _memory_bank_search_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.memory.store import MemoryBankStore + + query = str(kwargs.get("query") or kwargs.get("text") or kwargs.get("question") or "") + limit = int(kwargs.get("limit", 10)) + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + store = MemoryBankStore(effective) + vault, topic = _memory_scope_filters(kwargs) + memories = store.search( + query, + limit=limit, + kind=kwargs.get("kind"), + program_id=kwargs.get("program_id"), + vault=vault, + topic=topic, + ranked=not bool(kwargs.get("lexical_only", False)), + ) + return _success( + operation="memory_bank_search", + swarm_id=effective, + query=query, + count=len(memories), + memories=[m.to_dict() for m in memories], + ) + + +def _memory_bank_list_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.memory.store import MemoryBankStore + + limit = int(kwargs.get("limit", 20)) + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + store = MemoryBankStore(effective) + memories = store.list_recent(limit=limit, kind=kwargs.get("kind")) + return _success( + operation="memory_bank_list", + swarm_id=effective, + count=len(memories), + memories=[m.to_dict() for m in memories], + ) + + +def _memory_bank_briefing_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.memory.briefing import build_memory_briefing + + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + pack = build_memory_briefing( + effective, + query=str(kwargs.get("query") or kwargs.get("text") or ""), + limit=int(kwargs.get("limit", 12)), + program_id=kwargs.get("program_id"), + ) + return _success(operation="memory_bank_briefing", swarm_id=effective, **pack) + + +def _memory_bank_deep_briefing_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.memory.briefing import build_deep_briefing + + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + pack = build_deep_briefing( + effective, + query=str(kwargs.get("query") or kwargs.get("text") or ""), + reasoning_style=str(kwargs.get("reasoning_style", "balanced")), + lookback_days=int(kwargs.get("lookback_days", 7)), + memory_limit=int(kwargs.get("memory_limit", 10)), + max_tokens=int(kwargs.get("max_tokens", 1800)), + ) + return _success(operation="memory_bank_deep_briefing", swarm_id=effective, **pack) + + +def _memory_bank_stats_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.memory.store import MemoryBankStore + + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + return _success( + operation="memory_bank_stats", + swarm_id=effective, + stats=MemoryBankStore(effective).stats(), + ) + + +def _memory_bank_anchor_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.memory.anchor import build_session_anchor + + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + vault, _ = _memory_scope_filters(kwargs) + pack = build_session_anchor( + effective, + vault=vault, + query=str(kwargs.get("query") or kwargs.get("text") or ""), + ) + payload = dict(pack) + payload.pop("swarm_id", None) + return _success(operation="memory_bank_anchor", swarm_id=effective, **payload) + + +def _memory_bank_import_dialogue_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.memory.dialogue_import import import_dialogue_directory, import_dialogue_file + + path = str(kwargs.get("path") or kwargs.get("directory") or "") + if not path: + return { + "success": False, + "error": "path is required", + "operation": "memory_bank_import_dialogue", + } + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + resolved = Path(path).expanduser() + vault, _ = _memory_scope_filters(kwargs) + vault_name = str(vault or "") + if resolved.is_file(): + result = import_dialogue_file(resolved, swarm_id=effective, vault=vault_name) + else: + result = import_dialogue_directory( + resolved, + swarm_id=effective, + vault=vault_name, + pattern=str(kwargs.get("pattern") or "*.jsonl"), + ) + return _success(operation="memory_bank_import_dialogue", swarm_id=effective, result=result) + + +def _memory_relation_record_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.memory.relations import MemoryRelationGraph + + subject = str(kwargs.get("subject") or "") + predicate = str(kwargs.get("predicate") or kwargs.get("relation") or "") + obj = str(kwargs.get("object") or kwargs.get("obj") or "") + if not subject or not predicate or not obj: + return { + "success": False, + "error": "subject, predicate, and object are required", + "operation": "memory_relation_record", + } + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + graph = MemoryRelationGraph(effective) + relation = graph.record( + subject, + predicate, + obj, + valid_from=kwargs.get("valid_from"), + valid_to=kwargs.get("valid_to"), + memory_id=kwargs.get("memory_id"), + ) + return _success( + operation="memory_relation_record", + swarm_id=effective, + relation=relation.to_dict(), + ) + + +def _memory_relation_query_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.memory.relations import MemoryRelationGraph + + entity = str(kwargs.get("entity") or kwargs.get("subject") or "") + if not entity: + return { + "success": False, + "error": "entity is required", + "operation": "memory_relation_query", + } + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + relations = MemoryRelationGraph(effective).query( + entity, + as_of=kwargs.get("as_of"), + limit=int(kwargs.get("limit", 50)), + ) + return _success( + operation="memory_relation_query", + swarm_id=effective, + entity=entity, + count=len(relations), + relations=[relation.to_dict() for relation in relations], + ) + + +def _framework_session_start_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.learning.framework_skills import build_framework_session_pack + + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + vault, _ = _memory_scope_filters(kwargs) + pack = build_framework_session_pack( + str(kwargs.get("task") or kwargs.get("query") or kwargs.get("text") or ""), + swarm_id=effective, + project_id=str(vault or kwargs.get("project_id") or ""), + skill_limit=int(kwargs.get("limit", 5)), + ) + payload = dict(pack) + payload.pop("swarm_id", None) + return _success(operation="framework_session_start", swarm_id=effective, **payload) + + +def _framework_skill_route_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.learning.framework_skills import format_skill_playbook, route_skills_for_task + + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + vault, _ = _memory_scope_filters(kwargs) + task = str(kwargs.get("task") or kwargs.get("query") or kwargs.get("text") or "") + matches = route_skills_for_task( + task, + swarm_id=effective, + project_id=str(vault or kwargs.get("project_id") or ""), + limit=int(kwargs.get("limit", 5)), + learned_only=bool(kwargs.get("learned_only", True)), + ) + return _success( + operation="framework_skill_route", + swarm_id=effective, + task=task, + count=len(matches), + matched_skills=[m.to_dict() for m in matches], + playbook=format_skill_playbook(matches), + ) + + +def _framework_skill_run_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.learning.framework_skills import run_framework_skill + + name = str(kwargs.get("name") or kwargs.get("skill") or kwargs.get("skill_name") or "") + if not name: + return { + "success": False, + "error": "name is required", + "operation": "framework_skill_run", + } + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + arg = str(kwargs.get("arg") or kwargs.get("argument") or kwargs.get("text") or "") + return run_framework_skill(name, arg=arg, swarm_id=effective) + + +def _growth_merge_briefing_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.learning.growth_merge import build_growth_briefing + + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + pack = build_growth_briefing( + effective, + query=str(kwargs.get("query") or kwargs.get("text") or kwargs.get("trigger") or ""), + limit=int(kwargs.get("limit", 8)), + ) + payload = dict(pack) + payload.pop("swarm_id", None) + return _success(operation="growth_merge_briefing", swarm_id=effective, **payload) + + +def _memory_relation_expire_handler(**kwargs: Any) -> dict[str, Any]: + from agentdrive.memory.relations import MemoryRelationGraph + + subject = str(kwargs.get("subject") or "") + predicate = str(kwargs.get("predicate") or "") + obj = str(kwargs.get("object") or "") + if not subject or not predicate or not obj: + return { + "success": False, + "error": "subject, predicate, and object are required", + "operation": "memory_relation_expire", + } + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + updated = MemoryRelationGraph(effective).expire( + subject, predicate, obj, ended=kwargs.get("ended") + ) + return _success( + operation="memory_relation_expire", + swarm_id=effective, + updated=updated, + ) + + +def _handler_synthesize_fused_skill(**kwargs: Any) -> dict[str, Any]: + from agentdrive.learning.skill_fusion import synthesize_from_inputs + + trigger = str(kwargs.get("trigger") or kwargs.get("task") or kwargs.get("text") or "") + if not trigger: + return { + "success": False, + "error": "trigger is required", + "operation": "synthesize_fused_skill", + } + dry_run = bool(kwargs.get("dry_run", False)) + effective, _ = _integrated_recorder(kwargs.get("swarm_id")) + if dry_run: + return _dry_plan( + "synthesize_fused_skill", + swarm_id=effective, + trigger=trigger[:200], + ) + + try: + fused = synthesize_from_inputs( + trigger=trigger, + swarm_id=effective, + program_id=str(kwargs.get("program_id") or "skill-fusion"), + operations=list(kwargs.get("operations") or []), + experience_traces=list(kwargs.get("experience_traces") or []), + source_skills=list(kwargs.get("source_skills") or kwargs.get("skills") or []), + pattern_projects=list(kwargs.get("pattern_projects") or kwargs.get("projects") or []), + promote=bool(kwargs.get("promote", False)), + ) + except ValueError as exc: + return { + "success": False, + "error": str(exc), + "operation": "synthesize_fused_skill", + } + + return _success(operation="synthesize_fused_skill", swarm_id=effective, fused_skill=fused) + + _HANDLERS: dict[str, OperationHandler] = { "think": _handler_think, "pool_query": _handler_pool_query, @@ -843,9 +2008,41 @@ def _handler_harness_compose(**kwargs: Any) -> dict[str, Any]: "experience_graph_context_pack": _handler_experience_graph_context_pack, "experience_graph_record_reasoning": _handler_experience_graph_record_reasoning, "experience_graph_suggest_reasoning": _handler_experience_graph_suggest_reasoning, + "multiverse_run_full": _handler_multiverse_run_full, + "multiverse_get_session": _handler_multiverse_get_session, + "multiverse_list_sessions": _handler_multiverse_list_sessions, + "multiverse_parent_decision": _handler_multiverse_parent_decision, + "external_parent_decision": _handler_external_parent_decision, + "multiverse_reopen_stale": _handler_multiverse_reopen_stale, + "multiverse_densify": _handler_multiverse_densify, "learnings_log": _handler_learnings_log, "learnings_list": _handler_learnings_list, "harness_compose": _handler_harness_compose, + "codebase_register_project": _handler_codebase_register_project, + "codebase_observe_file": _handler_codebase_observe_file, + "codebase_patterns_profile": _handler_codebase_patterns_profile, + "codebase_patterns_match": _handler_codebase_patterns_match, + "codebase_list_projects": _handler_codebase_list_projects, + "codebase_mimic": _handler_codebase_mimic, + "codebase_transform_style": _handler_codebase_transform_style, + "codebase_mirror_resonance": _handler_codebase_mirror_resonance, + "synthesize_fused_skill": _handler_synthesize_fused_skill, + "memory_bank_store": _memory_bank_store_handler, + "memory_bank_recall": _memory_bank_recall_handler, + "memory_bank_search": _memory_bank_search_handler, + "memory_bank_list": _memory_bank_list_handler, + "memory_bank_briefing": _memory_bank_briefing_handler, + "memory_bank_deep_briefing": _memory_bank_deep_briefing_handler, + "memory_bank_stats": _memory_bank_stats_handler, + "memory_bank_anchor": _memory_bank_anchor_handler, + "memory_bank_import_dialogue": _memory_bank_import_dialogue_handler, + "memory_relation_record": _memory_relation_record_handler, + "memory_relation_query": _memory_relation_query_handler, + "memory_relation_expire": _memory_relation_expire_handler, + "growth_merge_briefing": _growth_merge_briefing_handler, + "framework_session_start": _framework_session_start_handler, + "framework_skill_route": _framework_skill_route_handler, + "framework_skill_run": _framework_skill_run_handler, } _OPERATIONS_BY_NAME: dict[str, OperationSpec] = {op.name: op for op in OPERATIONS} @@ -881,7 +2078,7 @@ def run_operation(name: str, **kwargs: Any) -> dict[str, Any]: if name not in _HANDLERS: raise KeyError(f"unknown operation: {name}") try: - return _HANDLERS[name](**kwargs) + result = _HANDLERS[name](**kwargs) except Exception as exc: return { "success": False, @@ -889,6 +2086,17 @@ def run_operation(name: str, **kwargs: Any) -> dict[str, Any]: "error": str(exc), "error_type": type(exc).__name__, } + if isinstance(result, dict) and result.get("success") and not result.get("dry_run"): + try: + from agentdrive.learning.auto_absorb import maybe_absorb_operation_outcome + + absorbed = maybe_absorb_operation_outcome(name, kwargs, result) + if absorbed: + result = dict(result) + result["auto_learning"] = absorbed + except Exception: + logger.debug("auto_learning hook failed for %s", name, exc_info=True) + return result def export_operations_json(*, indent: int = 2) -> str: @@ -930,4 +2138,4 @@ def _coerce_value(raw: str) -> Any: return float(raw) return int(raw) except ValueError: - return raw \ No newline at end of file + return raw diff --git a/src/agentdrive/patterns/__init__.py b/src/agentdrive/patterns/__init__.py index 961248d..83a4209 100644 --- a/src/agentdrive/patterns/__init__.py +++ b/src/agentdrive/patterns/__init__.py @@ -34,4 +34,4 @@ "resolve_fabric_root", "resolve_pattern_path", "sanitize_pattern_name", -] \ No newline at end of file +] diff --git a/src/agentdrive/patterns/catalog.py b/src/agentdrive/patterns/catalog.py index 6fc857f..82fae6c 100644 --- a/src/agentdrive/patterns/catalog.py +++ b/src/agentdrive/patterns/catalog.py @@ -147,4 +147,4 @@ def apply_pattern(name: str, input_text: str) -> str: parts.append(f"# SYSTEM\n\n{system.strip()}") if user.strip(): parts.append(f"# USER\n\n{user.strip()}") - return "\n\n".join(parts) \ No newline at end of file + return "\n\n".join(parts) diff --git a/src/agentdrive/patterns/fabric_import.py b/src/agentdrive/patterns/fabric_import.py index 57ade25..665a777 100644 --- a/src/agentdrive/patterns/fabric_import.py +++ b/src/agentdrive/patterns/fabric_import.py @@ -91,9 +91,7 @@ def _is_fabric_root(path: Path) -> bool: def _validate_fabric_root(path: Path) -> None: if not _is_fabric_root(path): - raise FileNotFoundError( - f"Not a Fabric repository (missing data/patterns): {path}" - ) + raise FileNotFoundError(f"Not a Fabric repository (missing data/patterns): {path}") def _fabric_patterns_root(fabric_root: Path) -> Path: @@ -268,4 +266,4 @@ def import_fabric_corpus( ) except FabricPatternNotFoundError: continue - return imported \ No newline at end of file + return imported diff --git a/src/agentdrive/session_events.py b/src/agentdrive/session_events.py index 1307190..551e4c9 100644 --- a/src/agentdrive/session_events.py +++ b/src/agentdrive/session_events.py @@ -42,12 +42,7 @@ def session_events_path(agent_id: str, session_id: str) -> Path: safe_agent = safe_name(agent_id) safe_session = safe_name(session_id) return ( - get_agentdrive_home() - / "agents" - / safe_agent - / "sessions" - / safe_session - / "events.jsonl" + get_agentdrive_home() / "agents" / safe_agent / "sessions" / safe_session / "events.jsonl" ) @@ -239,4 +234,4 @@ def format_type_histogram(counts: dict[str, int], *, max_types: int = 10) -> str "resolve_session_id", "session_events_path", "summarize_event_types", -] \ No newline at end of file +] diff --git a/src/agentdrive/skills/__init__.py b/src/agentdrive/skills/__init__.py index 272e022..24d81fe 100644 --- a/src/agentdrive/skills/__init__.py +++ b/src/agentdrive/skills/__init__.py @@ -1,24 +1,58 @@ """AgentDrive skills — SKILL.md registry (Pattern 5).""" from agentdrive.skills.compose import compose_skills_block, match_skills_for_turn +from agentdrive.skills.curation import ( + SkillAssimilationReport, + SkillDNAExport, + SkillReview, + assimilate_inherited_skills, + ingest_skill_as_dna, + promote_inherited_skill, + prune_inherited_skill, + review_inherited_skills, + skill_to_genome, +) from agentdrive.skills.registry import ( SkillEntry, discover_skills, get_skill, init_skill, + install_inherited_skill, list_skills, list_skills_by_tier, ) from agentdrive.skills.runner import run_skill +from agentdrive.skills.usage import ( + SkillUsage, + get_skill_usage, + list_skill_usage, + record_skill_match, + record_skill_run, +) __all__ = [ "SkillEntry", + "SkillAssimilationReport", + "SkillDNAExport", + "SkillReview", "compose_skills_block", "discover_skills", "get_skill", "init_skill", + "install_inherited_skill", "list_skills", "list_skills_by_tier", "match_skills_for_turn", "run_skill", -] \ No newline at end of file + "SkillUsage", + "get_skill_usage", + "list_skill_usage", + "record_skill_match", + "record_skill_run", + "ingest_skill_as_dna", + "assimilate_inherited_skills", + "promote_inherited_skill", + "prune_inherited_skill", + "review_inherited_skills", + "skill_to_genome", +] diff --git a/src/agentdrive/skills/compose.py b/src/agentdrive/skills/compose.py index 5fc7ae2..e16bf0b 100644 --- a/src/agentdrive/skills/compose.py +++ b/src/agentdrive/skills/compose.py @@ -2,10 +2,14 @@ from __future__ import annotations +import logging import os import re from agentdrive.skills.registry import SkillEntry, discover_skills, list_skills_by_tier +from agentdrive.skills.usage import record_skill_match, skill_usage_boost + +logger = logging.getLogger(__name__) _TOKEN_RE = re.compile(r"[a-z0-9][a-z0-9_-]{1,}") @@ -72,6 +76,7 @@ def _score_skill(entry: SkillEntry, message: str, *, role: str | None) -> float: elif entry.role and entry.role != role: score -= 2.0 + score += skill_usage_boost(entry.name, inherited=entry.category == "inherited") return score @@ -81,6 +86,7 @@ def match_skills_for_turn( top_k: int = 3, role: str | None = None, harness: str | None = None, + record_matches: bool = True, ) -> list[SkillEntry]: """Rank skills by keyword/tag overlap with the user message.""" active = harness or active_harness() @@ -92,7 +98,14 @@ def match_skills_for_turn( if score > 0: ranked.append((score, entry)) ranked.sort(key=lambda item: (-item[0], item[1].name)) - return [entry for _, entry in ranked[:top_k]] + selected = ranked[:top_k] + if record_matches: + for score, entry in selected: + try: + record_skill_match(entry.name, score=score) + except Exception: + logger.debug("Failed to record skill match for %s", entry.name, exc_info=True) + return [entry for _, entry in selected] def format_skills_catalog(*, per_tier: int = 8, harness: str | None = None) -> str: @@ -153,9 +166,7 @@ def compose_skills_block( ) -> str: """Full skills section for build_system_prompt.""" active = harness or active_harness() - matched = match_skills_for_turn( - user_message, top_k=top_k, role=role, harness=active - ) + matched = match_skills_for_turn(user_message, top_k=top_k, role=role, harness=active) parts: list[str] = [] if include_catalog: catalog = format_skills_catalog(harness=active) @@ -163,4 +174,4 @@ def compose_skills_block( parts.append(catalog) if matched: parts.append(format_matched_skill_bodies(matched)) - return "\n".join(parts) \ No newline at end of file + return "\n".join(parts) diff --git a/src/agentdrive/skills/curation.py b/src/agentdrive/skills/curation.py new file mode 100644 index 0000000..03196ef --- /dev/null +++ b/src/agentdrive/skills/curation.py @@ -0,0 +1,586 @@ +"""Review, promote, and prune inherited skills using local evidence.""" + +from __future__ import annotations + +import re +from dataclasses import asdict, dataclass +from datetime import UTC, datetime +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import yaml + +from agentdrive.genome.models import Genome +from agentdrive.skills.registry import SkillEntry, discover_skills, get_skill +from agentdrive.skills.usage import SkillUsage, get_skill_usage + +if TYPE_CHECKING: + from agentdrive.drive.drive import AgentDrive, DriveIngestResult + +_FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n(.*)", re.DOTALL) +_GENOME_ID_RE = re.compile(r"[^a-z0-9._-]+") + + +@dataclass(frozen=True) +class SkillReview: + """Curation recommendation for one inherited skill candidate.""" + + name: str + recommendation: str + reason: str + path: str + category: str + source: str + matches: int = 0 + runs: int = 0 + successes: int = 0 + failures: int = 0 + success_rate: float = 0.0 + promoted: bool = False + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +@dataclass(frozen=True) +class SkillDNAExport: + """Result of turning a curated skill into pool DNA.""" + + skill_name: str + genome_id: str + accepted: bool + reason: str + path: str + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +@dataclass(frozen=True) +class SkillAssimilationReport: + """Result of a gated inherited-skill assimilation pass.""" + + reviewed: int + promoted: list[SkillReview] + dna_exports: list[SkillDNAExport] + pruned: list[dict[str, str]] + watched: list[SkillReview] + errors: list[dict[str, str]] + + def to_dict(self) -> dict[str, Any]: + return { + "reviewed": self.reviewed, + "promoted": [item.to_dict() for item in self.promoted], + "dna_exports": [item.to_dict() for item in self.dna_exports], + "pruned": list(self.pruned), + "watched": [item.to_dict() for item in self.watched], + "errors": list(self.errors), + } + + +def review_inherited_skills(*, include_promoted: bool = True) -> list[SkillReview]: + """Review inherited skill candidates and return promote/watch/prune advice.""" + reviews: list[SkillReview] = [] + for entry in discover_skills(): + if entry.category not in ("inherited", "promoted"): + continue + if entry.category == "promoted" and not include_promoted: + continue + usage = get_skill_usage(entry.name) + recommendation, reason = _recommend(entry, usage) + reviews.append( + SkillReview( + name=entry.name, + recommendation=recommendation, + reason=reason, + path=str(entry.path), + category=entry.category, + source=entry.source, + matches=usage.matches, + runs=usage.runs, + successes=usage.successes, + failures=usage.failures, + success_rate=usage.success_rate, + promoted=entry.category == "promoted", + ) + ) + return sorted( + reviews, + key=lambda item: ( + _recommendation_rank(item.recommendation), + -item.successes, + -item.matches, + item.failures, + item.name, + ), + ) + + +def assimilate_inherited_skills( + *, + target_drive: AgentDrive | None = None, + ingest_dna: bool = True, + prune: bool = False, + include_promoted: bool = False, + skill_names: list[str] | tuple[str, ...] | set[str] | None = None, +) -> SkillAssimilationReport: + """Apply gated curation recommendations to inherited sub-agent skills. + + This is the parent-bench assimilation pass: promote candidates that already + meet the evidence threshold, optionally ingest promoted skills into DNA, and + only prune weak candidates when explicitly requested. + """ + reviews = review_inherited_skills(include_promoted=include_promoted) + if skill_names: + wanted = {name.strip().lower() for name in skill_names if name.strip()} + reviews = [review for review in reviews if review.name.lower() in wanted] + promoted: list[SkillReview] = [] + dna_exports: list[SkillDNAExport] = [] + pruned: list[dict[str, str]] = [] + watched: list[SkillReview] = [] + errors: list[dict[str, str]] = [] + + for review in reviews: + if review.recommendation == "promote" and not review.promoted: + try: + promoted_review = promote_inherited_skill(review.name) + promoted.append(promoted_review) + if ingest_dna: + dna_exports.append( + ingest_skill_as_dna(promoted_review.name, target_drive=target_drive) + ) + except Exception as exc: + errors.append({"skill_name": review.name, "action": "promote", "error": str(exc)}) + continue + + if review.recommendation == "promoted": + if ingest_dna and include_promoted: + try: + dna_exports.append(ingest_skill_as_dna(review.name, target_drive=target_drive)) + except Exception as exc: + errors.append({"skill_name": review.name, "action": "dna", "error": str(exc)}) + watched.append(review) + continue + + if review.recommendation == "prune" and prune: + try: + path = prune_inherited_skill(review.name, reason=review.reason) + pruned.append( + {"skill_name": review.name, "path": str(path), "reason": review.reason} + ) + except Exception as exc: + errors.append({"skill_name": review.name, "action": "prune", "error": str(exc)}) + continue + + watched.append(review) + + return SkillAssimilationReport( + reviewed=len(reviews), + promoted=promoted, + dna_exports=dna_exports, + pruned=pruned, + watched=watched, + errors=errors, + ) + + +def promote_inherited_skill(name: str) -> SkillReview: + """Mark an inherited skill as promoted parent bench knowledge.""" + entry = _require_skill(name) + if entry.category not in ("inherited", "promoted"): + raise ValueError(f"Skill is not inherited: {name}") + meta, body = _read_skill_doc(entry.path) + tags = _normalize_tags(meta.get("tags")) + if "promoted" not in tags: + tags.append("promoted") + if "inherited" not in tags: + tags.append("inherited") + meta.update( + { + "category": "promoted", + "role": meta.get("role") or "shared", + "harness": meta.get("harness") or "agentdrive", + "tags": tags, + "promotion": { + "status": "promoted", + "source": "agentdrive skills promote", + "usage": _usage_payload(entry.name), + }, + } + ) + _write_skill_doc(entry.path, meta, body) + promoted = get_skill(entry.name) + if promoted is None: + promoted = entry + usage = get_skill_usage(entry.name) + recommendation, reason = _recommend(promoted, usage) + return SkillReview( + name=promoted.name, + recommendation=recommendation, + reason=reason, + path=str(promoted.path), + category=promoted.category, + source=promoted.source, + matches=usage.matches, + runs=usage.runs, + successes=usage.successes, + failures=usage.failures, + success_rate=usage.success_rate, + promoted=True, + ) + + +def prune_inherited_skill(name: str, *, reason: str = "") -> Path: + """Disable a weak inherited skill without deleting its file.""" + entry = _require_skill(name) + if entry.category not in ("inherited", "promoted"): + raise ValueError(f"Skill is not inherited/promoted: {name}") + meta, body = _read_skill_doc(entry.path) + tags = _normalize_tags(meta.get("tags")) + if "pruned" not in tags: + tags.append("pruned") + meta.update( + { + "disabled": True, + "category": "pruned", + "tags": tags, + "promotion": { + "status": "pruned", + "reason": reason or "pruned by agentdrive skills prune", + "source": "agentdrive skills prune", + "usage": _usage_payload(entry.name), + }, + } + ) + _write_skill_doc(entry.path, meta, body) + return entry.path + + +def skill_to_genome(entry: SkillEntry) -> Genome: + """Convert a promoted/inherited skill into a durable Genome.""" + if entry.category not in ("inherited", "promoted"): + raise ValueError(f"Skill is not inherited/promoted: {entry.name}") + usage = get_skill_usage(entry.name) + meta, _body = _read_skill_doc(entry.path) + inheritance = _inheritance_payload(meta, entry=entry) + revision_sources = [ + revision["source"] for revision in inheritance["revisions"] if revision.get("source") + ] + revision_subagents = sorted( + { + part + for source in revision_sources + for part in [_subagent_from_inheritance_source(str(source))] + if part + } + ) + genome_id = _skill_genome_id(entry.name) + genome_version = _skill_genome_version(inheritance["revision_count"]) + tags = list(entry.tags) + source_parts = [p for p in entry.source.split(":") if p] + swarm_id = source_parts[1] if len(source_parts) >= 3 else "" + subagent_id = source_parts[2] if len(source_parts) >= 3 else "" + body_steps = _body_steps(entry.body) + framework = { + "type": "inherited_skill", + "skill_name": entry.name, + "description": entry.description, + "source": entry.source, + "body": entry.body, + "steps": body_steps, + "usage": _usage_payload(entry.name), + "inheritance": inheritance, + } + applicability = { + "domains": sorted(set(["agent-skills", "inherited-skills", *tags])), + "problem_signatures": [ + entry.name, + entry.description, + entry.when_to_call, + *tags, + ], + "source_skill": entry.name, + "source_subagent_id": subagent_id, + "source_subagent_ids": revision_subagents or ([subagent_id] if subagent_id else []), + "swarm_id": swarm_id, + "revision_count": inheritance["revision_count"], + } + evaluation_score = { + "skill_success_rate": usage.success_rate, + "skill_successes": float(usage.successes), + "skill_matches": float(usage.matches), + "skill_revision_count": float(inheritance["revision_count"]), + } + reasoning_patterns = { + "skill_body": entry.body, + "when_to_call": entry.when_to_call, + "inheritance_revisions": inheritance["revisions"], + "patterns_recognized": [ + { + "framework_id": entry.name, + "intents": [entry.name, *tags], + "fields": ["skill", "subagent", "inheritance", "promotion", "revision"], + } + ], + } + provenance = { + "lineage": [ + { + "parent": entry.source or str(entry.path), + "relation": "skill-to-dna", + "timestamp": datetime.now(UTC).isoformat(), + "notes": f"Promoted inherited skill {entry.name} into AgentDrive DNA", + } + ] + } + for revision in inheritance["revisions"]: + provenance["lineage"].append( + { + "parent": revision.get("source") or entry.source or str(entry.path), + "relation": "skill-revision", + "timestamp": revision.get("recorded_at") or datetime.now(UTC).isoformat(), + "notes": ( + f"Inherited skill revision for {entry.name} from " + f"{revision.get('subagent_id') or 'subagent'}" + ), + "swarm_id": revision.get("swarm_id") or "", + "subagent_id": revision.get("subagent_id") or "", + } + ) + authors: list[dict[str, str]] = [ + {"type": "agent", "id": "agentdrive-skills", "name": "AgentDrive skills"} + ] + for sid in revision_subagents or ([subagent_id] if subagent_id else []): + authors.append({"type": "agent", "id": f"sub:{sid}", "name": sid}) + return Genome.create( + id=genome_id, + version=genome_version, + framework=framework, + authors=authors, + applicability=applicability, + dependencies={"genomes": [], "agent_capabilities": ["skill-reuse", "inheritance"]}, + evaluation_score=evaluation_score, + reasoning_patterns=reasoning_patterns, + provenance=provenance, + ) + + +def ingest_skill_as_dna( + name: str, + *, + target_drive: AgentDrive | None = None, +) -> SkillDNAExport: + """Ingest a promoted/inherited skill into the AgentDrive DNA pool.""" + entry = _require_skill(name) + if entry.category not in ("inherited", "promoted"): + raise ValueError(f"Skill is not inherited/promoted: {name}") + if target_drive is None: + from agentdrive.drive.drive import get_default_drive + + target_drive = get_default_drive() + + genome = skill_to_genome(entry) + try: + latest = target_drive.registry.load(genome.manifest.id) + except Exception: + latest = None + if latest is not None and latest.manifest.content_hash: + if latest.manifest.content_hash not in genome.manifest.supersedes: + genome.manifest.supersedes.append(latest.manifest.content_hash) + genome.manifest.content_hash = "sha256:pending" + genome.finalize() + + result: DriveIngestResult = target_drive.ingest( + genome, + source="skill-promotion", + actor="agentdrive-skills", + ) + meta, body = _read_skill_doc(entry.path) + meta["dna"] = { + "status": "ingested", + "genome_id": result.genome_id, + "accepted": result.accepted, + "reason": result.reason, + "source": "agentdrive skills dna", + } + _write_skill_doc(entry.path, meta, body) + return SkillDNAExport( + skill_name=entry.name, + genome_id=result.genome_id, + accepted=result.accepted, + reason=result.reason, + path=str(entry.path), + ) + + +def _recommend(entry: SkillEntry, usage: SkillUsage) -> tuple[str, str]: + if entry.category == "promoted": + return "promoted", "already promoted into the parent skill bench" + if usage.failures >= 2 and usage.failures > usage.successes: + return "prune", "failures outnumber successful outcomes" + if usage.runs >= 2 and usage.success_rate >= 0.75: + return "promote", "outcome evidence shows reliable success" + if usage.successes >= 1 and usage.matches >= 3 and usage.failures == 0: + return "promote", "matched repeatedly and has successful outcome evidence" + if usage.matches >= 3 and usage.failures == 0: + return "watch", "retrieved repeatedly; needs success outcome evidence" + return "watch", "not enough outcome evidence yet" + + +def _recommendation_rank(value: str) -> int: + order = {"promote": 0, "prune": 1, "watch": 2, "promoted": 3} + return order.get(value, 9) + + +def _usage_payload(name: str) -> dict[str, Any]: + usage = get_skill_usage(name) + return { + "matches": usage.matches, + "runs": usage.runs, + "successes": usage.successes, + "failures": usage.failures, + "success_rate": usage.success_rate, + "last_matched_at": usage.last_matched_at, + "last_run_at": usage.last_run_at, + } + + +def _skill_genome_id(name: str) -> str: + cleaned = _GENOME_ID_RE.sub("-", name.strip().lower()).strip("-._") + if not cleaned: + cleaned = "skill" + return f"skill-{cleaned}"[:120].strip("-._") + + +def _skill_genome_version(revision_count: int) -> str: + patch = max(0, int(revision_count) - 1) + return f"1.0.{patch}" + + +def _body_steps(body: str) -> list[dict[str, str]]: + steps: list[dict[str, str]] = [] + for line in body.splitlines(): + stripped = line.strip() + if not stripped: + continue + stripped = re.sub(r"^#{1,6}\s*", "", stripped) + stripped = re.sub(r"^(?:[-*]|\d+[.)])\s*", "", stripped) + if not stripped: + continue + steps.append( + { + "id": str(len(steps) + 1), + "name": stripped[:90], + "description": stripped, + } + ) + if len(steps) >= 12: + break + if not steps: + steps.append({"id": "1", "name": "Apply skill", "description": body[:500]}) + return steps + + +def _subagent_from_inheritance_source(source: str) -> str: + parts = [part for part in source.split(":") if part] + if len(parts) >= 3 and parts[0] == "inheritance": + return parts[2] + return "" + + +def _inheritance_payload(meta: dict[str, Any], *, entry: SkillEntry) -> dict[str, Any]: + raw = meta.get("inheritance") or {} + if not isinstance(raw, dict): + raw = {} + revisions = raw.get("revisions") or [] + if not isinstance(revisions, list): + revisions = [] + + normalized: list[dict[str, Any]] = [] + for revision in revisions: + if not isinstance(revision, dict): + continue + source = str(revision.get("source") or "").strip() + normalized.append( + { + "source": source, + "swarm_id": _swarm_from_inheritance_source(source), + "subagent_id": _subagent_from_inheritance_source(source), + "recorded_at": str(revision.get("recorded_at") or ""), + "description": str(revision.get("description") or ""), + "body_chars": _safe_int(revision.get("body_chars")), + "tags": _normalize_tags(revision.get("tags")), + } + ) + + if not normalized: + source = entry.source or "unknown" + normalized.append( + { + "source": source, + "swarm_id": _swarm_from_inheritance_source(source), + "subagent_id": _subagent_from_inheritance_source(source), + "recorded_at": str(raw.get("latest_recorded_at") or ""), + "description": entry.description, + "body_chars": len(entry.body), + "tags": list(entry.tags), + } + ) + + latest = normalized[-1] + return { + "status": str(raw.get("status") or "active"), + "revision_count": len(normalized), + "latest_source": str(raw.get("latest_source") or latest["source"]), + "latest_recorded_at": str(raw.get("latest_recorded_at") or latest["recorded_at"]), + "revisions": normalized, + } + + +def _swarm_from_inheritance_source(source: str) -> str: + parts = [part for part in source.split(":") if part] + if len(parts) >= 3 and parts[0] == "inheritance": + return parts[1] + return "" + + +def _safe_int(value: Any) -> int: + try: + return int(value or 0) + except (TypeError, ValueError): + return 0 + + +def _require_skill(name: str) -> SkillEntry: + entry = get_skill(name) + if entry is None: + raise ValueError(f"Unknown skill: {name}") + return entry + + +def _read_skill_doc(path: Path) -> tuple[dict[str, Any], str]: + text = path.read_text(encoding="utf-8") + match = _FRONTMATTER_RE.match(text) + if not match: + raise ValueError(f"Skill file has no frontmatter: {path}") + meta = yaml.safe_load(match.group(1)) or {} + if not isinstance(meta, dict): + raise ValueError(f"Skill frontmatter is not a mapping: {path}") + return meta, match.group(2).strip() + + +def _write_skill_doc(path: Path, meta: dict[str, Any], body: str) -> None: + path.write_text( + "---\n" + yaml.safe_dump(meta, sort_keys=False).strip() + "\n---\n\n" + body.strip() + "\n", + encoding="utf-8", + ) + + +def _normalize_tags(raw: Any) -> list[str]: + if isinstance(raw, str): + return [t.strip() for t in raw.split(",") if t.strip()] + if isinstance(raw, list): + return [str(t).strip() for t in raw if str(t).strip()] + if isinstance(raw, tuple): + return [str(t).strip() for t in raw if str(t).strip()] + return [] diff --git a/src/agentdrive/skills/registry.py b/src/agentdrive/skills/registry.py index f3c42cd..74bad67 100644 --- a/src/agentdrive/skills/registry.py +++ b/src/agentdrive/skills/registry.py @@ -4,6 +4,7 @@ import re from dataclasses import dataclass +from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -33,6 +34,9 @@ class SkillEntry: _FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n(.*)", re.DOTALL) +_MAX_INHERITED_SKILL_NAME_CHARS = 64 +_MAX_INHERITED_SKILL_DESCRIPTION_CHARS = 1024 +_MAX_INHERITED_SKILL_BODY_CHARS = 15_000 def _skills_roots() -> list[Path]: @@ -61,6 +65,9 @@ def _parse_skill_file(path: Path, *, skills_root: Path | None = None) -> SkillEn if not isinstance(meta, dict): return None + if bool(meta.get("disabled")): + return None + name = str(meta.get("name") or path.parent.name).strip() if not name: return None @@ -206,6 +213,184 @@ def _normalize_skill_name(name: str) -> str: return cleaned +def _validate_inherited_skill(slug: str, description: str, body: str) -> None: + if len(slug) > _MAX_INHERITED_SKILL_NAME_CHARS: + raise ValueError(f"Inherited skill name must be <= {_MAX_INHERITED_SKILL_NAME_CHARS} chars") + if len(description) > _MAX_INHERITED_SKILL_DESCRIPTION_CHARS: + raise ValueError( + f"Inherited skill description must be <= {_MAX_INHERITED_SKILL_DESCRIPTION_CHARS} chars" + ) + if not body.strip(): + raise ValueError("Inherited skill body cannot be empty") + if len(body) > _MAX_INHERITED_SKILL_BODY_CHARS: + raise ValueError(f"Inherited skill body must be <= {_MAX_INHERITED_SKILL_BODY_CHARS} chars") + + +def install_inherited_skill( + *, + name: str, + description: str, + body: str, + source_subagent_id: str, + swarm_id: str = "", + tags: list[str] | tuple[str, ...] | None = None, + operation: str | None = None, + when_to_call: str = "", + force: bool = False, + update_existing: bool = False, +) -> Path: + """Install a sub-agent playbook as a parent-visible SKILL.md. + + This is the Hermes-style skill learning path for AgentDrive: a sub-agent + can hand back a reusable playbook in its inheritance manifest, and the + parent stores it as an auditable, discoverable skill instead of opaque chat + memory. + """ + slug = _normalize_skill_name(name) + desc = description.strip() or f"Inherited skill from {source_subagent_id}" + skill_body = body.strip() + _validate_inherited_skill(slug, desc, skill_body) + + source = safe_name(source_subagent_id or "subagent") + swarm = safe_name(swarm_id or "default") + skill_dir = get_agentdrive_home() / "skills" / "inherited" / swarm / source / slug + skill_md = skill_dir / "SKILL.md" + if update_existing: + existing = get_skill(slug) + if existing is not None and existing.category in ("inherited", "promoted"): + try: + existing.path.relative_to(get_agentdrive_home() / "skills") + skill_md = existing.path + skill_dir = existing.path.parent + except ValueError: + pass + if skill_md.exists() and not force and not update_existing: + raise FileExistsError(f"Inherited skill already exists: {skill_md}") + + tag_values = [str(tag).strip() for tag in (tags or []) if str(tag).strip()] + if "inherited" not in tag_values: + tag_values.append("inherited") + if source_subagent_id and source_subagent_id not in tag_values: + tag_values.append(source_subagent_id) + + revision = { + "source": f"inheritance:{swarm_id or 'default'}:{source_subagent_id or 'subagent'}", + "recorded_at": datetime.now(UTC).isoformat(), + "description": desc, + "body_chars": len(skill_body), + "tags": list(tag_values), + } + + meta: dict[str, Any] = { + "name": slug, + "description": desc, + "harness": "agentdrive", + "category": "inherited", + "role": "shared", + "tags": tag_values, + "source": f"inheritance:{swarm_id or 'default'}:{source_subagent_id or 'subagent'}", + "when_to_call": when_to_call.strip() + or "Use when a task matches the sub-agent playbook captured in this inherited skill.", + "inheritance": { + "status": "active", + "revision_count": 1, + "latest_source": revision["source"], + "latest_recorded_at": revision["recorded_at"], + "revisions": [revision], + }, + } + if operation: + meta["agentdrive_operation"] = operation + + if skill_md.exists() and (force or update_existing): + existing_meta, existing_body = _read_skill_doc(skill_md) + if not force and existing_body.strip() == skill_body: + return skill_md + + existing_tags = _normalize_tags(existing_meta.get("tags")) + for tag in tag_values: + if tag not in existing_tags: + existing_tags.append(tag) + + inheritance = existing_meta.get("inheritance") or {} + if not isinstance(inheritance, dict): + inheritance = {} + revisions = inheritance.get("revisions") or [] + if not isinstance(revisions, list): + revisions = [] + if revisions == []: + revisions.append( + { + "source": existing_meta.get("source") or "unknown", + "recorded_at": inheritance.get("latest_recorded_at") or "", + "description": existing_meta.get("description") or "", + "body_chars": len(existing_body), + "tags": existing_tags, + } + ) + revisions.append(revision) + + existing_meta.update( + { + "description": desc or existing_meta.get("description") or "", + "harness": existing_meta.get("harness") or "agentdrive", + "category": existing_meta.get("category") or "inherited", + "role": existing_meta.get("role") or "shared", + "tags": existing_tags, + "source": meta["source"], + "when_to_call": existing_meta.get("when_to_call") or meta["when_to_call"], + "inheritance": { + "status": "active", + "revision_count": len(revisions), + "latest_source": revision["source"], + "latest_recorded_at": revision["recorded_at"], + "revisions": revisions, + }, + } + ) + if operation: + existing_meta["agentdrive_operation"] = operation + + _write_skill_doc(skill_md, existing_meta, skill_body) + return skill_md + + skill_dir.mkdir(parents=True, exist_ok=True) + _write_skill_doc(skill_md, meta, skill_body) + return skill_md + + +def _read_skill_doc(path: Path) -> tuple[dict[str, Any], str]: + text = path.read_text(encoding="utf-8") + match = _FRONTMATTER_RE.match(text) + if not match: + return {}, text + try: + meta = yaml.safe_load(match.group(1)) or {} + except yaml.YAMLError: + meta = {} + if not isinstance(meta, dict): + meta = {} + return meta, match.group(2).strip() + + +def _write_skill_doc(path: Path, meta: dict[str, Any], body: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + "---\n" + yaml.safe_dump(meta, sort_keys=False).strip() + "\n---\n\n" + body.strip() + "\n", + encoding="utf-8", + ) + + +def _normalize_tags(raw: Any) -> list[str]: + if isinstance(raw, str): + return [tag.strip() for tag in raw.split(",") if tag.strip()] + if isinstance(raw, list): + return [str(tag).strip() for tag in raw if str(tag).strip()] + if isinstance(raw, tuple): + return [str(tag).strip() for tag in raw if str(tag).strip()] + return [] + + def init_skill(name: str, *, description: str = "", force: bool = False) -> Path: """Scaffold ``~/.agentdrive/skills//SKILL.md`` with frontmatter template.""" slug = _normalize_skill_name(name) @@ -249,4 +434,4 @@ def skill_operation_kwargs(entry: SkillEntry, arg: str) -> dict[str, Any]: else: if arg.strip(): kwargs[key] = arg.strip() - return kwargs \ No newline at end of file + return kwargs diff --git a/src/agentdrive/skills/runner.py b/src/agentdrive/skills/runner.py index 7c6449f..f7074cd 100644 --- a/src/agentdrive/skills/runner.py +++ b/src/agentdrive/skills/runner.py @@ -5,10 +5,11 @@ from typing import Any from agentdrive.operations import run_operation -from agentdrive.skills.registry import SkillEntry, get_skill, skill_operation_kwargs +from agentdrive.skills.registry import get_skill, skill_operation_kwargs +from agentdrive.skills.usage import record_skill_run -def run_skill(name: str, arg: str = "") -> dict[str, Any]: +def run_skill(name: str, arg: str = "", *, swarm_id: str = "") -> dict[str, Any]: """Execute a skill. Returns operation result dict or error envelope.""" entry = get_skill(name) if entry is None: @@ -18,18 +19,25 @@ def run_skill(name: str, arg: str = "") -> dict[str, Any]: from agentdrive.golden_path import verify_all summary = verify_all() + success = bool(summary.get("required_pass")) + _record_run_safely(entry.name, success=success) return { - "success": bool(summary.get("required_pass")), + "success": success, "skill": entry.name, "result": summary, } if entry.operation: kwargs = skill_operation_kwargs(entry, arg) + if swarm_id: + kwargs["swarm_id"] = swarm_id result = run_operation(entry.operation, **kwargs) - return {"success": result.get("success", False), "skill": entry.name, "result": result} + success = bool(result.get("success", False)) + _record_run_safely(entry.name, success=success) + return {"success": success, "skill": entry.name, "result": result} # No bound operation — return skill body for the caller to display. + _record_run_safely(entry.name, success=True) return { "success": True, "skill": entry.name, @@ -61,4 +69,11 @@ def format_skill_result(result: dict[str, Any], *, preview_limit: int = 4000) -> text = str(body) return text[:preview_limit] + ("…" if len(text) > preview_limit else "") - return result.get("description") or "ok" \ No newline at end of file + return result.get("description") or "ok" + + +def _record_run_safely(name: str, *, success: bool) -> None: + try: + record_skill_run(name, success=success) + except Exception: + pass diff --git a/src/agentdrive/skills/usage.py b/src/agentdrive/skills/usage.py new file mode 100644 index 0000000..2be3f02 --- /dev/null +++ b/src/agentdrive/skills/usage.py @@ -0,0 +1,183 @@ +"""Local usage ledger for SKILL.md candidates. + +Inherited sub-agent skills should not be static prompt text forever. This +ledger tracks whether a skill is being retrieved and whether explicit runs +succeed, giving the matcher a small feedback signal without mutating SKILL.md. +""" + +from __future__ import annotations + +import json +import logging +import tempfile +from dataclasses import asdict, dataclass, field +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from agentdrive.constants import get_agentdrive_home + +logger = logging.getLogger(__name__) + +USAGE_FILE_NAME = "usage.json" + + +def _utc_now_iso() -> str: + return datetime.now(UTC).isoformat() + + +def _usage_path() -> Path: + return get_agentdrive_home() / "skills" / USAGE_FILE_NAME + + +@dataclass +class SkillUsage: + """Usage and outcome summary for one skill.""" + + name: str + matches: int = 0 + runs: int = 0 + successes: int = 0 + failures: int = 0 + last_score: float = 0.0 + last_matched_at: str = "" + last_run_at: str = "" + sources: dict[str, int] = field(default_factory=dict) + + @classmethod + def from_raw(cls, name: str, raw: Any) -> SkillUsage: + if not isinstance(raw, dict): + return cls(name=name) + sources = raw.get("sources") or {} + if not isinstance(sources, dict): + sources = {} + return cls( + name=str(raw.get("name") or name), + matches=int(raw.get("matches") or 0), + runs=int(raw.get("runs") or 0), + successes=int(raw.get("successes") or 0), + failures=int(raw.get("failures") or 0), + last_score=float(raw.get("last_score") or 0.0), + last_matched_at=str(raw.get("last_matched_at") or ""), + last_run_at=str(raw.get("last_run_at") or ""), + sources={str(k): int(v or 0) for k, v in sources.items()}, + ) + + @property + def success_rate(self) -> float: + if self.runs <= 0: + return 0.0 + return self.successes / max(1, self.runs) + + +def _load_all() -> dict[str, SkillUsage]: + path = _usage_path() + if not path.is_file(): + return {} + try: + raw = json.loads(path.read_text(encoding="utf-8")) + except Exception: + logger.debug("Failed to read skill usage ledger %s", path, exc_info=True) + return {} + skills = raw.get("skills") if isinstance(raw, dict) else {} + if not isinstance(skills, dict): + return {} + return {str(name): SkillUsage.from_raw(str(name), value) for name, value in skills.items()} + + +def _save_all(items: dict[str, SkillUsage]) -> None: + path = _usage_path() + path.parent.mkdir(parents=True, exist_ok=True) + payload = { + "version": 1, + "updated_at": _utc_now_iso(), + "skills": {name: asdict(item) for name, item in sorted(items.items())}, + } + fd, tmp_name = tempfile.mkstemp( + prefix=f".{USAGE_FILE_NAME}.", + suffix=".tmp", + dir=str(path.parent), + text=True, + ) + try: + with open(fd, "w", encoding="utf-8") as fh: + json.dump(payload, fh, indent=2, sort_keys=True) + fh.write("\n") + Path(tmp_name).replace(path) + except Exception: + try: + Path(tmp_name).unlink(missing_ok=True) + except Exception: + pass + raise + + +def get_skill_usage(name: str) -> SkillUsage: + """Return current usage for a skill, or a zero record.""" + key = name.strip() + if not key: + return SkillUsage(name="") + return _load_all().get(key, SkillUsage(name=key)) + + +def list_skill_usage() -> list[SkillUsage]: + """Return usage records sorted by strongest evidence first.""" + return sorted( + _load_all().values(), + key=lambda item: (-item.successes, -item.matches, item.failures, item.name), + ) + + +def record_skill_match(name: str, *, score: float = 0.0, source: str = "compose") -> SkillUsage: + """Record that a skill was selected for a turn.""" + key = name.strip() + if not key: + return SkillUsage(name="") + items = _load_all() + usage = items.get(key, SkillUsage(name=key)) + usage.matches += 1 + usage.last_score = float(score) + usage.last_matched_at = _utc_now_iso() + usage.sources[source] = usage.sources.get(source, 0) + 1 + items[key] = usage + _save_all(items) + return usage + + +def record_skill_run(name: str, *, success: bool, source: str = "run") -> SkillUsage: + """Record a skill execution or task-outcome evidence point.""" + key = name.strip() + if not key: + return SkillUsage(name="") + items = _load_all() + usage = items.get(key, SkillUsage(name=key)) + usage.runs += 1 + if success: + usage.successes += 1 + else: + usage.failures += 1 + usage.last_run_at = _utc_now_iso() + usage.sources[source] = usage.sources.get(source, 0) + 1 + items[key] = usage + _save_all(items) + return usage + + +def skill_usage_boost(name: str, *, inherited: bool = False) -> float: + """Small ranking adjustment from local evidence. + + The boost is deliberately bounded. Keyword relevance still dominates, but + repeated successful inherited skills rise above equally relevant unproven + siblings, and failing ones stop crowding the parent prompt. + """ + usage = get_skill_usage(name) + boost = min(1.5, usage.matches * 0.05) + boost += min(4.0, usage.successes * 1.0) + boost -= min(4.0, usage.failures * 1.25) + if inherited: + boost += min(1.0, usage.matches * 0.05) + if usage.runs and usage.success_rate >= 0.75: + boost += 1.0 + if usage.failures > usage.successes: + boost -= 1.5 + return boost diff --git a/src/agentdrive/sprint/__init__.py b/src/agentdrive/sprint/__init__.py index 7cc84bb..97c0282 100644 --- a/src/agentdrive/sprint/__init__.py +++ b/src/agentdrive/sprint/__init__.py @@ -10,4 +10,4 @@ "SprintResult", "SprintStep", "run_ship_chain", -] \ No newline at end of file +] diff --git a/src/agentdrive/sprint/chain.py b/src/agentdrive/sprint/chain.py index 66cbc08..e1e974c 100644 --- a/src/agentdrive/sprint/chain.py +++ b/src/agentdrive/sprint/chain.py @@ -10,8 +10,8 @@ from typing import Any from agentdrive.drive.drive import get_default_drive -from agentdrive.registry import GenomeRegistry from agentdrive.reconciliation import ReconciliationRunner +from agentdrive.registry import GenomeRegistry from agentdrive.sprint.checkpoint import CheckpointPending, CheckpointStore from agentdrive.synthesis.engine import _ensure_mandatory_gaps @@ -316,4 +316,4 @@ def run_ship_chain( prior_failed=False, ) - return results \ No newline at end of file + return results diff --git a/src/agentdrive/sprint/checkpoint.py b/src/agentdrive/sprint/checkpoint.py index d0285a3..353f7c9 100644 --- a/src/agentdrive/sprint/checkpoint.py +++ b/src/agentdrive/sprint/checkpoint.py @@ -179,4 +179,4 @@ def is_step_completed(self, step_id: str) -> bool: def reset_chain(self) -> None: """Clear completed steps and checkpoints for a fresh ship run.""" - self._save(self._default_state()) \ No newline at end of file + self._save(self._default_state()) diff --git a/src/agentdrive/system/integrated_real_time_evolution_system.py b/src/agentdrive/system/integrated_real_time_evolution_system.py index b67d578..f755a37 100644 --- a/src/agentdrive/system/integrated_real_time_evolution_system.py +++ b/src/agentdrive/system/integrated_real_time_evolution_system.py @@ -683,8 +683,170 @@ def get_parent_actionable_briefing(self) -> dict: ) except Exception: pass + + # Multiverse Cognition: recent collapses + open superposition for Parent + try: + from agentdrive.cognition import MultiverseEngine + + mv_engine = MultiverseEngine(self.recorder) + briefing["multiverse_context"] = mv_engine.briefing_context(limit=5) + briefing["multiverse_usage"] = ( + "On non-trivial decisions: multiverse_run_full(trigger=...) or " + "IntegratedRealTimeEvolutionSystem.run_multiverse_parent_decision(trigger=...)" + ) + except Exception: + pass + return briefing + def run_multiverse_parent_decision( + self, + trigger: str, + *, + n_branches: int = 7, + forward_steps: int | None = None, + program_id: str | None = None, + user_objective_refs: list[str] | None = None, + record_decision: bool = True, + durable: bool = False, + densify_invariants: bool = True, + use_llm: bool = True, + ) -> dict[str, Any]: + """ + Canonical Parent hook: full multiverse pipeline → collapse → record_parent_decision. + + Spawns Cognitive Agent Team role branches, extracts invariants, stress-tests, + collapses, and writes fabric DNA + parent_decision into the active evolution cycle. + """ + from agentdrive.cognition import MultiverseEngine + + engine = MultiverseEngine( + self.recorder, + program_id=program_id, + user_objective_refs=user_objective_refs, + use_llm=use_llm, + ) + session = engine.run_full( + trigger, + n_branches=n_branches, + forward_steps=forward_steps, + durable=durable, + densify_invariants=densify_invariants, + ) + + result: dict[str, Any] = { + "session_id": session.session_id, + "status": session.status.value, + "collapsed_branch_id": session.collapsed_branch_id, + "collapse_policy": (session.collapse_policy.value if session.collapse_policy else None), + "collapse_reason": session.collapse_reason, + "invariant_count": len(session.invariants), + "llm_mode": engine.resolve_llm_mode(trigger), + "session": engine.to_mcp_dict(session), + } + + if record_decision: + decision_result = engine.record_parent_decision( + session, + integrated=self, + actions_taken=[f"multiverse_run_full:{session.session_id}"], + ) + result.update(decision_result) + + return result + + def run_external_parent_decision( + self, + trigger: str, + branches: list[dict[str, Any]], + *, + collapsed_branch_id: str, + invariants: list[dict[str, Any]] | None = None, + collapse_reason: str = "", + collapse_policy: str | None = None, + reasoning_provider: str = "mcp-external", + convergence_points: list[str] | None = None, + divergence_points: list[str] | None = None, + fabric_reasoning: dict[str, Any] | None = None, + program_id: str | None = None, + user_objective_refs: list[str] | None = None, + record_decision: bool = True, + densify_invariants: bool = True, + ) -> dict[str, Any]: + """ + External MCP Parent path: Grok / Claude / Codex / Continue supply branch reasoning; + AgentDrive persists the collapse and records Parent DNA. + """ + from agentdrive.cognition import MultiverseEngine + + engine = MultiverseEngine( + self.recorder, + program_id=program_id, + user_objective_refs=user_objective_refs, + use_llm=False, + ) + session = engine.ingest_external_parent_decision( + trigger, + branches, + collapsed_branch_id=collapsed_branch_id, + invariants=invariants, + collapse_reason=collapse_reason, + collapse_policy=collapse_policy, + reasoning_provider=reasoning_provider, + convergence_points=convergence_points, + divergence_points=divergence_points, + fabric_reasoning=fabric_reasoning, + densify_invariants=densify_invariants, + ) + + collapsed = next( + (b for b in session.branches if b.branch_id == session.collapsed_branch_id), + None, + ) + result: dict[str, Any] = { + "session_id": session.session_id, + "status": session.status.value, + "collapsed_branch_id": session.collapsed_branch_id, + "collapse_policy": (session.collapse_policy.value if session.collapse_policy else None), + "collapse_reason": session.collapse_reason, + "invariant_count": len(session.invariants), + "llm_mode": "external", + "reasoning_provider": reasoning_provider, + "session": engine.to_mcp_dict(session), + } + + if record_decision: + decision_result = engine.record_parent_decision( + session, + integrated=self, + actions_taken=[f"external_parent_decision:{session.session_id}"], + ) + result.update(decision_result) + + if collapsed: + result["decision"] = { + "directive": collapsed.path_summary, + "multiverse_session_id": session.session_id, + "collapsed_branch_id": session.collapsed_branch_id, + "collapse_policy": result.get("collapse_policy"), + } + + return result + + def reopen_stale_multiverse_sessions(self, *, max_age_hours: float = 24.0) -> list[str]: + """M4: reopen stale open superposition sessions (Grid background hook).""" + from agentdrive.cognition import MultiverseEngine + + engine = MultiverseEngine(self.recorder) + return engine.reopen_stale_sessions(max_age_hours=max_age_hours) + + def densify_multiverse_invariants(self, session_id: str) -> dict[str, Any]: + """M3: GraphGardener densification on multiverse invariant clusters.""" + from agentdrive.cognition import MultiverseEngine + + engine = MultiverseEngine(self.recorder) + return engine.densify_invariant_clusters(session_id) + def record_parent_decision( self, cycle_id: str | None, diff --git a/src/agentdrive/tui/app.py b/src/agentdrive/tui/app.py index 2678c17..c77f6d5 100644 --- a/src/agentdrive/tui/app.py +++ b/src/agentdrive/tui/app.py @@ -904,7 +904,10 @@ def _show_help(self) -> None: Section( "Board", [ - ("board / b / kanban", "AgentDrive Mission Board (terminal) — web Kanban at http://127.0.0.1:8421/ (run `agentdrive board`)"), + ( + "board / b / kanban", + "AgentDrive Mission Board (terminal) — web Kanban at http://127.0.0.1:8421/ (run `agentdrive board`)", + ), ("board recent", "compact recent-missions view"), ("board create ", "stage a Pending mission"), ("board stats", "lane counts + avg duration"), diff --git a/src/agentdrive/tui/chat.py b/src/agentdrive/tui/chat.py index ef6319c..01484fc 100644 --- a/src/agentdrive/tui/chat.py +++ b/src/agentdrive/tui/chat.py @@ -692,9 +692,7 @@ def _stream_assistant_reply(self, message: str) -> None: result_container: dict = {} self._message_lane.reset() - self._message_lane.set_session_id( - getattr(self.agent.session, "session_id", None) - ) + self._message_lane.set_session_id(getattr(self.agent.session, "session_id", None)) def worker() -> None: try: @@ -1438,12 +1436,9 @@ def _cmd_genome_search(self, query: str = "") -> None: for idx, m in enumerate(matches, 1): dom = ", ".join(m.domains[:2]) or "—" label = ( - f"[{p.muted}]{idx:>2}[/] [bold {p.genome}]{m.genome_id}[/] " - f"[dim]@{m.version}[/]" - ) - secondary = ( - f"{dom} [{p.muted}]·[/] score [{p.evolution}]{m.score:.2f}[/]" + f"[{p.muted}]{idx:>2}[/] [bold {p.genome}]{m.genome_id}[/] [dim]@{m.version}[/]" ) + secondary = f"{dom} [{p.muted}]·[/] score [{p.evolution}]{m.score:.2f}[/]" rows.append(TreeRow(label=label, secondary=secondary)) self.console.print() diff --git a/src/agentdrive/tui/experience.py b/src/agentdrive/tui/experience.py index c7f8f88..b522c91 100644 --- a/src/agentdrive/tui/experience.py +++ b/src/agentdrive/tui/experience.py @@ -13,7 +13,16 @@ from agentdrive.golden_path import GOLDEN_STEPS, run_walkthrough, verify_all from agentdrive.operations import run_operation -from agentdrive.tui.chrome import Group, Palette, Section, Tree, TreeRow, ok_line, section_panel, warn_line +from agentdrive.tui.chrome import ( + Group, + Palette, + Section, + Tree, + TreeRow, + ok_line, + section_panel, + warn_line, +) def _golden_path_config() -> dict[str, Any]: @@ -111,9 +120,7 @@ def render_golden_path_gate(console: Console, *, palette: Palette | None = None) f"[{p.muted}]In chat:[/] [{p.accent}]/golden-path run[/] " f"[{p.muted}]· shell:[/] [{p.accent}]agentdrive golden-path run[/]" ), - Text.from_markup( - f"[{p.muted}]Docs:[/] docs/GOLDEN_PATH.md" - ), + Text.from_markup(f"[{p.muted}]Docs:[/] docs/GOLDEN_PATH.md"), ], ) ) @@ -182,9 +189,7 @@ def handle_ops_slash( if sub in ("steps", "list"): for i, step in enumerate(GOLDEN_STEPS, 1): opt = " [dim](optional)[/]" if step.optional else "" - console.print( - f" [dim]{i}.[/] [{p.accent}]{step.title}[/]{opt} — {step.command}" - ) + console.print(f" [dim]{i}.[/] [{p.accent}]{step.title}[/]{opt} — {step.command}") elif sub == "verify": summary = golden_path_verify_summary() for item in summary.get("steps", []): @@ -243,9 +248,7 @@ def handle_ops_slash( # /learnings log my-key insight text here log_parts = rest.split(maxsplit=1) if len(log_parts) < 2: - console.print( - warn_line("Usage: /learnings log ", palette=p) - ) + console.print(warn_line("Usage: /learnings log ", palette=p)) return key, insight = log_parts[0], log_parts[1] result = run_operation( @@ -266,13 +269,9 @@ def handle_ops_slash( hits = LearningsStore().search(rest, limit=10) for entry in hits: - console.print( - f" [{p.genome}]{entry.get('key')}[/] {entry.get('insight', '')}" - ) + console.print(f" [{p.genome}]{entry.get('key')}[/] {entry.get('insight', '')}") else: - console.print( - warn_line("Usage: /learnings list|log|search ...", palette=p) - ) + console.print(warn_line("Usage: /learnings list|log|search ...", palette=p)) return if cmd == "/session": @@ -344,14 +343,15 @@ def handle_ops_slash( filtered = filter_events_by_type(events, type_filter) counts = summarize_event_types(events) - filter_note = f" [dim]· filter {type_filter} ({len(filtered)}/{len(events)})[/]" if type_filter else "" + filter_note = ( + f" [dim]· filter {type_filter} ({len(filtered)}/{len(events)})[/]" + if type_filter + else "" + ) if sub == "panel": type_rows = [(ev_type, str(n)) for ev_type, n in counts.items()] - timeline_rows = [ - TreeRow(label=format_event_summary(ev)) - for ev in filtered[-60:] - ] + timeline_rows = [TreeRow(label=format_event_summary(ev)) for ev in filtered[-60:]] if len(filtered) > 60: timeline_rows.insert( 0, @@ -361,7 +361,9 @@ def handle_ops_slash( ) console.print( section_panel( - Section("Session", [(resolved, path.name), ("events", str(len(events)))], palette=p), + Section( + "Session", [(resolved, path.name), ("events", str(len(events)))], palette=p + ), Section( "Event types", type_rows or [("(none)", "0")], @@ -415,13 +417,14 @@ def handle_ops_slash( if sub == "list": entries = list_skills() if not entries: - console.print(warn_line("No skills found under ~/.agentdrive/skills", palette=p)) + console.print( + warn_line("No skills found under ~/.agentdrive/skills", palette=p) + ) return for entry in entries: op = f" [dim]→ {entry.operation}[/]" if entry.operation else "" console.print( - f" [{p.accent}]{entry.name}[/]{op} " - f"[dim]{entry.description[:60]}[/]" + f" [{p.accent}]{entry.name}[/]{op} [dim]{entry.description[:60]}[/]" ) console.print() console.print( @@ -474,4 +477,4 @@ def handle_ops_slash( console.print(format_skill_result(result)) return - console.print(warn_line(f"Unknown ops slash: {cmd}", palette=p)) \ No newline at end of file + console.print(warn_line(f"Unknown ops slash: {cmd}", palette=p)) diff --git a/src/agentdrive/tui/message_stream_lane.py b/src/agentdrive/tui/message_stream_lane.py index 5f98fe7..a3173c8 100644 --- a/src/agentdrive/tui/message_stream_lane.py +++ b/src/agentdrive/tui/message_stream_lane.py @@ -71,4 +71,4 @@ def detach(self) -> None: unsubscribe(tok) except Exception: pass - self._tokens.clear() \ No newline at end of file + self._tokens.clear() diff --git a/src/agentdrive/tui/pool_lane.py b/src/agentdrive/tui/pool_lane.py index e2abe2a..97e0022 100644 --- a/src/agentdrive/tui/pool_lane.py +++ b/src/agentdrive/tui/pool_lane.py @@ -49,8 +49,7 @@ def _on_match(ev: PoolMatch) -> None: extra = len(ev.genomes) - 3 tail = f" +{extra}" if extra > 0 else "" _set( - f"[{p.genome}]▸ matched {len(ev.genomes)} genomes[/] " - f"[{p.muted}]({scores}{tail})[/]" + f"[{p.genome}]▸ matched {len(ev.genomes)} genomes[/] [{p.muted}]({scores}{tail})[/]" ) def _on_ingest(ev: PoolIngest) -> None: @@ -98,4 +97,4 @@ def renderable(self) -> Text | None: text.append(" ─ pool ", style=p.muted) text.append_text(Text.from_markup(self._line)) text.append(" ─", style=p.muted) - return text \ No newline at end of file + return text diff --git a/src/agentdrive/tui/swarm_lane.py b/src/agentdrive/tui/swarm_lane.py index 67728ee..c11640c 100644 --- a/src/agentdrive/tui/swarm_lane.py +++ b/src/agentdrive/tui/swarm_lane.py @@ -76,9 +76,7 @@ def has_swarm_activity(self) -> bool: def _is_active_locked(self) -> bool: """Swarm in flight — caller must hold ``_lock``.""" - return self._child_count() > 0 and ( - self._active or not self._tree.is_done() - ) + return self._child_count() > 0 and (self._active or not self._tree.is_done()) def renderable(self) -> RenderableType | None: """Rich renderable for the activity lane, or None when collapsed.""" @@ -92,9 +90,7 @@ def renderable(self) -> RenderableType | None: def summary_line(self) -> str | None: """One-line post-swarm summary when collapsed.""" with self._lock: - children = [ - n for n in self._tree.nodes() if n.subagent_id != self._root_id - ] + children = [n for n in self._tree.nodes() if n.subagent_id != self._root_id] if not children: return None done = sum(1 for n in children if n.status == "done") @@ -102,10 +98,7 @@ def summary_line(self) -> str | None: total = len(children) p = self.palette if failed: - return ( - f"[{p.warn}]swarm · {done}/{total} ok · " - f"[{p.error}]{failed} failed[/][/]" - ) + return f"[{p.warn}]swarm · {done}/{total} ok · [{p.error}]{failed} failed[/][/]" return f"[{p.ok}]swarm · {done}/{total} sub-agents complete[/]" def reset(self) -> None: @@ -115,4 +108,4 @@ def reset(self) -> None: root_id=self._root_id, root_label=self._root_label, ) - self._active = False \ No newline at end of file + self._active = False diff --git a/src/agentdrive/tui/transcript_lane.py b/src/agentdrive/tui/transcript_lane.py index 59abb1a..c370ba5 100644 --- a/src/agentdrive/tui/transcript_lane.py +++ b/src/agentdrive/tui/transcript_lane.py @@ -189,4 +189,4 @@ def detach(self) -> None: unsubscribe(tok) except Exception: pass - self._tokens.clear() \ No newline at end of file + self._tokens.clear() diff --git a/tests/mission_control/test_v15_surfaces.py b/tests/mission_control/test_v15_surfaces.py index a9c2703..8f95a10 100644 --- a/tests/mission_control/test_v15_surfaces.py +++ b/tests/mission_control/test_v15_surfaces.py @@ -45,7 +45,9 @@ def test_smoke_mission_control_with_integrated_system_covers_core_surfaces(): # Core families from recorder emissions + command dispatches + rich static fire assert result["saw_loop_step"] or "loop_step" in str(result.get("counts_by_type", {})) assert result["saw_fabric_update"] or "fabric_update" in str(result.get("counts_by_type", {})) - assert result["saw_parent_decision"] or "parent_decision" in str(result.get("counts_by_type", {})) + assert result["saw_parent_decision"] or "parent_decision" in str( + result.get("counts_by_type", {}) + ) assert result["saw_static_fire"] or "static_fire" in str(result.get("counts_by_type", {})) assert "command_results" in result # All commands in the smoke either succeeded or gracefully reported no_mission (none should be unknown) @@ -107,7 +109,9 @@ def test_replay_seq_integrity_and_bounded(): # Seed some events with increasing seq for i in range(5): hub._record_event_for_introspection( - LoopStepEvent(event_type="loop_step", timestamp=time.time(), step=1, description=f"seed-{i}") + LoopStepEvent( + event_type="loop_step", timestamp=time.time(), step=1, description=f"seed-{i}" + ) ) # Simulate the replay logic exactly as in handle_inbound_command @@ -118,7 +122,9 @@ def test_replay_seq_integrity_and_bounded(): assert hub._event_seq >= max(e["seq"] for e in replay) # Bounded behavior (the [:64] guard) for _ in range(100): - hub._record_event_for_introspection(LoopStepEvent(event_type="loop_step", timestamp=time.time(), step=6)) + hub._record_event_for_introspection( + LoopStepEvent(event_type="loop_step", timestamp=time.time(), step=6) + ) replay2 = [e for e in hub.recent_events if e.get("seq", 0) > 0][:64] assert len(replay2) <= 64 @@ -129,6 +135,7 @@ def test_rich_static_fire_telemetry_publish_and_context(phase): hub = MissionControlHub() # Swap module hub for test isolation (restore after) import agentdrive.mission_control.server as mc_server + orig = mc_server.hub try: mc_server.hub = hub # type: ignore[assignment] @@ -143,7 +150,9 @@ def test_rich_static_fire_telemetry_publish_and_context(phase): total_lift=8.0, parent_interventions=1, fabric_edges_delta=4, - key_events=[{"type": "parent_intervention", "summary": "steer in stabilization-wave-20260531"}], + key_events=[ + {"type": "parent_intervention", "summary": "steer in stabilization-wave-20260531"} + ], final_report={"post_densif_fabric": {"coherence_end": 0.89}, "lift_pct": 8.0}, recorder_snippets=["rec:stabilization-test"], log_line="rich telemetry test", @@ -165,7 +174,9 @@ def test_rich_static_fire_telemetry_publish_and_context(phase): coherence_start=0.80, ) as sess: sess.report_progress(cycles_completed=1, current_coherence=0.83, log_line="mid") - sess.record_intervention("test decision during fire", cycle_id="stabilization-20260531-c1") + sess.record_intervention( + "test decision during fire", cycle_id="stabilization-20260531-c1" + ) sess.add_recorder_snippet("fabric delta in fire") if phase == "completed": sess.complete(final_coherence=0.91) @@ -214,6 +225,7 @@ def test_daily_dream_emission_path_via_durable_helper(): hub = MissionControlHub() import agentdrive.mission_control.server as mc_server + orig = mc_server.hub try: mc_server.hub = hub @@ -240,7 +252,9 @@ def test_daily_dream_emission_path_via_durable_helper(): metadata={"stabilization_wave": "stabilization-wave-20260531"}, ) - events = [e for e in hub.recent_events if "daily" in str(e).lower() or e.get("cycle_id") == _cid] + events = [ + e for e in hub.recent_events if "daily" in str(e).lower() or e.get("cycle_id") == _cid + ] assert len(events) >= 2 types = {e.get("event_type") for e in events} assert "loop_step" in types @@ -257,7 +271,9 @@ def test_attach_points_on_integrated_do_not_crash(): IntegratedRealTimeEvolutionSystem, ) - system = IntegratedRealTimeEvolutionSystem(swarm_id="stabilization-wave-20260531", overseer_poll_interval_s=0.01) + system = IntegratedRealTimeEvolutionSystem( + swarm_id="stabilization-wave-20260531", overseer_poll_interval_s=0.01 + ) hub = MissionControlHub() # The attach method (and internal recorder/overseer/grid wiring) diff --git a/tests/test_auto_learning.py b/tests/test_auto_learning.py new file mode 100644 index 0000000..6aa0c6b --- /dev/null +++ b/tests/test_auto_learning.py @@ -0,0 +1,127 @@ +"""Tests for end-to-end automatic learning on run_operation.""" + +from __future__ import annotations + +import pytest + +from agentdrive.learning.auto_absorb import reset_sessions +from agentdrive.operations.registry import run_operation +from agentdrive.skills import get_skill + + +@pytest.fixture(autouse=True) +def _clean_learning_sessions(): + reset_sessions() + yield + reset_sessions() + + +@pytest.fixture +def swarm_id(isolated_agentdrive_home): + return "stabilization-wave-20260531" + + +def _sample_branches(): + return [ + { + "branch_id": "branch:operator-1", + "role": "operator", + "path_summary": "Fund credits then judge Gate drafts", + "robustness_score": 0.9, + "stress_test_passed": True, + }, + ] + + +def test_dry_run_skips_auto_learning(swarm_id) -> None: + result = run_operation( + "external_parent_decision", + dry_run=True, + trigger="No absorb on dry run", + branches=_sample_branches(), + collapsed_branch_id="branch:operator-1", + swarm_id=swarm_id, + ) + assert result.get("success") is True + assert "auto_learning" not in result + + +def test_auto_learning_disabled(monkeypatch: pytest.MonkeyPatch, swarm_id) -> None: + monkeypatch.setenv("AGENTDRIVE_AUTO_LEARN", "0") + result = run_operation( + "external_parent_decision", + trigger="Disabled auto learn", + branches=_sample_branches(), + collapsed_branch_id="branch:operator-1", + swarm_id=swarm_id, + reasoning_provider="test", + ) + assert result.get("success") is True + assert "auto_learning" not in result + + +def test_external_parent_decision_auto_distills_skill(swarm_id) -> None: + result = run_operation( + "external_parent_decision", + trigger="Interegy fund xAI before VPS", + branches=_sample_branches(), + collapsed_branch_id="branch:operator-1", + collapse_reason="Operator path", + reasoning_provider="grok-test", + program_id="grok-test@stabilization-wave-20260531", + fabric_reasoning={ + "decision_rationale": "Draft quality unproven", + "llm_mode": "external", + }, + swarm_id=swarm_id, + ) + assert result.get("success") is True + auto = result.get("auto_learning") or {} + assert auto.get("operation") == "external_parent_decision" + skill = auto.get("skill") or {} + assert skill.get("name", "").startswith("learned-parent-decision") + installed = get_skill(skill["name"]) + assert installed is not None + assert "auto-learned" in installed.tags + assert installed.category in ("inherited", "promoted") + + +def test_think_after_context_pack_auto_records_reasoning(swarm_id) -> None: + run_operation( + "experience_graph_context_pack", + swarm_id=swarm_id, + max_tokens=400, + ) + result = run_operation( + "think", + question="What is the next move for Interegy launch?", + swarm_id=swarm_id, + ) + assert result.get("success") is True + auto = result.get("auto_learning") or {} + assert auto.get("reasoning_trace") or auto.get("skill") + + +def test_auto_learn_updates_existing_skill_revision(swarm_id) -> None: + first = run_operation( + "external_parent_decision", + trigger="Same trigger revision one", + branches=_sample_branches(), + collapsed_branch_id="branch:operator-1", + swarm_id=swarm_id, + reasoning_provider="test", + ) + second = run_operation( + "external_parent_decision", + trigger="Same trigger revision one", + branches=_sample_branches(), + collapsed_branch_id="branch:operator-1", + swarm_id=swarm_id, + reasoning_provider="test", + ) + name1 = (first.get("auto_learning") or {}).get("skill", {}).get("name") + name2 = (second.get("auto_learning") or {}).get("skill", {}).get("name") + assert name1 and name1 == name2 + entry = get_skill(name1) + assert entry is not None + assert entry.body.count("Auto-learned playbook") >= 1 diff --git a/tests/test_bootstrap_registry.py b/tests/test_bootstrap_registry.py index 06859cb..0d3c32d 100644 --- a/tests/test_bootstrap_registry.py +++ b/tests/test_bootstrap_registry.py @@ -4,8 +4,6 @@ from pathlib import Path -import pytest - from agentdrive.constants import get_default_drive_path, get_genomes_dir from agentdrive.drive.bootstrap import ( _migrate_legacy_personal_genomes, @@ -47,4 +45,4 @@ def test_ensure_experience_layer_seed_registers_in_home_genomes( home_reg = GenomeRegistry() ids = home_reg.list_genomes() assert any("living-experience-seed-v3" in gid for gid in ids) - assert (get_genomes_dir() / "living-experience-seed-v3").exists() \ No newline at end of file + assert (get_genomes_dir() / "living-experience-seed-v3").exists() diff --git a/tests/test_cli_commands.py b/tests/test_cli_commands.py index ce5c9ec..c855bf3 100644 --- a/tests/test_cli_commands.py +++ b/tests/test_cli_commands.py @@ -9,8 +9,6 @@ import sys from pathlib import Path -import pytest - from agentdrive.cli_catalog import CATALOG, search_catalog from agentdrive.cli_surface import build_help_epilog @@ -220,4 +218,4 @@ def test_eval_replay_missing_file(isolated_agentdrive_home): home=isolated_agentdrive_home, ) assert result.returncode == 1 - assert "not found" in result.stdout.lower() or "Artifact" in result.stdout \ No newline at end of file + assert "not found" in result.stdout.lower() or "Artifact" in result.stdout diff --git a/tests/test_cli_json_output.py b/tests/test_cli_json_output.py index 21f3f27..b8684e9 100644 --- a/tests/test_cli_json_output.py +++ b/tests/test_cli_json_output.py @@ -31,4 +31,4 @@ def test_learnings_list_json_parseable(isolated_agentdrive_home: Path): proc = _run("learnings", "list", "--json", home=isolated_agentdrive_home) assert proc.returncode == 0 data = json.loads(proc.stdout.strip()) - assert data.get("success") is True \ No newline at end of file + assert data.get("success") is True diff --git a/tests/test_codebase_patterns.py b/tests/test_codebase_patterns.py new file mode 100644 index 0000000..a75f732 --- /dev/null +++ b/tests/test_codebase_patterns.py @@ -0,0 +1,121 @@ +"""Tests for codebase pattern recognition framework.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from agentdrive.codebase.analyzer import analyze_content +from agentdrive.codebase.framework import crystallize_framework, match_against_framework +from agentdrive.codebase.observe import observe_file +from agentdrive.codebase.registry import register_project +from agentdrive.learning.auto_absorb import reset_sessions +from agentdrive.operations.registry import run_operation + + +@pytest.fixture(autouse=True) +def _clean_learning(): + reset_sessions() + yield + reset_sessions() + + +@pytest.fixture +def sample_project(tmp_path: Path) -> tuple[str, Path]: + root = tmp_path / "demo-app" + lib = root / "lib" + lib.mkdir(parents=True) + (lib / "service.py").write_text( + '"""Service module."""\n\n' + "import logging\n\n" + "logger = logging.getLogger(__name__)\n\n" + "def fetch_data(item_id: str) -> dict:\n" + ' """Fetch one item."""\n' + " return {'id': item_id}\n", + encoding="utf-8", + ) + (lib / "models.py").write_text( + "from dataclasses import dataclass\n\n" + "@dataclass\n" + "class ItemRecord:\n" + " item_id: str\n" + " label: str\n", + encoding="utf-8", + ) + (root / "test_service.py").write_text( + "import pytest\n\ndef test_fetch():\n assert True\n", + encoding="utf-8", + ) + register_project(project_id="demo-app", root=str(root), primary_language="python") + return "demo-app", root + + +def test_analyze_python_snake_case() -> None: + signals = analyze_content( + path="lib/service.py", + content="def load_user_data() -> str:\n pass\n", + ) + assert signals.language == "python" + assert signals.signals.get("function_naming") == "snake_case" + + +def test_observe_files_builds_framework(sample_project: tuple[str, Path]) -> None: + project_id, _ = sample_project + for rel in ("lib/service.py", "lib/models.py", "test_service.py"): + result = observe_file(project_id=project_id, path=rel) + assert result["success"] is True + + framework = crystallize_framework(project_id, force=True) + patterns = framework.get("patterns") or [] + assert framework["file_count"] == 3 + assert any(p["id"].startswith("naming-functions-") for p in patterns) + assert framework.get("writing_guide") + + +def test_match_snippet_alignment(sample_project: tuple[str, Path]) -> None: + project_id, _ = sample_project + observe_file(project_id=project_id, path="lib/service.py") + observe_file(project_id=project_id, path="lib/models.py") + observe_file(project_id=project_id, path="test_service.py") + crystallize_framework(project_id, force=True) + + good = match_against_framework( + project_id, + code="def save_user_record(user_id: str) -> None:\n pass\n", + path="lib/new.py", + ) + assert good["alignment_score"] >= 0.0 + + bad = match_against_framework( + project_id, + code="function loadUserData() { return null; }", + path="lib/bad.ts", + ) + assert "conflicts" in bad + + +def test_run_operation_observe_with_auto_learning( + sample_project: tuple[str, Path], + isolated_agentdrive_home: Path, +) -> None: + project_id, _ = sample_project + result = run_operation( + "codebase_observe_file", + project_id=project_id, + path="lib/service.py", + ) + assert result.get("success") is True + assert result.get("auto_learning") is not None + skill = (result.get("auto_learning") or {}).get("skill") or {} + assert skill.get("name", "").startswith(f"learned-{project_id}-observe") + + +def test_register_and_list_via_operations(sample_project: tuple[str, Path]) -> None: + project_id, root = sample_project + listed = run_operation("codebase_list_projects") + assert listed.get("success") is True + assert listed.get("count", 0) >= 1 + profile = run_operation("codebase_patterns_profile", project_id=project_id) + assert profile.get("success") is True + assert profile.get("framework") is not None diff --git a/tests/test_compose_layers.py b/tests/test_compose_layers.py index 74ab830..f09963f 100644 --- a/tests/test_compose_layers.py +++ b/tests/test_compose_layers.py @@ -146,4 +146,4 @@ def test_compose_context_backward_compatible_without_layers() -> None: def test_compose_layers_exported_from_harness_package() -> None: from agentdrive.harness import ComposeLayers as ExportedComposeLayers - assert ExportedComposeLayers is ComposeLayers \ No newline at end of file + assert ExportedComposeLayers is ComposeLayers diff --git a/tests/test_doctor_verbose.py b/tests/test_doctor_verbose.py index 7ba0307..4414c58 100644 --- a/tests/test_doctor_verbose.py +++ b/tests/test_doctor_verbose.py @@ -32,4 +32,4 @@ def _capture(*args, **kwargs) -> None: for arg in printed: render_console.print(arg) rendered = buf.getvalue() - assert "Verbose diagnostics" in rendered \ No newline at end of file + assert "Verbose diagnostics" in rendered diff --git a/tests/test_dream_cycle.py b/tests/test_dream_cycle.py index d303140..f5f1d64 100644 --- a/tests/test_dream_cycle.py +++ b/tests/test_dream_cycle.py @@ -6,8 +6,6 @@ import threading from pathlib import Path -import pytest - from agentdrive.dreaming.cycle import ( DREAM_PHASES, DreamCycleLockError, @@ -84,4 +82,4 @@ def test_get_dream_cycle_status(isolated_agentdrive_home: Path) -> None: status = get_dream_cycle_status(home=isolated_agentdrive_home) assert status["lock_held"] is False assert len(status["phases"]) == 5 - assert status["last_run"] is not None \ No newline at end of file + assert status["last_run"] is not None diff --git a/tests/test_drive_retrieval.py b/tests/test_drive_retrieval.py index 2f830aa..963f3d0 100644 --- a/tests/test_drive_retrieval.py +++ b/tests/test_drive_retrieval.py @@ -2,7 +2,6 @@ from __future__ import annotations -import os from datetime import UTC, datetime import pytest @@ -27,7 +26,9 @@ def _genome( ) return Genome( manifest=manifest, - framework={"steps": [{"id": str(i), "name": s} for i, s in enumerate(framework_steps or [])]}, + framework={ + "steps": [{"id": str(i), "name": s} for i, s in enumerate(framework_steps or [])] + }, reasoning_patterns={p: {"weight": 1.0} for p in (patterns or [])}, ) @@ -66,4 +67,4 @@ def test_query_flag_off_uses_additive_mode(tmp_path, monkeypatch: pytest.MonkeyP assert results fusion = getattr(results[0], "_hybrid_fusion", None) if fusion: - assert fusion.get("mode") in (None, "additive") \ No newline at end of file + assert fusion.get("mode") in (None, "additive") diff --git a/tests/test_eval_replay.py b/tests/test_eval_replay.py index 5d3a6ed..b127389 100644 --- a/tests/test_eval_replay.py +++ b/tests/test_eval_replay.py @@ -21,4 +21,4 @@ def test_replay_healingfactor_artifact_decision() -> None: result = replay_genome_artifact_file(path, tolerance=0.25) assert result["stored_decision"] == "keep_promote_with_lineage" assert result["decision_match"] is True - assert result["replayed_decision"] == "keep_promote_with_lineage" \ No newline at end of file + assert result["replayed_decision"] == "keep_promote_with_lineage" diff --git a/tests/test_experience_graph.py b/tests/test_experience_graph.py index 47842fb..b4f1fa1 100644 --- a/tests/test_experience_graph.py +++ b/tests/test_experience_graph.py @@ -7,7 +7,6 @@ from __future__ import annotations import json -import time from pathlib import Path import pytest @@ -16,18 +15,17 @@ from agentdrive.evolution.experience_graph import ( CONNECTION_STRENGTHENED_BY, CROSS_CYCLE_CONTINUATION, - DENSIFIED_VIA_GARDENER, DENSIFICATION_INVERSE_MAP, + DENSIFIED_VIA_GARDENER, FABRIC_COHERENCE_CONTRIBUTED, FABRIC_LINK, GRAPH_COHERENCE_LIFT, - LoopEdge, PARENT_FABRIC_REASONING_TRACE, ExperienceGraphRecorder, + LoopEdge, get_recorder_for_drive, ) - # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @@ -102,7 +100,9 @@ def test_record_artifact_adds_to_cycle(recorder: ExperienceGraphRecorder) -> Non assert graph["artifacts"][0]["type"] == "overseer_briefing" -def test_record_connection_creates_loop_edge_and_persists(recorder: ExperienceGraphRecorder) -> None: +def test_record_connection_creates_loop_edge_and_persists( + recorder: ExperienceGraphRecorder, +) -> None: cycle_id = recorder.start_cycle("corr-conn-001") edge = recorder.record_connection( cycle_id, @@ -118,9 +118,7 @@ def test_record_connection_creates_loop_edge_and_persists(recorder: ExperienceGr relations = {c["relation"] for c in data["connections"]} assert "overseer_briefing_informed_parent_decision" in relations # Bidirectional mirror (Obsidian-style) - assert any( - c["source"] == "node-b" and c["target"] == "node-a" for c in data["connections"] - ) + assert any(c["source"] == "node-b" and c["target"] == "node-a" for c in data["connections"]) def test_record_connection_unknown_cycle_creates_minimal_cycle( @@ -207,12 +205,22 @@ def test_get_fabric_context_pack_returns_expected_keys(recorder: ExperienceGraph "top_weak_clusters", "strong_continuations", "recent_high_value_densifications", + "memory_systems_triage", "actionable_structural_recommendations", "compact_graph_summary", ): assert key in pack assert pack["reasoning_style"] == "balanced" assert isinstance(pack["top_weak_clusters"], list) + assert set(pack["memory_systems_triage"]["queues"]) == { + "working_set", + "consolidate", + "reconsolidate", + "archive", + } + assert ( + pack["memory_systems_triage"]["control_plan"]["primary_context_order"][0] == "working_set" + ) def test_get_recent_parent_fabric_reasoning_traces_returns_list( @@ -235,7 +243,9 @@ def test_get_recent_parent_fabric_reasoning_traces_returns_list( ) -def test_suggest_fabric_reasoning_structure_returns_schema(recorder: ExperienceGraphRecorder) -> None: +def test_suggest_fabric_reasoning_structure_returns_schema( + recorder: ExperienceGraphRecorder, +) -> None: schema = recorder.suggest_fabric_reasoning_structure() assert "fabric_reasoning_prompt_template" in schema template = schema["fabric_reasoning_prompt_template"] @@ -326,7 +336,9 @@ def test_record_densification_lift_updates_coherence(recorder: ExperienceGraphRe cycle_id = recorder.start_cycle("corr-lift-001") cycle = recorder._active_cycles[cycle_id] cycle.enter_densification_phase() - recorder.record_densification_lift(cycle_id, pre_coherence=0.55, post_coherence=0.72, new_edge_count=3) + recorder.record_densification_lift( + cycle_id, pre_coherence=0.55, post_coherence=0.72, new_edge_count=3 + ) data = _load_cycle_json(recorder, cycle_id) assert data["coherence_score"] >= 0.72 assert data["status"] == "closed" @@ -462,9 +474,11 @@ def test_find_weak_connections_on_sparse_cycle(recorder: ExperienceGraphRecorder def test_aggregate_graph_across_cycles_includes_both(recorder: ExperienceGraphRecorder) -> None: c1 = recorder.start_cycle(new_correlation_id()) c2 = recorder.start_cycle(new_correlation_id()) - recorder.record_fabric_contribution(c1, target_cycle=c2, contribution_type=CROSS_CYCLE_CONTINUATION) + recorder.record_fabric_contribution( + c1, target_cycle=c2, contribution_type=CROSS_CYCLE_CONTINUATION + ) agg = recorder.aggregate_graph_across_cycles(lookback_days=7) assert agg.get("cycle_count", 0) >= 2 participating = set(agg.get("participating_cycles", [])) assert c1 in participating - assert c2 in participating \ No newline at end of file + assert c2 in participating diff --git a/tests/test_external_parent_decision.py b/tests/test_external_parent_decision.py new file mode 100644 index 0000000..2482668 --- /dev/null +++ b/tests/test_external_parent_decision.py @@ -0,0 +1,126 @@ +"""Tests for external MCP Parent multiverse submission (Grok/Claude/Codex path).""" + +from __future__ import annotations + +import pytest + +from agentdrive.operations.registry import run_operation + + +@pytest.fixture +def swarm_id(isolated_agentdrive_home): + return "stabilization-wave-20260531" + + +def _sample_branches(): + return [ + { + "branch_id": "branch:architect-0", + "role": "architect", + "path_summary": "Map system before intervening", + "assumptions": ["Wiring complete"], + "robustness_score": 0.8, + "stress_test_passed": True, + }, + { + "branch_id": "branch:operator-1", + "role": "operator", + "path_summary": "Fund credits then judge Gate drafts", + "assumptions": ["Draft quality is the load-bearing risk"], + "robustness_score": 0.9, + "stress_test_passed": True, + }, + ] + + +def test_external_parent_decision_dry_run(swarm_id) -> None: + result = run_operation( + "external_parent_decision", + dry_run=True, + trigger="Interegy launch path", + branches=_sample_branches(), + collapsed_branch_id="branch:operator-1", + swarm_id=swarm_id, + reasoning_provider="grok-test", + ) + assert result.get("success") is True + assert result.get("dry_run") is True + assert result.get("operation") == "external_parent_decision" + + +def test_external_parent_decision_records_external_session(swarm_id) -> None: + result = run_operation( + "external_parent_decision", + trigger="External MCP parent smoke test", + branches=_sample_branches(), + collapsed_branch_id="branch:operator-1", + invariants=[ + { + "statement": "Draft quality at Gate is load-bearing", + "branch_coverage": 1.0, + "kind": "robust", + "source_branches": ["branch:architect-0", "branch:operator-1"], + } + ], + collapse_reason="Operator path: cheapest reversible quality proof", + reasoning_provider="grok-test", + program_id="grok-interegy-web@stabilization-wave-20260531", + fabric_reasoning={ + "fabric_elements_considered": ["interegy-web/HANDOFF.md"], + "decision_rationale": "Fund xAI before VPS", + "expected_lift_signal": 0.12, + "llm_mode": "external", + }, + swarm_id=swarm_id, + ) + assert result.get("success") is True + payload = result.get("result") or {} + assert payload.get("llm_mode") == "external" + assert payload.get("reasoning_provider") == "grok-test" + assert payload.get("collapsed_branch_id") == "branch:operator-1" + session = payload.get("session") or {} + assert session.get("llm_mode") == "external" + assert session.get("reasoning_provider") == "grok-test" + + session_id = payload.get("session_id") + assert session_id + + loaded = run_operation( + "multiverse_get_session", + session_id=session_id, + swarm_id=swarm_id, + ) + assert loaded.get("success") is True + loaded_session = loaded.get("session") or {} + assert loaded_session.get("llm_mode") == "external" + + +def test_external_parent_requires_branches(swarm_id) -> None: + result = run_operation( + "external_parent_decision", + trigger="missing branches", + branches=[], + collapsed_branch_id="branch:x", + swarm_id=swarm_id, + ) + assert result.get("success") is False + + +def test_mcp_server_registers_external_parent_tool() -> None: + pytest.importorskip("mcp.server.fastmcp") + from agentdrive.adapters.mcp_server import create_mcp_server + + server = create_mcp_server() + tools = set(server._tool_manager._tools.keys()) # noqa: SLF001 + assert "external_parent_decision" in tools + + +def test_suggest_reasoning_documents_external_flow(swarm_id) -> None: + result = run_operation( + "experience_graph_suggest_reasoning", + swarm_id=swarm_id, + ) + structure = result.get("structure") or {} + assert "external_mcp_parent_flow" in structure + modes = structure.get("reasoning_provider_modes") or {} + assert "external_mcp" in modes diff --git a/tests/test_fabric_import.py b/tests/test_fabric_import.py index 1cc375d..b318cab 100644 --- a/tests/test_fabric_import.py +++ b/tests/test_fabric_import.py @@ -151,4 +151,4 @@ def test_resolve_fabric_root_from_env( resolved = resolve_fabric_root() - assert resolved == fabric_root.resolve() \ No newline at end of file + assert resolved == fabric_root.resolve() diff --git a/tests/test_framework_skills.py b/tests/test_framework_skills.py new file mode 100644 index 0000000..a797fdf --- /dev/null +++ b/tests/test_framework_skills.py @@ -0,0 +1,108 @@ +"""Tests for AgentDrive-as-framework skill playbook routing.""" + +from __future__ import annotations + +import pytest + +from agentdrive.learning.auto_absorb import reset_sessions +from agentdrive.learning.framework_skills import ( + build_framework_session_pack, + route_skills_for_task, +) +from agentdrive.operations.registry import run_operation +from agentdrive.skills.registry import install_inherited_skill + + +@pytest.fixture(autouse=True) +def _clean_sessions(): + reset_sessions() + yield + reset_sessions() + + +@pytest.fixture +def swarm_id(isolated_agentdrive_home): + return "framework-skills-test-swarm" + + +def _install_learned(name: str, *, when_to_call: str, project: str) -> None: + install_inherited_skill( + name=name, + description=f"Learned playbook for {project}", + body=f"# {name}\n\n1. Pull context.\n2. Apply {project} patterns.\n3. Record outcome.", + source_subagent_id="mcp-auto-learning", + swarm_id="framework-skills-test-swarm", + tags=["learned", project, "codebase-mimic"], + when_to_call=when_to_call, + operation="codebase_mimic", + update_existing=True, + ) + + +def test_route_prioritizes_learned_skills_for_project(swarm_id) -> None: + _install_learned( + "learned-openmangos-mimic-growth-merge", + when_to_call="Task resembles: wire growth merge into OpenMango context pack", + project="openmangos", + ) + _install_learned( + "learned-other-patterns", + when_to_call="Other project patterns", + project="other", + ) + matches = route_skills_for_task( + "wire growth merge into OpenMango context pack", + swarm_id=swarm_id, + project_id="openmangos", + learned_only=True, + ) + assert matches + assert matches[0].name.startswith("learned-openmangos") + assert matches[0].kind == "learned" + assert "framework_skill_run" in matches[0].invoke_hint + + +def test_framework_session_start_operation(swarm_id) -> None: + _install_learned( + "learned-openmangos-mimic-test", + when_to_call="OpenMango mimic tasks", + project="openmangos", + ) + result = run_operation( + "framework_session_start", + task="OpenMango adaptive terminal work", + project_id="openmangos", + swarm_id=swarm_id, + ) + assert result.get("success") is True + assert "framework_briefing" in result + assert result.get("learned_skill_count", 0) >= 1 + assert result.get("matched_skills") + + +def test_framework_skill_route_operation(swarm_id) -> None: + _install_learned( + "learned-demo-mimic-gateway", + when_to_call="Gateway helper tasks", + project="demo", + ) + result = run_operation( + "framework_skill_route", + task="gateway helper deploy", + project_id="demo", + swarm_id=swarm_id, + ) + assert result.get("success") is True + assert result.get("count", 0) >= 1 + assert "playbook" in result + + +def test_build_framework_session_pack_includes_workflow(swarm_id) -> None: + pack = build_framework_session_pack( + "ship feature", + swarm_id=swarm_id, + project_id="agentdrive", + ) + assert "framework_workflow" in pack + assert "framework_briefing" in pack + assert "AgentDrive framework loop" in pack["framework_briefing"] diff --git a/tests/test_genomes_api.py b/tests/test_genomes_api.py index 204fe71..f2f890b 100644 --- a/tests/test_genomes_api.py +++ b/tests/test_genomes_api.py @@ -87,4 +87,4 @@ def test_search_genomes_with_real_pool(registry: GenomeRegistry) -> None: matches = genomes_api.search_genomes("testing", pool=pool, limit=3) ids = {m.id for m in matches} - assert "real-search-hit" in ids \ No newline at end of file + assert "real-search-hit" in ids diff --git a/tests/test_golden_path.py b/tests/test_golden_path.py index 60687d9..5d4b3cf 100644 --- a/tests/test_golden_path.py +++ b/tests/test_golden_path.py @@ -99,4 +99,4 @@ def test_cli_golden_path_run_dry(isolated_agentdrive_home: Path): data = _json_from_stdout(proc.stdout) assert isinstance(data, dict) assert data.get("dry_run") is True - assert data.get("success") is True \ No newline at end of file + assert data.get("success") is True diff --git a/tests/test_grok_spawn_telemetry.py b/tests/test_grok_spawn_telemetry.py index 365bd9f..fc7a0b8 100644 --- a/tests/test_grok_spawn_telemetry.py +++ b/tests/test_grok_spawn_telemetry.py @@ -2,13 +2,17 @@ from __future__ import annotations +import sys import time +import types +from agentdrive.adapters import grok_build_adapter from agentdrive.agent.turn_telemetry import ( emit_external_subagent_done, emit_external_subagent_spawn, ) -from agentdrive.events import SubagentDone, SubagentSpawn, default_bus +from agentdrive.events import default_bus +from agentdrive.inheritance import load_manifest def test_external_spawn_and_done_pair(): @@ -25,4 +29,36 @@ def test_external_spawn_and_done_pair(): default_bus.unsubscribe(token) assert captured.count("SubagentSpawn") == 1 - assert captured.count("SubagentDone") == 1 \ No newline at end of file + assert captured.count("SubagentDone") == 1 + + +def test_grok_wrapper_writes_skill_handoff_manifest(monkeypatch): + fake_grok = types.ModuleType("grok_build") + + def _spawn_subagent(**_kwargs): + return """ +```agentdrive-skill +name: grok-worker-handoff +description: Convert a successful Grok worker result into a parent skill. +tags: [grok, handoff] +--- +# Grok Worker Handoff + +1. Preserve the worker's reusable procedure. +2. Return a parent-ready skill block with evidence. +``` +""" + + fake_grok.spawn_subagent = _spawn_subagent + monkeypatch.setitem(sys.modules, "grok_build", fake_grok) + monkeypatch.setattr(grok_build_adapter, "spawn_subagent", None) + + adapter = grok_build_adapter.GrokBuildAgentDriveAdapter() + adapter.activate_for_current_session(swarm_id="grok-swarm") + + fake_grok.spawn_subagent(task="teach parent from worker", subagent_id="worker-1") + + manifest = load_manifest("grok-swarm", "worker-1") + assert manifest is not None + assert manifest.skills_created[0].name == "grok-worker-handoff" + assert "worker's reusable procedure" in manifest.skills_created[0].body diff --git a/tests/test_growth_merge.py b/tests/test_growth_merge.py new file mode 100644 index 0000000..4886739 --- /dev/null +++ b/tests/test_growth_merge.py @@ -0,0 +1,137 @@ +"""Tests for growth merge — experience + patterns + memory compounding.""" + +from __future__ import annotations + +import pytest + +from agentdrive.learning.auto_absorb import ( + LearningSession, + maybe_absorb_operation_outcome, + reset_sessions, +) +from agentdrive.learning.growth_merge import ( + GrowthAxes, + merge_session_growth, + recognize_growth_patterns, +) +from agentdrive.memory import MemoryBankStore +from agentdrive.operations.registry import run_operation + + +@pytest.fixture(autouse=True) +def _clean_sessions(): + reset_sessions() + yield + reset_sessions() + + +@pytest.fixture +def swarm_id(isolated_agentdrive_home): + return "growth-merge-test-swarm" + + +def test_growth_axes_merge_ready() -> None: + assert GrowthAxes(experience=True, patterns=True).merge_ready() is True + assert GrowthAxes(experience=True).merge_ready() is False + + +def test_recognize_growth_patterns_from_memory(swarm_id) -> None: + MemoryBankStore(swarm_id).store( + kind="pattern", + title="Gateway bootstrap auth", + content="Interegy gateway uses X-Ren-API-Key for bootstrap requests.", + vault="interegy", + topic="auth", + ) + patterns = recognize_growth_patterns( + swarm_id=swarm_id, + trigger="gateway bootstrap auth header", + ) + assert patterns + assert any(p.source == "memory_bank" for p in patterns) + + +def test_merge_session_growth_writes_compound_memory( + swarm_id, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("AGENTDRIVE_AUTO_GROWTH_MERGE", "1") + session = LearningSession(swarm_id=swarm_id, program_id="test-program") + session.ops = [ + ("experience_graph_record_reasoning", "reasoning"), + ("codebase_mimic", "mimic"), + ] + session.experience_traces = ["trace-abc"] + session.pattern_projects = ["demo-project"] + session.distilled_skills = ["auto-think-demo"] + + record = merge_session_growth(session, trigger="Ship gateway helper") + assert record is not None + assert record.memory_id + assert "experience" in record.axes.present() + assert "patterns" in record.axes.present() + assert "skills" in record.axes.present() + + store = MemoryBankStore(swarm_id) + recalled = store.recall(record.memory_id) + assert recalled is not None + assert recalled.vault == "growth" + assert recalled.topic == "merge" + assert "Growth merge" in recalled.title + + +def test_auto_absorb_emits_growth_merge(swarm_id, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("AGENTDRIVE_AUTO_GROWTH_MERGE", "1") + MemoryBankStore(swarm_id).store( + kind="insight", + title="Prior gateway work", + content="Bootstrap uses credits before deploy.", + ) + result = { + "success": True, + "swarm_id": swarm_id, + "trace_slug": "fabric-trace-1", + "result": {"directive": "Fund credits first"}, + } + absorbed = maybe_absorb_operation_outcome( + "experience_graph_record_reasoning", + {"trigger": "Gateway deploy", "program_id": "growth-test", "swarm_id": swarm_id}, + result, + ) + assert absorbed is not None + # First op may not merge yet — simulate richer session + result2 = { + "success": True, + "swarm_id": swarm_id, + "project_id": "demo-project", + "mimicry_prompt": "Match handler naming in api/", + } + absorbed2 = maybe_absorb_operation_outcome( + "codebase_mimic", + { + "trigger": "Gateway deploy", + "program_id": "growth-test", + "swarm_id": swarm_id, + "project_id": "demo-project", + }, + result2, + ) + assert absorbed2 is not None + assert absorbed2.get("growth_merge") or absorbed2.get("memory") + + +def test_growth_merge_briefing_operation(swarm_id) -> None: + MemoryBankStore(swarm_id).store( + kind="insight", + title="Growth merge: deploy flow", + content="Compound growth from experience and patterns.", + vault="growth", + topic="merge", + ) + result = run_operation( + "growth_merge_briefing", + query="deploy flow", + swarm_id=swarm_id, + ) + assert result.get("success") is True + assert "growth_briefing" in result + assert result.get("axes_integrated") diff --git a/tests/test_inheritance.py b/tests/test_inheritance.py index 484c99f..9922543 100644 --- a/tests/test_inheritance.py +++ b/tests/test_inheritance.py @@ -15,12 +15,14 @@ from pathlib import Path import pytest +import yaml from agentdrive.drive.drive import AgentDrive from agentdrive.events import ( InheritanceAbsorbed, InheritanceReceived, PoolIngest, + SkillAssimilated, SubagentDone, default_bus, emit, @@ -31,12 +33,18 @@ from agentdrive.inheritance import ( InheritanceManifest, InheritanceResult, + InheritedSkillCandidate, + extract_skill_candidates_from_result, list_manifests, load_manifest, manifest_path, record_manifest, + write_subagent_result_manifest, ) from agentdrive.registry import GenomeRegistry +from agentdrive.skills.compose import match_skills_for_turn +from agentdrive.skills.registry import get_skill +from agentdrive.skills.usage import get_skill_usage # ───────────────────────────────────────────────────────────────────── # fixtures @@ -98,6 +106,21 @@ def _build_manifest(swarm_id: str, subagent_id: str, created: list[str]) -> Inhe ) +def _skill_candidate(name: str = "incident-retrospective-playbook") -> InheritedSkillCandidate: + return InheritedSkillCandidate( + name=name, + description="Reusable playbook learned by a sub-agent during incident review", + body=( + "# Incident Retrospective Playbook\n\n" + "1. Pull the recent failure timeline.\n" + "2. Compare the parent hypothesis against sub-agent findings.\n" + "3. Record reusable prevention DNA and follow-up owners." + ), + tags=["incident", "retrospective", "subagent"], + evidence={"score": 0.86, "source_task": "incident review"}, + ) + + # ───────────────────────────────────────────────────────────────────── # tests # ───────────────────────────────────────────────────────────────────── @@ -107,6 +130,7 @@ def test_manifest_round_trips_to_disk(isolated_agentdrive_home: Path) -> None: swarm = "swarm-A" sub = "sub-001" manifest = _build_manifest(swarm, sub, ["learned-genome"]) + manifest.skills_created.append(_skill_candidate()) p = manifest_path(swarm, sub) p.parent.mkdir(parents=True, exist_ok=True) @@ -117,6 +141,7 @@ def test_manifest_round_trips_to_disk(isolated_agentdrive_home: Path) -> None: assert back.subagent_id == sub assert back.swarm_id == swarm assert back.genomes_created == ["learned-genome"] + assert back.skills_created[0].name == "incident-retrospective-playbook" assert back.duration_s == 12.5 listed = list_manifests(swarm_id=swarm) @@ -124,6 +149,236 @@ def test_manifest_round_trips_to_disk(isolated_agentdrive_home: Path) -> None: assert listed[0].subagent_id == sub +def test_record_manifest_installs_subagent_skill_into_parent_bench( + registry: GenomeRegistry, + clean_bus: None, + isolated_agentdrive_home: Path, +) -> None: + parent_pool = AgentDrive(registry=registry) + manifest = _build_manifest("swarm-A", "analyst-7", []) + manifest.skills_created.append(_skill_candidate()) + + absorbed_events: list[InheritanceAbsorbed] = [] + summary: list[InheritanceReceived] = [] + t1 = subscribe(absorbed_events.append, [InheritanceAbsorbed]) + t2 = subscribe(summary.append, [InheritanceReceived]) + try: + result = record_manifest( + manifest, + target_pool=parent_pool, + auto_absorb=True, + ) + finally: + unsubscribe(t1) + unsubscribe(t2) + + assert result.skills_absorbed == ["incident-retrospective-playbook"] + assert result.skills_rejected == [] + + installed = get_skill("incident-retrospective-playbook") + assert installed is not None + assert installed.category == "inherited" + assert installed.role == "shared" + assert installed.source == "inheritance:swarm-A:analyst-7" + assert "Pull the recent failure timeline" in installed.body + + usage_before_match = get_skill_usage("incident-retrospective-playbook") + assert usage_before_match.runs == 0 + assert usage_before_match.successes == 0 + + matched = match_skills_for_turn("run an incident retrospective after this outage") + assert any(skill.name == "incident-retrospective-playbook" for skill in matched) + assert any(e.skill_name == "incident-retrospective-playbook" for e in absorbed_events) + assert summary[0].skills_absorbed == ["incident-retrospective-playbook"] + + +def test_record_manifest_updates_existing_inherited_skill_revision( + registry: GenomeRegistry, + clean_bus: None, + isolated_agentdrive_home: Path, +) -> None: + parent_pool = AgentDrive(registry=registry) + first = _build_manifest("swarm-A", "analyst-7", []) + first.skills_created.append(_skill_candidate("incident-retrospective-playbook")) + second = _build_manifest("swarm-A", "analyst-8", []) + improved = _skill_candidate("incident-retrospective-playbook") + improved.description = "Improved playbook learned by another sub-agent" + improved.body = ( + "# Incident Retrospective Playbook\n\n" + "1. Pull the recent failure timeline.\n" + "2. Compare the parent hypothesis against sub-agent findings.\n" + "3. Add contradiction checks from each worker.\n" + "4. Record reusable prevention DNA and follow-up owners." + ) + improved.tags.append("contradiction") + second.skills_created.append(improved) + + first_result = record_manifest(first, target_pool=parent_pool, auto_absorb=True) + second_result = record_manifest(second, target_pool=parent_pool, auto_absorb=True) + + assert first_result.skills_absorbed == ["incident-retrospective-playbook"] + assert second_result.skills_absorbed == ["incident-retrospective-playbook"] + assert second_result.skills_rejected == [] + + installed = get_skill("incident-retrospective-playbook") + assert installed is not None + assert "Add contradiction checks" in installed.body + assert "contradiction" in installed.tags + assert installed.source == "inheritance:swarm-A:analyst-8" + + raw_meta = installed.path.read_text(encoding="utf-8").split("---", 2)[1] + meta = yaml.safe_load(raw_meta) + revisions = meta["inheritance"]["revisions"] + assert meta["inheritance"]["revision_count"] == 2 + assert revisions[0]["source"] == "inheritance:swarm-A:analyst-7" + assert revisions[1]["source"] == "inheritance:swarm-A:analyst-8" + + +def test_extract_skill_candidates_from_subagent_handoff_block() -> None: + result = """ +Sub-agent complete. + +```agentdrive-skill +name: outage-command-center +description: Use after an outage to coordinate findings into one command review. +tags: [incident, command, subagent] +--- +# Outage Command Center + +1. Gather each worker's timeline and confidence. +2. Collapse duplicate hypotheses before assigning owners. +3. Record the reusable prevention pattern. +``` +""" + + candidates = extract_skill_candidates_from_result( + result, + task="coordinate outage review", + ) + + assert len(candidates) == 1 + skill = candidates[0] + assert skill.name == "outage-command-center" + assert "coordinate findings" in skill.description + assert skill.tags == ["incident", "command", "subagent"] + assert "Collapse duplicate hypotheses" in skill.body + assert skill.evidence["source_task"] == "coordinate outage review" + + +def test_subagent_done_absorbs_skill_handoff_manifest( + registry: GenomeRegistry, + clean_bus: None, + inheritance_bus_subscribed: None, + isolated_agentdrive_home: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + parent_pool = AgentDrive(registry=registry) + + import agentdrive.drive.drive as pool_module + + monkeypatch.setattr(pool_module, "get_default_drive", lambda: parent_pool) + + result = """ +```agentdrive-skill +name: worker-synthesis-handoff +description: Synthesize multiple worker reports into one parent-ready decision. +tags: [handoff, synthesis] +--- +# Worker Synthesis Handoff + +1. List each worker's strongest claim. +2. Mark contradictions and missing evidence. +3. Return one parent-ready decision with follow-up owners. +``` +""" + + manifest = write_subagent_result_manifest( + swarm_id="swarm-A", + subagent_id="worker-9", + task="summarize parallel worker findings", + result=result, + duration_s=3.5, + ) + assert manifest is not None + assert manifest.skills_created[0].name == "worker-synthesis-handoff" + + received: list[InheritanceReceived] = [] + token = subscribe(received.append, [InheritanceReceived]) + try: + emit( + SubagentDone( + subagent_id="worker-9", + swarm_id="swarm-A", + ok=True, + duration_s=3.5, + ) + ) + finally: + unsubscribe(token) + + installed = get_skill("worker-synthesis-handoff") + assert installed is not None + assert installed.source == "inheritance:swarm-A:worker-9" + assert "parent-ready decision" in installed.body + assert received[0].skills_absorbed == ["worker-synthesis-handoff"] + + usage = get_skill_usage("worker-synthesis-handoff") + assert usage.runs == 1 + assert usage.successes == 1 + assert usage.failures == 0 + assert usage.sources["inheritance:swarm-A:worker-9"] == 1 + + +def test_external_inherited_skills_are_not_installed_without_review( + registry: GenomeRegistry, + clean_bus: None, + isolated_agentdrive_home: Path, +) -> None: + parent_pool = AgentDrive(registry=registry) + manifest = _build_manifest("federation-X", "peer-sub-1", []) + manifest.skills_created.append(_skill_candidate("peer-dangerous-playbook")) + + result = record_manifest( + manifest, + target_pool=parent_pool, + auto_absorb=True, + quarantine_external=True, + ) + + assert result.skills_absorbed == [] + assert result.skills_rejected == ["peer-dangerous-playbook"] + assert ( + "external inherited skills require review" + in result.reason_per_rejected["skill:peer-dangerous-playbook"] + ) + assert get_skill("peer-dangerous-playbook") is None + + +def test_oversized_inherited_skills_are_rejected( + registry: GenomeRegistry, + clean_bus: None, + isolated_agentdrive_home: Path, +) -> None: + parent_pool = AgentDrive(registry=registry) + manifest = _build_manifest("swarm-A", "analyst-7", []) + candidate = _skill_candidate("too-large-playbook") + candidate.body = "# Too Large\n\n" + ("repeat this step\n" * 1200) + manifest.skills_created.append(candidate) + + result = record_manifest( + manifest, + target_pool=parent_pool, + auto_absorb=True, + ) + + assert result.skills_absorbed == [] + assert result.skills_rejected == ["too-large-playbook"] + assert ( + "Inherited skill body must be <=" in result.reason_per_rejected["skill:too-large-playbook"] + ) + assert get_skill("too-large-playbook") is None + + def test_record_manifest_with_auto_absorb_ingests_new_genomes( registry: GenomeRegistry, clean_bus: None ) -> None: @@ -249,6 +504,107 @@ def get_or_create_pool(self, _swarm: str, _sub: str) -> AgentDrive: assert "ferried-home" in received[0].genomes_absorbed +def test_subagent_done_auto_assimilates_proven_inherited_skill( + registry: GenomeRegistry, + clean_bus: None, + inheritance_bus_subscribed: None, + isolated_agentdrive_home: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + parent_pool = AgentDrive(registry=registry) + + import agentdrive.drive.drive as pool_module + + monkeypatch.setattr(pool_module, "get_default_drive", lambda: parent_pool) + + first = _build_manifest("swarm-A", "teacher-1", []) + first.skills_created.append(_skill_candidate("auto-assimilate-playbook")) + record_manifest( + first, + target_pool=parent_pool, + auto_absorb=True, + skill_outcome_success=True, + ) + + second = _build_manifest("swarm-A", "teacher-2", []) + improved = _skill_candidate("auto-assimilate-playbook") + improved.body = ( + "# Auto Assimilate Playbook\n\n" + "1. Gather initial evidence.\n" + "2. Add second-worker contradiction checks.\n" + "3. Record reusable prevention DNA." + ) + second.skills_created.append(improved) + p = manifest_path("swarm-A", "teacher-2") + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(second.to_json(), encoding="utf-8") + + assimilated: list[SkillAssimilated] = [] + token = subscribe(assimilated.append, [SkillAssimilated]) + try: + emit(SubagentDone(subagent_id="teacher-2", swarm_id="swarm-A", ok=True, duration_s=4.0)) + finally: + unsubscribe(token) + + installed = get_skill("auto-assimilate-playbook") + assert installed is not None + assert installed.category == "promoted" + assert "second-worker contradiction checks" in installed.body + + assert len(assimilated) == 1 + assert assimilated[0].promoted_skills == ["auto-assimilate-playbook"] + assert assimilated[0].dna_genomes == ["skill-auto-assimilate-playbook@1.0.1"] + + genome = registry.load("skill-auto-assimilate-playbook") + assert genome is not None + assert genome.genome_id == "skill-auto-assimilate-playbook@1.0.1" + assert genome.framework is not None + assert genome.framework["inheritance"]["revision_count"] == 2 + + +def test_subagent_done_auto_assimilation_can_be_disabled( + registry: GenomeRegistry, + clean_bus: None, + inheritance_bus_subscribed: None, + isolated_agentdrive_home: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + parent_pool = AgentDrive(registry=registry) + + import agentdrive.drive.drive as pool_module + + monkeypatch.setattr(pool_module, "get_default_drive", lambda: parent_pool) + monkeypatch.setenv("AGENTDRIVE_AUTO_ASSIMILATE_SKILLS", "0") + + first = _build_manifest("swarm-A", "teacher-1", []) + first.skills_created.append(_skill_candidate("manual-assimilate-playbook")) + record_manifest( + first, + target_pool=parent_pool, + auto_absorb=True, + skill_outcome_success=True, + ) + + second = _build_manifest("swarm-A", "teacher-2", []) + second.skills_created.append(_skill_candidate("manual-assimilate-playbook")) + p = manifest_path("swarm-A", "teacher-2") + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(second.to_json(), encoding="utf-8") + + assimilated: list[SkillAssimilated] = [] + token = subscribe(assimilated.append, [SkillAssimilated]) + try: + emit(SubagentDone(subagent_id="teacher-2", swarm_id="swarm-A", ok=True, duration_s=4.0)) + finally: + unsubscribe(token) + + installed = get_skill("manual-assimilate-playbook") + assert installed is not None + assert installed.category == "inherited" + assert assimilated == [] + assert registry.load("skill-manual-assimilate-playbook") is None + + def test_pool_ingest_source_marks_inheritance_origin( registry: GenomeRegistry, clean_bus: None ) -> None: diff --git a/tests/test_learnings.py b/tests/test_learnings.py index 93da46a..005483a 100644 --- a/tests/test_learnings.py +++ b/tests/test_learnings.py @@ -38,7 +38,9 @@ def test_learnings_dir_uses_isolated_home(isolated_agentdrive_home: Path) -> Non def test_log_search_count_and_dedup(isolated_agentdrive_home: Path) -> None: store = LearningsStore(slug="test-project") - store.log(_sample_entry(key="foo", insight="foo related guidance", ts="2026-06-01T00:00:00+00:00")) + store.log( + _sample_entry(key="foo", insight="foo related guidance", ts="2026-06-01T00:00:00+00:00") + ) store.log( _sample_entry( key="foo", @@ -146,4 +148,4 @@ def test_harness_record_learning_and_compose_context(isolated_agentdrive_home: P assert "Base task prompt" in composed assert "Project learnings:" in composed assert "[pytest-isolation]" in composed - assert "isolated_agentdrive_home" in composed \ No newline at end of file + assert "isolated_agentdrive_home" in composed diff --git a/tests/test_mcp_config.py b/tests/test_mcp_config.py index 939c205..fc07759 100644 --- a/tests/test_mcp_config.py +++ b/tests/test_mcp_config.py @@ -4,6 +4,8 @@ import json import shutil +import sys +from datetime import timedelta import pytest @@ -19,13 +21,16 @@ def test_mcp_package_available_when_installed(): + if not mcp_package_available(): + pytest.skip("mcp not installed (pip install -e '.[test]')") assert mcp_package_available() is True def test_resolve_mcp_launcher_returns_stdio_args(): launcher = resolve_mcp_launcher() assert launcher.command - assert launcher.args[:2] == ["--transport", "stdio"] + transport_index = launcher.args.index("--transport") + assert launcher.args[transport_index + 1] == "stdio" assert launcher.method in ("binary", "module", "uvx") @@ -44,6 +49,8 @@ def test_get_mcp_server_block_matches_launcher(): def test_run_mcp_doctor_passes_in_dev_env(): + if not mcp_package_available(): + pytest.skip("mcp not installed (pip install -e '.[test]')") report = run_mcp_doctor() assert report.get("tool_count", 0) >= 25 assert any(c["name"] == "mcp package" and c["ok"] for c in report.get("checks", [])) @@ -55,6 +62,41 @@ def test_count_mcp_tools_at_least_registry_ops(): assert count_mcp_tools() >= 25 +def test_stdio_mcp_client_initializes_and_lists_tools(tmp_path): + pytest.importorskip("mcp") + import anyio + from mcp import ClientSession + from mcp.client.stdio import StdioServerParameters, stdio_client + + async def smoke() -> None: + server = StdioServerParameters( + command=sys.executable, + args=["-m", "agentdrive.adapters.mcp_server", "--transport", "stdio"], + env={ + "AGENTDRIVE_HOME": str(tmp_path / ".agentdrive"), + "PYTHONUNBUFFERED": "1", + }, + ) + async with stdio_client(server) as (read, write): + async with ClientSession( + read, + write, + read_timeout_seconds=timedelta(seconds=5), + ) as session: + await session.initialize() + tools = await session.list_tools() + names = {tool.name for tool in tools.tools} + assert "agentdrive_mcp_catalog" in names + assert "experience_graph_get_context_pack" in names + assert "agentdrive_review_inherited_skills" in names + assert "agentdrive_assimilate_inherited_skills" in names + assert "agentdrive_promote_inherited_skill" in names + assert "agentdrive_prune_inherited_skill" in names + assert "agentdrive_ingest_skill_dna" in names + + anyio.run(smoke) + + def test_write_client_config_dry_run_cursor(tmp_path, monkeypatch: pytest.MonkeyPatch): monkeypatch.setenv("HOME", str(tmp_path)) result = write_client_config("cursor", dry_run=True) @@ -77,4 +119,4 @@ def test_write_client_config_creates_cursor_json(tmp_path, monkeypatch: pytest.M def test_binary_launcher_when_on_path(): launcher = resolve_mcp_launcher() if shutil.which("agentdrive-mcp"): - assert launcher.method == "binary" \ No newline at end of file + assert launcher.method == "binary" diff --git a/tests/test_mcp_think.py b/tests/test_mcp_think.py index 6794d2c..3e896ba 100644 --- a/tests/test_mcp_think.py +++ b/tests/test_mcp_think.py @@ -85,9 +85,7 @@ def test_ensure_mandatory_gaps_injects_when_empty(): def test_ensure_mandatory_gaps_preserves_existing_gaps(): question = "How do we handle secret rotation?" - payload = _sample_result( - gaps=[Gap("Existing honest gap", "low", "noop")] - ).to_mcp_dict() + payload = _sample_result(gaps=[Gap("Existing honest gap", "low", "noop")]).to_mcp_dict() enriched = _ensure_mandatory_gaps(payload, question) @@ -138,4 +136,4 @@ def test_agentdrive_think_tool_returns_mandatory_gaps(mock_get_pool): assert payload["correlation_id"] assert len(payload["gaps"]) >= 1 assert payload["gaps"][0]["severity"] == "high" - assert "Insufficient evidence in drive for full answer on:" in payload["gaps"][0]["description"] \ No newline at end of file + assert "Insufficient evidence in drive for full answer on:" in payload["gaps"][0]["description"] diff --git a/tests/test_memory_bank.py b/tests/test_memory_bank.py new file mode 100644 index 0000000..89c006c --- /dev/null +++ b/tests/test_memory_bank.py @@ -0,0 +1,121 @@ +"""Tests for AgentDrive Memory Bank.""" + +from __future__ import annotations + +import pytest + +from agentdrive.learning.auto_absorb import reset_sessions +from agentdrive.memory import MemoryBankStore, build_deep_briefing, build_memory_briefing +from agentdrive.operations.registry import run_operation + + +@pytest.fixture(autouse=True) +def _clean_sessions(): + reset_sessions() + yield + reset_sessions() + + +@pytest.fixture +def swarm_id(isolated_agentdrive_home): + return "stabilization-wave-20260531" + + +def test_memory_bank_store_and_recall(swarm_id) -> None: + store = MemoryBankStore(swarm_id) + entry = store.store( + kind="fact", + title="Gateway auth header", + content="Interegy uses X-Ren-API-Key for bootstrap, not X-Ren-Bootstrap-Key.", + confidence=0.9, + source="user", + tags=["interegy", "auth"], + ) + assert entry.memory_id.startswith("mem-") + recalled = store.recall(entry.memory_id) + assert recalled is not None + assert recalled.title == "Gateway auth header" + + +def test_memory_bank_search(swarm_id) -> None: + store = MemoryBankStore(swarm_id) + store.store(kind="procedure", title="Deploy flow", content="Fund xAI credits before VPS deploy") + store.store(kind="fact", title="Unrelated", content="Something else entirely") + hits = store.search("xAI VPS deploy", limit=5) + assert hits + assert any("Deploy" in h.title or "xAI" in h.content for h in hits) + + +def test_memory_bank_briefing(swarm_id) -> None: + store = MemoryBankStore(swarm_id) + store.store( + kind="insight", title="Test insight", content="Memory bank grows with every session." + ) + pack = build_memory_briefing(swarm_id, limit=5) + assert pack["memory_count"] >= 1 + assert "Memory Bank" in pack["briefing"] + assert pack["integrated_layers"] + + +def test_memory_bank_store_operation(swarm_id) -> None: + result = run_operation( + "memory_bank_store", + kind="preference", + title="User prefers Drive terminology", + content="Use Drive in docs; Pool is internal engine module name.", + swarm_id=swarm_id, + ) + assert result.get("success") is True + memory = result.get("memory") or {} + assert memory.get("memory_id") + + +def test_auto_ingest_from_external_parent(swarm_id) -> None: + branches = [ + { + "branch_id": "branch:op-1", + "role": "operator", + "path_summary": "Fund credits first", + "robustness_score": 0.9, + } + ] + result = run_operation( + "external_parent_decision", + trigger="Memory bank auto ingest test", + branches=branches, + collapsed_branch_id="branch:op-1", + swarm_id=swarm_id, + reasoning_provider="test", + ) + assert result.get("success") is True + auto = result.get("auto_learning") or {} + mem = auto.get("memory") + if mem: + assert mem.get("memory_id") + store = MemoryBankStore(swarm_id) + assert store.count() >= 1 + + +def test_deep_briefing_includes_graph_and_memory(swarm_id) -> None: + MemoryBankStore(swarm_id).store( + kind="fact", + title="Deep briefing test", + content="Unified structural + personal memory.", + ) + pack = build_deep_briefing(swarm_id, memory_limit=5, max_tokens=500) + assert "fabric_context_pack" in pack + assert "memory_bank" in pack + assert "deep_briefing" in pack + assert pack["memory_count"] >= 1 + + +def test_learnings_log_writes_memory(swarm_id) -> None: + result = run_operation( + "learnings_log", + key="memory-bank-test", + insight="Learnings also flow into the deep memory bank.", + type="operational", + swarm_id=swarm_id, + ) + assert result.get("success") is True + assert result.get("memory") or MemoryBankStore(swarm_id).count() >= 1 diff --git a/tests/test_memory_triage.py b/tests/test_memory_triage.py new file mode 100644 index 0000000..fa65004 --- /dev/null +++ b/tests/test_memory_triage.py @@ -0,0 +1,117 @@ +"""Tests for human-inspired memory triage primitives.""" + +from __future__ import annotations + +from agentdrive.memory import ( + MemoryTraceCandidate, + build_memory_control_plan, + forgetting_curve_strength, + triage_memory_candidates, +) + + +def test_forgetting_curve_decays_and_rehearsal_helps() -> None: + fresh = forgetting_curve_strength(0) + old = forgetting_curve_strength(14) + rehearsed_old = forgetting_curve_strength(14, rehearsal_count=5) + + assert fresh == 1.0 + assert old < fresh + assert rehearsed_old > old + + +def test_triage_routes_high_relevance_to_working_set() -> None: + summary = triage_memory_candidates( + [ + MemoryTraceCandidate( + item_id="active-brief", + source="test", + salience=0.9, + retrieval_relevance=0.95, + trust=0.9, + novelty=0.3, + ) + ] + ) + + assert summary["queues"]["working_set"][0]["item_id"] == "active-brief" + assert summary["counts"]["working_set"] == 1 + + +def test_triage_routes_conflict_to_reconsolidation() -> None: + summary = triage_memory_candidates( + [ + MemoryTraceCandidate( + item_id="unstable-memory", + source="test", + salience=0.8, + retrieval_relevance=0.8, + coherence=0.2, + contradiction_pressure=0.9, + ) + ] + ) + + assert summary["queues"]["reconsolidate"][0]["item_id"] == "unstable-memory" + assert summary["counts"]["reconsolidate"] == 1 + + +def test_triage_routes_novel_salient_low_depth_item_to_consolidation() -> None: + summary = triage_memory_candidates( + [ + MemoryTraceCandidate( + item_id="new-pattern", + source="test", + age_days=21, + salience=0.95, + retrieval_relevance=0.0, + coherence=0.75, + trust=0.9, + novelty=0.9, + consolidation_depth=0.0, + ) + ] + ) + + assert summary["queues"]["consolidate"][0]["item_id"] == "new-pattern" + assert summary["counts"]["consolidate"] == 1 + + +def test_triage_caps_each_route_and_preserves_schema() -> None: + candidates = [ + MemoryTraceCandidate( + item_id=f"active-{i}", + source="test", + salience=0.9, + retrieval_relevance=0.95, + trust=0.9, + ) + for i in range(5) + ] + + summary = triage_memory_candidates(candidates, per_route_limit=2) + + assert summary["model"] == "human-inspired-memory-triage-v1" + assert set(summary["queues"]) == { + "working_set", + "consolidate", + "reconsolidate", + "archive", + } + assert summary["counts"]["working_set"] == 2 + assert summary["control_plan"]["next_focus"] == "reason_over_working_set" + + +def test_control_plan_is_stable_for_empty_queues() -> None: + queues = {"working_set": [], "consolidate": [], "reconsolidate": [], "archive": []} + + plan = build_memory_control_plan(queues) + + assert plan["next_focus"] == "no_active_memory_work" + assert plan["primary_context_order"] == [ + "working_set", + "reconsolidate", + "consolidate", + "archive", + ] + assert [step["route"] for step in plan["steps"]] == plan["primary_context_order"] diff --git a/tests/test_memory_vault.py b/tests/test_memory_vault.py new file mode 100644 index 0000000..1b6c685 --- /dev/null +++ b/tests/test_memory_vault.py @@ -0,0 +1,159 @@ +"""Tests for AgentDrive Memory Bank vault/topic/anchor/relation/dialogue APIs.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from agentdrive.memory import ( + MemoryBankStore, + MemoryRelationGraph, + build_session_anchor, + import_dialogue_file, + lexical_bm25_scores, + rank_memory_candidates, + resolve_topic, +) +from agentdrive.memory.store import MemoryEntry +from agentdrive.operations.registry import run_operation + + +@pytest.fixture +def swarm_id(isolated_agentdrive_home): + return "memory-vault-test-swarm" + + +def test_memory_entry_ignores_legacy_fields() -> None: + entry = MemoryEntry.from_dict( + { + "memory_id": "mem-1", + "kind": "episode", + "title": "Legacy", + "content": "body", + "wing": "old-vault", + "room": "old-topic", + "source_file": "/tmp/session.jsonl", + "chunk_index": 2, + "verbatim": False, + } + ) + assert entry.vault == "" + assert entry.topic == "" + assert entry.origin_path == "" + assert entry.shard_index is None + assert entry.preserves_source is True + + +def test_search_scoped_by_vault_and_topic(swarm_id) -> None: + store = MemoryBankStore(swarm_id) + store.store( + kind="fact", + title="Interegy auth", + content="Uses X-Ren-API-Key header", + vault="interegy", + topic="auth", + ) + store.store( + kind="fact", + title="Other project", + content="Unrelated deployment notes", + vault="other", + topic="deploy", + ) + hits = store.search("auth header", vault="interegy", limit=5) + assert hits + assert all(hit.vault in ("interegy", "") for hit in hits) + + +def test_ranking_prefers_matching_documents() -> None: + candidates = [ + {"text": "gateway bootstrap key", "signal_score": 1.0}, + {"text": "unrelated gardening tips", "signal_score": 1.0}, + ] + ranked = rank_memory_candidates(candidates, "gateway bootstrap") + assert ranked[0][1]["text"].startswith("gateway") + + +def test_lexical_bm25_scores_nonzero_for_overlap() -> None: + scores = lexical_bm25_scores("deploy gateway", ["deploy the gateway service", "cats and dogs"]) + assert scores[0] > scores[1] + + +def test_resolve_topic_from_tags() -> None: + assert resolve_topic("episode", ["auto-ingest", "claude"]) == "claude" + assert resolve_topic("fact", []) == "fact" + + +def test_build_session_anchor_includes_tiers(swarm_id, isolated_agentdrive_home) -> None: + identity = isolated_agentdrive_home / "identity.txt" + identity.write_text("## Agent\nTest operator", encoding="utf-8") + MemoryBankStore(swarm_id).store( + kind="insight", + title="Anchor memory", + content="Essential project context for session start.", + confidence=0.95, + vault="test-vault", + ) + pack = build_session_anchor(swarm_id, vault="test-vault", query="project context") + assert "Test operator" in pack["anchor_text"] + assert "Essential memories" in pack["anchor_text"] + assert pack["tiers"]["agent_brief"] + assert pack["tiers"]["essential"] + assert pack["token_estimate"] > 0 + + +def test_memory_bank_anchor_operation(swarm_id) -> None: + result = run_operation("memory_bank_anchor", swarm_id=swarm_id) + assert result.get("success") is True + assert "anchor_text" in result + assert result.get("operation") == "memory_bank_anchor" + + +def test_import_dialogue_file_jsonl(swarm_id, tmp_path: Path) -> None: + dialogue = tmp_path / "session.jsonl" + dialogue.write_text( + json.dumps({"role": "user", "content": "Remember to fund xAI credits before deploy."}) + + "\n", + encoding="utf-8", + ) + result = import_dialogue_file(dialogue, swarm_id=swarm_id, vault="test-dialogues") + assert result["imported"] >= 1 + store = MemoryBankStore(swarm_id) + hits = store.search("xAI credits", vault="test-dialogues") + assert hits + assert hits[0].preserves_source is True + assert hits[0].origin_path == str(dialogue.resolve()) + + +def test_memory_relation_record_and_query(swarm_id) -> None: + graph = MemoryRelationGraph(swarm_id) + relation = graph.record("Interegy", "uses_auth", "X-Ren-API-Key") + assert relation.relation_id.startswith("rel-") + hits = graph.query("Interegy") + assert len(hits) == 1 + assert hits[0].predicate == "uses_auth" + + +def test_memory_relation_expire_operation(swarm_id) -> None: + run_operation( + "memory_relation_record", + subject="Gateway", + predicate="requires", + object="credits", + swarm_id=swarm_id, + ) + result = run_operation( + "memory_relation_expire", + subject="Gateway", + predicate="requires", + object="credits", + swarm_id=swarm_id, + ) + assert result.get("success") is True + assert result.get("updated") == 1 + query = run_operation("memory_relation_query", entity="Gateway", swarm_id=swarm_id) + assert query.get("success") is True + relations = query.get("relations") or [] + assert relations and relations[0].get("valid_to") diff --git a/tests/test_message_stream_lane.py b/tests/test_message_stream_lane.py index 50bd856..98038ae 100644 --- a/tests/test_message_stream_lane.py +++ b/tests/test_message_stream_lane.py @@ -42,4 +42,4 @@ def test_message_stream_lane_reset(): emit(MessageDelta(text="after")) assert lane.text() == "after" finally: - lane.detach() \ No newline at end of file + lane.detach() diff --git a/tests/test_mirror_neurons.py b/tests/test_mirror_neurons.py new file mode 100644 index 0000000..fb07594 --- /dev/null +++ b/tests/test_mirror_neurons.py @@ -0,0 +1,96 @@ +"""Tests for mirror-neuron mimicry layer.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from agentdrive.codebase.exemplars import extract_exemplars +from agentdrive.codebase.mirrors import ( + fire_mirrors_for_intent, + global_mirror_field, + transform_toward_style, +) +from agentdrive.codebase.observe import observe_file +from agentdrive.codebase.registry import register_project +from agentdrive.operations.registry import run_operation + + +@pytest.fixture +def py_project(tmp_path: Path) -> str: + root = tmp_path / "mirror-demo" + (root / "api").mkdir(parents=True) + (root / "api" / "handler.py").write_text( + '"""API handlers."""\n\n' + "import logging\n\n" + "logger = logging.getLogger(__name__)\n\n" + "def fetch_user_record(user_id: str) -> dict:\n" + ' """Load user."""\n' + " return {'id': user_id}\n\n" + "def save_user_record(user_id: str, data: dict) -> None:\n" + " logger.info('save %s', user_id)\n", + encoding="utf-8", + ) + register_project(project_id="mirror-demo", root=str(root)) + return "mirror-demo" + + +def test_extract_exemplars_python() -> None: + content = "def alpha_one(x: int) -> int:\n return x\n" + ex = extract_exemplars(path="a.py", content=content, language="python") + assert any(e["name"] == "alpha_one" for e in ex) + + +def test_mirror_ingest_fires_motors(py_project: str) -> None: + result = observe_file(project_id=py_project, path="api/handler.py") + assert result["success"] is True + mirror = result.get("mirror_neurons") or {} + assert mirror.get("motors_fired", 0) >= 1 + + +def test_fire_mirrors_for_intent(py_project: str) -> None: + observe_file(project_id=py_project, path="api/handler.py") + fired = fire_mirrors_for_intent(py_project, intent="fetch user record helper") + assert fired.get("motors_fired", 0) >= 1 + assert "mimicry_prompt" in fired + assert "fetch_user_record" in fired.get("mimicry_prompt", "") or fired.get("motor_programs") + + +def test_transform_camel_to_snake(py_project: str) -> None: + observe_file(project_id=py_project, path="api/handler.py") + out = transform_toward_style( + py_project, + code="def loadUserData():\n pass\n", + path="api/new.py", + ) + assert "load_user_data" in out.get("transformed_code", "") or out.get("suggestions") + + +def test_codebase_mimic_operation(py_project: str, isolated_agentdrive_home: Path) -> None: + observe_file(project_id=py_project, path="api/handler.py") + result = run_operation( + "codebase_mimic", + project_id=py_project, + intent="save user helper", + ) + assert result.get("success") is True + assert result.get("mimicry_prompt") + assert result.get("auto_learning") + + +def test_global_resonance_after_two_projects( + tmp_path: Path, isolated_agentdrive_home: Path +) -> None: + for name in ("proj-a", "proj-b"): + root = tmp_path / name + root.mkdir() + (root / "mod.py").write_text( + "def do_work(item_id: str) -> str:\n return item_id\n", + encoding="utf-8", + ) + register_project(project_id=name, root=str(root)) + observe_file(project_id=name, path="mod.py") + + field = global_mirror_field() + assert field.get("projects_registered", 0) >= 2 diff --git a/tests/test_mission_control_authz.py b/tests/test_mission_control_authz.py index c081544..1ceebd3 100644 --- a/tests/test_mission_control_authz.py +++ b/tests/test_mission_control_authz.py @@ -85,4 +85,4 @@ def test_dispatch_command_operator_bypass_for_smoke( duration_seconds=1.0, operator_bypass=True, ) - assert result.get("error") != "cap_denied" \ No newline at end of file + assert result.get("error") != "cap_denied" diff --git a/tests/test_multiverse_engine.py b/tests/test_multiverse_engine.py new file mode 100644 index 0000000..5360c27 --- /dev/null +++ b/tests/test_multiverse_engine.py @@ -0,0 +1,138 @@ +"""Tests for MultiverseEngine core pipeline (beyond external_parent path).""" + +from __future__ import annotations + +import pytest + +from agentdrive.cognition.multiverse import ( + CollapsePolicy, + MultiverseEngine, + SessionStatus, +) +from agentdrive.evolution.experience_graph import ExperienceGraphRecorder +from agentdrive.operations.registry import run_operation + + +@pytest.fixture +def swarm_id(isolated_agentdrive_home): + return "stabilization-wave-20260531" + + +@pytest.fixture +def recorder(isolated_agentdrive_home, swarm_id): + from agentdrive.drive.drive import get_swarm_drive_path + + drive_path = get_swarm_drive_path(swarm_id) + return ExperienceGraphRecorder(drive_path=drive_path) + + +@pytest.fixture +def engine(recorder): + return MultiverseEngine( + recorder, + program_id="multiverse-test@stabilization-wave-20260531", + use_llm=False, + ) + + +def test_spawn_session_creates_branches(engine) -> None: + session = engine.spawn_session("Ship feature X", n_branches=5) + assert session.session_id.startswith("multiverse-session:") + assert len(session.branches) == 5 + assert session.status == SessionStatus.OPEN + roles = {b.role for b in session.branches} + assert roles # heuristic spawner assigns cognitive roles + + +def test_run_full_collapses_session(engine) -> None: + session = engine.run_full( + "Consolidation sprint: archive stale docs", + n_branches=4, + forward_steps=2, + stress_test_top_n=1, + densify_invariants=False, + ) + assert session.status == SessionStatus.COLLAPSED + assert session.collapsed_branch_id + assert session.collapse_policy is not None + assert session.invariants # extract_invariants runs in run_full + + +def test_simulate_and_extract_invariants(engine) -> None: + session = engine.spawn_session("Probe invariant extraction", n_branches=3) + engine.simulate_branches(session.session_id, forward_steps=2) + updated = engine.extract_invariants(session.session_id) + assert updated.invariants + assert updated.convergence_points is not None + assert updated.divergence_points is not None + + +def test_collapse_selects_branch(engine) -> None: + session = engine.spawn_session("Manual collapse test", n_branches=3) + target = session.branches[0].branch_id + collapsed = engine.collapse( + session.session_id, + branch_id=target, + policy=CollapsePolicy.CONDUCTOR_OVERRIDE, + reason="Test override", + ) + assert collapsed.status == SessionStatus.COLLAPSED + assert collapsed.collapsed_branch_id == target + assert collapsed.collapse_policy == CollapsePolicy.CONDUCTOR_OVERRIDE + + +def test_list_and_get_session_round_trip(engine) -> None: + spawned = engine.spawn_session("List/get round trip", n_branches=2) + listed = engine.list_sessions(limit=5) + assert any(s.session_id == spawned.session_id for s in listed) + loaded = engine.get_session(spawned.session_id) + assert loaded is not None + assert loaded.trigger == spawned.trigger + + +def test_multiverse_run_full_operation(swarm_id) -> None: + result = run_operation( + "multiverse_run_full", + trigger="Ops registry multiverse smoke", + n_branches=3, + forward_steps=2, + swarm_id=swarm_id, + ) + assert result.get("success") is True + session = result.get("session") or {} + assert session.get("session_id") + assert session.get("collapsed_branch_id") + + +def test_multiverse_parent_decision_heuristic(swarm_id) -> None: + result = run_operation( + "multiverse_parent_decision", + trigger="Integrated parent decision smoke", + n_branches=3, + heuristic_only=True, + skip_densify=True, + swarm_id=swarm_id, + ) + assert result.get("success") is True + payload = result.get("result") or {} + assert payload.get("session_id") + assert payload.get("collapsed_branch_id") + + +def test_multiverse_list_sessions_operation(swarm_id) -> None: + run_operation( + "multiverse_parent_decision", + trigger="Seed session for list test", + n_branches=2, + heuristic_only=True, + skip_densify=True, + swarm_id=swarm_id, + ) + listed = run_operation("multiverse_list_sessions", swarm_id=swarm_id, limit=5) + assert listed.get("success") is True + sessions = listed.get("sessions") or [] + assert len(sessions) >= 1 + + +def test_resolve_llm_mode_heuristic_when_disabled(engine) -> None: + assert engine.resolve_llm_mode("any trigger") == "heuristic" diff --git a/tests/test_operations_mcp_bridge.py b/tests/test_operations_mcp_bridge.py index 6df5fa5..dc6bcda 100644 --- a/tests/test_operations_mcp_bridge.py +++ b/tests/test_operations_mcp_bridge.py @@ -12,11 +12,7 @@ @pytest.fixture def mcp_server(): - mcp = pytest.importorskip("mcp") - try: - from mcp.server.fastmcp import FastMCP - except ImportError: - pytest.skip("mcp package not installed") + pytest.importorskip("mcp.server.fastmcp") from agentdrive.adapters.mcp_server import create_mcp_server return create_mcp_server() @@ -34,6 +30,11 @@ def test_mcp_server_registers_auto_ops(mcp_server) -> None: assert "agentdrive_doctor" in tools assert "agentdrive_dream_run" in tools assert "agentdrive_harness_compose" in tools + assert "agentdrive_review_inherited_skills" in tools + assert "agentdrive_assimilate_inherited_skills" in tools + assert "agentdrive_promote_inherited_skill" in tools + assert "agentdrive_prune_inherited_skill" in tools + assert "agentdrive_ingest_skill_dna" in tools def test_auto_registered_doctor_dry_run_via_run_operation(isolated_agentdrive_home) -> None: @@ -43,11 +44,11 @@ def test_auto_registered_doctor_dry_run_via_run_operation(isolated_agentdrive_ho def test_register_skips_existing_names() -> None: - from mcp.server.fastmcp import FastMCP + fastmcp = pytest.importorskip("mcp.server.fastmcp") - server = FastMCP("test-skip") + server = fastmcp.FastMCP("test-skip") server.add_tool(lambda: "ok", name="agentdrive_doctor", description="manual") before = set(server._tool_manager._tools.keys()) # noqa: SLF001 registered = register_operations_as_mcp_tools(server, skip_names=before) assert "agentdrive_doctor" not in registered - assert "agentdrive_patterns_list" in registered \ No newline at end of file + assert "agentdrive_patterns_list" in registered diff --git a/tests/test_operations_registry.py b/tests/test_operations_registry.py index de8de4e..630aa2e 100644 --- a/tests/test_operations_registry.py +++ b/tests/test_operations_registry.py @@ -95,4 +95,4 @@ def test_operations_include_required_names() -> None: "learnings_log", "harness_compose", } - assert required.issubset(names) \ No newline at end of file + assert required.issubset(names) diff --git a/tests/test_patterns.py b/tests/test_patterns.py index 78164b4..e1f7019 100644 --- a/tests/test_patterns.py +++ b/tests/test_patterns.py @@ -74,4 +74,4 @@ def test_user_overlay_wins_on_name_collision( def test_unknown_pattern_raises() -> None: with pytest.raises(PatternNotFoundError): - resolve_pattern_path("does-not-exist-pattern") \ No newline at end of file + resolve_pattern_path("does-not-exist-pattern") diff --git a/tests/test_pool_lane.py b/tests/test_pool_lane.py index 753efb7..f4f6912 100644 --- a/tests/test_pool_lane.py +++ b/tests/test_pool_lane.py @@ -43,4 +43,4 @@ def test_pool_lane_reset(): lane.reset() assert lane.renderable() is None finally: - lane.detach() \ No newline at end of file + lane.detach() diff --git a/tests/test_session_events.py b/tests/test_session_events.py index e8ba2b7..7cb70df 100644 --- a/tests/test_session_events.py +++ b/tests/test_session_events.py @@ -39,12 +39,7 @@ def clean_bus() -> Iterator[None]: def test_session_events_path(isolated_agentdrive_home: Path) -> None: path = session_events_path("my-agent", "sess-123") assert path == ( - isolated_agentdrive_home - / "agents" - / "my-agent" - / "sessions" - / "sess-123" - / "events.jsonl" + isolated_agentdrive_home / "agents" / "my-agent" / "sessions" / "sess-123" / "events.jsonl" ) @@ -110,9 +105,7 @@ def test_format_event_summary_covers_common_types() -> None: assert "user" in format_event_summary({"type": "MessageStart", "role": "user"}) assert "hello" in format_event_summary({"type": "MessageDelta", "text": "hello"}) assert "bash" in format_event_summary({"type": "ToolStart", "tool": "bash"}) - assert "g1" in format_event_summary( - {"type": "PoolMatch", "genomes": ["g1"], "scores": [0.87]} - ) + assert "g1" in format_event_summary({"type": "PoolMatch", "genomes": ["g1"], "scores": [0.87]}) assert "no DNA" in format_event_summary({"type": "PoolMatch", "genomes": [], "scores": []}) @@ -157,9 +150,7 @@ def test_cli_session_events_and_replay(isolated_agentdrive_home: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True) with path.open("w", encoding="utf-8") as fh: fh.write(json.dumps({"type": "MessageDelta", "text": "cli event"}) + "\n") - fh.write( - json.dumps({"type": "PoolMatch", "genomes": ["g-cli"], "scores": [0.5]}) + "\n" - ) + fh.write(json.dumps({"type": "PoolMatch", "genomes": ["g-cli"], "scores": [0.5]}) + "\n") listed = _run_cli("session", "events", session_id, home=isolated_agentdrive_home) assert listed.returncode == 0 @@ -194,4 +185,4 @@ def test_cli_session_panel(isolated_agentdrive_home: Path) -> None: panel = _run_cli("session", "panel", session_id, home=isolated_agentdrive_home) assert panel.returncode == 0 assert "Session replay" in panel.stdout - assert "StatusUpdate" in panel.stdout \ No newline at end of file + assert "StatusUpdate" in panel.stdout diff --git a/tests/test_shared_swarm_drive.py b/tests/test_shared_swarm_drive.py index c3398ce..fbcfcd9 100644 --- a/tests/test_shared_swarm_drive.py +++ b/tests/test_shared_swarm_drive.py @@ -23,6 +23,7 @@ from datetime import UTC, datetime from pathlib import Path +from agentdrive.adapters.base import create_scoped_pool from agentdrive.constants import get_swarm_drive_path from agentdrive.drive.swarm_manager import SwarmDriveManager from agentdrive.drive.swarm_policy import SwarmDrivePolicy @@ -85,6 +86,16 @@ def test_drive_path_is_the_real_swarm_path(isolated_agentdrive_home: Path) -> No ) +def test_create_scoped_pool_uses_shared_swarm_drive(isolated_agentdrive_home: Path) -> None: + """MCP/adapters create_scoped_pool must land on the same path as SwarmDriveManager.""" + via_adapter = create_scoped_pool(swarm_id="mcp-swarm", subagent_id="worker-1") + expected = isolated_agentdrive_home / "swarms" / "mcp-swarm" / "drive" + assert via_adapter.drive_path == expected + + via_sibling = create_scoped_pool(swarm_id="mcp-swarm", subagent_id="worker-2") + assert via_sibling is via_adapter + + def test_different_swarms_get_different_drives(isolated_agentdrive_home: Path) -> None: mgr = SwarmDriveManager() diff --git a/tests/test_skill_fusion.py b/tests/test_skill_fusion.py new file mode 100644 index 0000000..9772fa8 --- /dev/null +++ b/tests/test_skill_fusion.py @@ -0,0 +1,165 @@ +"""Tests for born-skill fusion (experience + skills + patterns).""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from agentdrive.learning.auto_absorb import reset_sessions +from agentdrive.learning.skill_fusion import ( + FusionLineage, + build_fused_skill_body, + synthesize_from_inputs, +) +from agentdrive.operations.registry import run_operation +from agentdrive.skills import get_skill, install_inherited_skill + + +@pytest.fixture(autouse=True) +def _clean_sessions(): + reset_sessions() + yield + reset_sessions() + + +@pytest.fixture +def swarm_id(isolated_agentdrive_home): + return "stabilization-wave-20260531" + + +def _seed_parent_skill(name: str = "parent-playbook") -> None: + install_inherited_skill( + name=name, + description="Parent skill for fusion test", + body="# Parent\n\n1. Do the thing.\n2. Verify.", + source_subagent_id="test-pawn", + swarm_id="stabilization-wave-20260531", + tags=["test"], + ) + + +def test_fusion_lineage_requires_two_axes() -> None: + lineage = FusionLineage( + trigger="Only experience", + swarm_id="swarm", + program_id="prog", + operations=["think"], + experience_traces=["trace-1"], + ) + assert lineage.axes_present() == {"experience"} + assert not lineage.fusion_ready() + + +def test_fusion_lineage_ready_with_experience_and_skills() -> None: + lineage = FusionLineage( + trigger="Ship feature", + swarm_id="swarm", + program_id="prog", + operations=["think", "external_parent_decision"], + experience_traces=["trace-1"], + source_skills=["auto-think-ship"], + ) + assert lineage.fusion_ready() + desc, body = build_fused_skill_body(lineage) + assert "born skill" in body.lower() or "Born skill" in body + assert "experience" in desc.lower() + assert "Ship feature" in body + + +def test_synthesize_fused_skill_installs_born_skill(swarm_id) -> None: + _seed_parent_skill("fusion-parent") + result = synthesize_from_inputs( + trigger="Gateway fetch helper with graph grounding", + swarm_id=swarm_id, + operations=["think", "codebase_mimic"], + experience_traces=["fabric-trace-abc"], + source_skills=["fusion-parent"], + pattern_projects=["agentdrive"], + ) + assert result.get("born") is True + assert result.get("name", "").startswith("fused-") + entry = get_skill(result["name"]) + assert entry is not None + assert "fused" in entry.tags + lineage_file = Path(result["path"]).parent / "fusion-lineage.json" + assert lineage_file.is_file() + meta = json.loads(lineage_file.read_text(encoding="utf-8")) + assert "fusion-parent" in meta["source_skills"] + + +def test_synthesize_fused_skill_operation(swarm_id) -> None: + _seed_parent_skill("op-parent") + result = run_operation( + "synthesize_fused_skill", + trigger="Fused via registry", + source_skills=["op-parent"], + pattern_projects=["agentdrive"], + operations=["think", "codebase_mimic"], + experience_traces=["trace-op"], + swarm_id=swarm_id, + ) + assert result.get("success") is True + fused = result.get("fused_skill") or {} + assert fused.get("born") is True + assert "experience" in (fused.get("axes") or []) + assert "skills" in fused.get("axes", []) + assert "patterns" in fused.get("axes", []) + + +def test_auto_fusion_after_rich_session(swarm_id, isolated_agentdrive_home, tmp_path) -> None: + """Session with think + codebase + external parent should birth a fused skill.""" + from agentdrive.codebase.observe import observe_file + from agentdrive.codebase.registry import register_project + + project_root = tmp_path / "fusion-repo" + project_root.mkdir() + sample = project_root / "helper.py" + sample.write_text( + "def fetch_data():\n return {}\n", + encoding="utf-8", + ) + register_project(project_id="fusion-test", root=str(project_root)) + observe_file(project_id="fusion-test", path="helper.py") + + _seed_parent_skill("session-parent") + + run_operation( + "experience_graph_context_pack", + swarm_id=swarm_id, + program_id="fusion-session@test", + ) + run_operation( + "codebase_mimic", + project_id="fusion-test", + intent="gateway fetch helper", + swarm_id=swarm_id, + program_id="fusion-session@test", + ) + branches = [ + { + "branch_id": "branch:op-1", + "role": "operator", + "path_summary": "Smallest shippable helper", + "robustness_score": 0.85, + } + ] + result = run_operation( + "external_parent_decision", + trigger="Gateway helper with repo patterns", + branches=branches, + collapsed_branch_id="branch:op-1", + swarm_id=swarm_id, + program_id="fusion-session@test", + skill_name="session-parent", + ) + assert result.get("success") is True + auto = result.get("auto_learning") or {} + fused = auto.get("fused_skill") + if fused: + assert fused.get("born") is True + assert len(fused.get("axes") or []) >= 2 + else: + # Distill path still records session lineage for explicit synthesis + assert auto.get("skill") or auto.get("reasoning_trace") diff --git a/tests/test_skill_naming.py b/tests/test_skill_naming.py new file mode 100644 index 0000000..a2e090d --- /dev/null +++ b/tests/test_skill_naming.py @@ -0,0 +1,62 @@ +"""Tests for descriptive learned/fused skill naming.""" + +from __future__ import annotations + +from agentdrive.learning.skill_naming import ( + fused_skill_name, + learned_skill_name, + learned_skill_title, +) + + +def test_learned_skill_name_with_project_and_intent() -> None: + name = learned_skill_name( + "codebase_mimic", + project_id="openmangos", + intent="wire growth_merge_briefing into context pack", + ) + assert name.startswith("learned-openmangos-mimic-") + assert "growth" in name or "wire" in name + assert "d416228298" not in name + + +def test_learned_skill_name_patterns_only() -> None: + name = learned_skill_name("codebase_patterns_profile", project_id="openmangos") + assert name == "learned-openmangos-patterns" + + +def test_learned_skill_name_decision_with_trigger() -> None: + name = learned_skill_name( + "external_parent_decision", + trigger="Ship gateway helper with graph grounding", + ) + assert name.startswith("learned-parent-decision-") + assert "gateway" in name or "ship" in name + + +def test_fused_skill_name_uses_project_and_axes() -> None: + name = fused_skill_name( + trigger="", + pattern_projects=["openmangos"], + axes=["experience", "patterns", "skills"], + ) + assert name == "fused-openmangos-experience-patterns-skills" + + +def test_fused_skill_name_falls_back_to_trigger() -> None: + name = fused_skill_name( + trigger="Gateway fetch helper", + pattern_projects=[], + axes=["experience", "skills"], + ) + assert name.startswith("fused-gateway-fetch-helper-") + + +def test_learned_skill_title_readable() -> None: + title = learned_skill_title( + "codebase_mimic", + project_id="openmangos", + intent="add growth merge briefing", + ) + assert "openmangos" in title + assert "mimic" in title.lower() diff --git a/tests/test_skills_compose.py b/tests/test_skills_compose.py index e0e3b59..7b81c20 100644 --- a/tests/test_skills_compose.py +++ b/tests/test_skills_compose.py @@ -8,7 +8,13 @@ format_skills_catalog, match_skills_for_turn, ) -from agentdrive.skills.registry import discover_skills, get_skill, list_skills_by_tier +from agentdrive.skills.registry import ( + discover_skills, + get_skill, + install_inherited_skill, + list_skills_by_tier, +) +from agentdrive.skills.usage import get_skill_usage, record_skill_run def test_discover_skills_includes_nested_categories(): @@ -83,4 +89,54 @@ def test_vendor_skills_included_when_harness_set(monkeypatch): harness="grok", ) names = {e.name for e in matched} - assert any(n.startswith("grok-") for n in names) \ No newline at end of file + assert any(n.startswith("grok-") for n in names) + + +def test_matched_inherited_skill_updates_usage_ledger(isolated_agentdrive_home): + install_inherited_skill( + name="zebra-incident-response", + description="Use for quantum-zebra incident response learned by a worker", + body="# Zebra Incident Response\n\n1. Check the quantum-zebra signal.", + source_subagent_id="worker-a", + swarm_id="swarm-usage", + tags=["quantum-zebra", "incident"], + ) + + matched = match_skills_for_turn( + "please run the quantum-zebra incident response", + top_k=1, + ) + + assert matched[0].name == "zebra-incident-response" + usage = get_skill_usage("zebra-incident-response") + assert usage.matches == 1 + assert usage.last_score > 0 + + +def test_successful_inherited_skill_gets_ranking_boost(isolated_agentdrive_home): + shared = { + "description": "Use for aurora-synthesis worker handoff decisions", + "body": "# Aurora Synthesis\n\n1. Merge worker evidence.", + "swarm_id": "swarm-rank", + "tags": ["aurora-synthesis", "handoff"], + } + install_inherited_skill( + name="aaa-unproven-aurora", + source_subagent_id="worker-a", + **shared, + ) + install_inherited_skill( + name="zzz-proven-aurora", + source_subagent_id="worker-b", + **shared, + ) + record_skill_run("zzz-proven-aurora", success=True) + record_skill_run("zzz-proven-aurora", success=True) + + matched = match_skills_for_turn( + "aurora-synthesis worker handoff", + top_k=1, + record_matches=False, + ) + + assert matched[0].name == "zzz-proven-aurora" diff --git a/tests/test_skills_registry.py b/tests/test_skills_registry.py index 93721b9..3452420 100644 --- a/tests/test_skills_registry.py +++ b/tests/test_skills_registry.py @@ -4,8 +4,20 @@ from pathlib import Path +import yaml + +from agentdrive.drive.drive import AgentDrive from agentdrive.skills import get_skill, init_skill, list_skills, run_skill -from agentdrive.skills.registry import list_skills_by_tier +from agentdrive.skills.curation import ( + assimilate_inherited_skills, + ingest_skill_as_dna, + promote_inherited_skill, + prune_inherited_skill, + review_inherited_skills, + skill_to_genome, +) +from agentdrive.skills.registry import install_inherited_skill, list_skills_by_tier +from agentdrive.skills.usage import get_skill_usage, record_skill_run def test_list_skills_includes_bundled_and_vendor_tiers(): @@ -62,6 +74,201 @@ def test_init_skill_scaffold(isolated_agentdrive_home): assert entry is not None +def test_run_skill_records_usage_outcome(isolated_agentdrive_home): + init_skill("usage-demo") + + result = run_skill("usage-demo") + + assert result["success"] is True + usage = get_skill_usage("usage-demo") + assert usage.runs == 1 + assert usage.successes == 1 + assert usage.failures == 0 + + +def test_review_promote_and_prune_inherited_skill(isolated_agentdrive_home): + install_inherited_skill( + name="reviewable-worker-playbook", + description="Reusable worker playbook with enough evidence to promote", + body="# Reviewable Worker Playbook\n\n1. Reuse the worker evidence.", + source_subagent_id="worker-review", + swarm_id="swarm-review", + tags=["worker", "review"], + ) + record_skill_run("reviewable-worker-playbook", success=True) + record_skill_run("reviewable-worker-playbook", success=True) + + reviews = review_inherited_skills(include_promoted=False) + review = next(r for r in reviews if r.name == "reviewable-worker-playbook") + assert review.recommendation == "promote" + + promoted = promote_inherited_skill("reviewable-worker-playbook") + assert promoted.promoted is True + entry = get_skill("reviewable-worker-playbook") + assert entry is not None + assert entry.category == "promoted" + + path = prune_inherited_skill("reviewable-worker-playbook", reason="superseded") + assert path.exists() + assert get_skill("reviewable-worker-playbook") is None + assert "disabled: true" in path.read_text(encoding="utf-8") + + +def test_ingest_promoted_skill_as_dna(registry, isolated_agentdrive_home): + install_inherited_skill( + name="dna-worker-playbook", + description="Reusable worker playbook that should become DNA", + body=( + "# DNA Worker Playbook\n\n" + "1. Gather the worker evidence.\n" + "2. Summarize the reusable decision rule." + ), + source_subagent_id="worker-dna", + swarm_id="swarm-dna", + tags=["worker", "decision"], + ) + install_inherited_skill( + name="dna-worker-playbook", + description="Improved worker playbook that should become DNA", + body=( + "# DNA Worker Playbook\n\n" + "1. Gather the worker evidence.\n" + "2. Add contradiction checks from a second worker.\n" + "3. Summarize the reusable decision rule." + ), + source_subagent_id="worker-dna-2", + swarm_id="swarm-dna", + tags=["worker", "decision", "contradiction"], + update_existing=True, + ) + record_skill_run("dna-worker-playbook", success=True) + promote_inherited_skill("dna-worker-playbook") + entry = get_skill("dna-worker-playbook") + assert entry is not None + + genome = skill_to_genome(entry) + assert genome.manifest.id == "skill-dna-worker-playbook" + assert genome.manifest.version == "1.0.1" + assert genome.framework is not None + assert genome.framework["skill_name"] == "dna-worker-playbook" + assert genome.framework["inheritance"]["revision_count"] == 2 + assert genome.framework["inheritance"]["revisions"][0]["subagent_id"] == "worker-dna" + assert genome.framework["inheritance"]["revisions"][1]["subagent_id"] == "worker-dna-2" + assert "Add contradiction checks" in genome.framework["body"] + assert genome.manifest.applicability["source_subagent_ids"] == [ + "worker-dna", + "worker-dna-2", + ] + assert genome.manifest.evaluation_score["skill_successes"] == 1.0 + assert genome.manifest.evaluation_score["skill_revision_count"] == 2.0 + revision_lineage = [ + item for item in genome.provenance.lineage if item.get("relation") == "skill-revision" + ] + assert [item.get("subagent_id") for item in revision_lineage] == [ + "worker-dna", + "worker-dna-2", + ] + author_ids = {author.id for author in genome.manifest.authors} + assert {"sub:worker-dna", "sub:worker-dna-2"}.issubset(author_ids) + + drive = AgentDrive(registry=registry) + export = ingest_skill_as_dna("dna-worker-playbook", target_drive=drive) + + assert export.accepted is True + assert export.genome_id == "skill-dna-worker-playbook@1.0.1" + details = registry.list_genome_details() + assert any( + d.get("id") == "skill-dna-worker-playbook" and d.get("version") == "1.0.1" for d in details + ) + skill_text = entry.path.read_text(encoding="utf-8") + assert "genome_id: skill-dna-worker-playbook@1.0.1" in skill_text + + +def test_assimilate_promotes_and_ingests_proven_inherited_skills( + registry, + isolated_agentdrive_home, +): + install_inherited_skill( + name="assimilate-worker-playbook", + description="Reusable worker playbook that should be assimilated", + body="# Assimilate Worker Playbook\n\n1. Preserve the useful child-agent procedure.", + source_subagent_id="worker-assimilate", + swarm_id="swarm-assimilate", + tags=["worker", "assimilation"], + ) + install_inherited_skill( + name="watch-worker-playbook", + description="Reusable worker playbook without enough evidence yet", + body="# Watch Worker Playbook\n\n1. Wait for more evidence.", + source_subagent_id="worker-watch", + swarm_id="swarm-assimilate", + tags=["worker", "watch"], + ) + record_skill_run("assimilate-worker-playbook", success=True) + record_skill_run("assimilate-worker-playbook", success=True) + + drive = AgentDrive(registry=registry) + report = assimilate_inherited_skills(target_drive=drive) + + assert report.reviewed >= 2 + assert [item.name for item in report.promoted] == ["assimilate-worker-playbook"] + assert [item.skill_name for item in report.dna_exports] == ["assimilate-worker-playbook"] + assert report.errors == [] + assimilated = get_skill("assimilate-worker-playbook") + watched = get_skill("watch-worker-playbook") + assert assimilated is not None + assert watched is not None + assert assimilated.category == "promoted" + assert watched.category == "inherited" + details = registry.list_genome_details() + assert any(d.get("id") == "skill-assimilate-worker-playbook" for d in details) + + +def test_revised_skill_ingest_creates_new_dna_version(registry, isolated_agentdrive_home): + install_inherited_skill( + name="versioned-worker-playbook", + description="First reusable worker playbook", + body="# Versioned Worker Playbook\n\n1. Gather initial evidence.", + source_subagent_id="worker-v1", + swarm_id="swarm-versioned", + tags=["worker", "versioned"], + ) + record_skill_run("versioned-worker-playbook", success=True) + record_skill_run("versioned-worker-playbook", success=True) + promote_inherited_skill("versioned-worker-playbook") + + drive = AgentDrive(registry=registry) + first_export = ingest_skill_as_dna("versioned-worker-playbook", target_drive=drive) + assert first_export.genome_id == "skill-versioned-worker-playbook@1.0.0" + + install_inherited_skill( + name="versioned-worker-playbook", + description="Improved reusable worker playbook", + body=( + "# Versioned Worker Playbook\n\n" + "1. Gather initial evidence.\n" + "2. Add contradiction checks from a later worker." + ), + source_subagent_id="worker-v2", + swarm_id="swarm-versioned", + tags=["worker", "versioned", "contradiction"], + update_existing=True, + ) + second_export = ingest_skill_as_dna("versioned-worker-playbook", target_drive=drive) + assert second_export.genome_id == "skill-versioned-worker-playbook@1.0.1" + assert registry.get_versions("skill-versioned-worker-playbook") == ["1.0.0", "1.0.1"] + + first = registry.load("skill-versioned-worker-playbook@1.0.0") + latest = registry.load("skill-versioned-worker-playbook") + assert first is not None + assert latest is not None + assert latest.genome_id == "skill-versioned-worker-playbook@1.0.1" + assert first.manifest.content_hash in latest.manifest.supersedes + assert latest.framework is not None + assert latest.framework["inheritance"]["revision_count"] == 2 + assert "later worker" in latest.framework["body"] + + def test_init_skill_refuses_overwrite(isolated_agentdrive_home): init_skill("dup-skill") try: @@ -71,6 +278,41 @@ def test_init_skill_refuses_overwrite(isolated_agentdrive_home): pass +def test_inherited_skill_update_existing_records_revision(isolated_agentdrive_home): + first_path = install_inherited_skill( + name="self-improving-worker", + description="First worker version", + body="# First Worker\n\n1. Gather initial evidence.", + source_subagent_id="worker-a", + swarm_id="swarm-self", + tags=["worker"], + ) + second_path = install_inherited_skill( + name="self-improving-worker", + description="Second worker version", + body="# Second Worker\n\n1. Gather initial evidence.\n2. Add contradiction checks.", + source_subagent_id="worker-b", + swarm_id="swarm-self", + tags=["worker", "contradiction"], + update_existing=True, + ) + + assert second_path == first_path + entry = get_skill("self-improving-worker") + assert entry is not None + assert "Add contradiction checks" in entry.body + assert "contradiction" in entry.tags + + text = first_path.read_text(encoding="utf-8") + raw_meta = text.split("---", 2)[1] + meta = yaml.safe_load(raw_meta) + inheritance = meta["inheritance"] + assert inheritance["revision_count"] == 2 + assert inheritance["latest_source"] == "inheritance:swarm-self:worker-b" + assert inheritance["revisions"][0]["source"] == "inheritance:swarm-self:worker-a" + assert inheritance["revisions"][1]["source"] == "inheritance:swarm-self:worker-b" + + def test_user_skill_overlay(isolated_agentdrive_home): skills_dir = isolated_agentdrive_home / "skills" / "custom-test" skills_dir.mkdir(parents=True) @@ -80,4 +322,4 @@ def test_user_skill_overlay(isolated_agentdrive_home): ) entry = get_skill("custom-test") assert entry is not None - assert entry.path == Path(skills_dir / "SKILL.md") \ No newline at end of file + assert entry.path == Path(skills_dir / "SKILL.md") diff --git a/tests/test_sprint.py b/tests/test_sprint.py index 79d4b43..46c78bd 100644 --- a/tests/test_sprint.py +++ b/tests/test_sprint.py @@ -3,15 +3,15 @@ from __future__ import annotations from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest from agentdrive.harness.harness import Harness from agentdrive.sprint import ( + SHIP_CHAIN, CheckpointPending, CheckpointStore, - SHIP_CHAIN, run_ship_chain, ) from agentdrive.sprint.chain import SprintResult @@ -128,4 +128,4 @@ def test_harness_checkpoint_and_ack(isolated_agentdrive_home: Path) -> None: assert harness.ack_checkpoint(cp_id) is True with pytest.raises(CheckpointPending): - harness.checkpoint("manual_review", "Still blocked until fresh checkpoint acked") \ No newline at end of file + harness.checkpoint("manual_review", "Still blocked until fresh checkpoint acked") diff --git a/tests/test_swarm_lane.py b/tests/test_swarm_lane.py index f82b346..8588984 100644 --- a/tests/test_swarm_lane.py +++ b/tests/test_swarm_lane.py @@ -6,7 +6,6 @@ SubagentDone, SubagentSpawn, SubagentTool, - default_bus, emit, ) from agentdrive.tui.swarm_lane import SwarmActivityLane @@ -89,4 +88,4 @@ def test_swarm_lane_failed_subagent_summary(): assert summary is not None assert "failed" in summary finally: - lane.detach() \ No newline at end of file + lane.detach() diff --git a/tests/test_transcript_lane.py b/tests/test_transcript_lane.py index 98f824e..840366c 100644 --- a/tests/test_transcript_lane.py +++ b/tests/test_transcript_lane.py @@ -61,4 +61,4 @@ def test_transcript_lane_on_line_callback(clean_bus: None) -> None: assert calls == [1] assert lane.line_count == 1 finally: - lane.detach() \ No newline at end of file + lane.detach() diff --git a/tests/test_tui_experience.py b/tests/test_tui_experience.py index 4acd3b5..863f1b6 100644 --- a/tests/test_tui_experience.py +++ b/tests/test_tui_experience.py @@ -88,4 +88,4 @@ def test_session_panel_slash(isolated_agentdrive_home: Path): text = out.getvalue() assert "Session replay" in text assert "PoolMatch" in text or "g-panel" in text - assert session_events_path(agent_id, session_id).exists() \ No newline at end of file + assert session_events_path(agent_id, session_id).exists() diff --git a/tests/test_turn_telemetry.py b/tests/test_turn_telemetry.py index 128ebae..a35edbd 100644 --- a/tests/test_turn_telemetry.py +++ b/tests/test_turn_telemetry.py @@ -78,4 +78,4 @@ def test_stream_lanes_combined_renderable(): assert pool.renderable() is not None finally: swarm.detach() - pool.detach() \ No newline at end of file + pool.detach()