diff --git a/ABOUT.md b/ABOUT.md index b01f909..aeb0d50 100644 --- a/ABOUT.md +++ b/ABOUT.md @@ -1,4 +1,4 @@ -**Hermespace is a functional J-Space workbench for Hermes agents** — FOA desk, dual decode, and an append-only world projection. Not [J-Space](https://github.com/anomalyco/j-space) neural weights. Not a second runtime. A harness-level global workspace inside Hermes that runs **standalone** and is **powered by [HermesCube](https://github.com/PabloTheThinker/hermescube)** when present. +**Hermespace is a functional Access Workspace workbench for Hermes agents** — FOA desk, dual decode, and an append-only world projection. Not [Access Workspace](https://github.com/anomalyco/j-space) neural weights. Not a second runtime. A harness-level global workspace inside Hermes that runs **standalone** and is **powered by [HermesCube](https://github.com/PabloTheThinker/hermescube)** when present. Alongside the desk: skills+MEMORY fabric, neural FOA, autonomy grid, Cube heart/center cable (soft-fail). @@ -38,7 +38,7 @@ Hermespace solves this with two systems: | Project | Relation | |---------|----------| | [ActiveGraph](https://github.com/anomalyco/ActiveGraph) | Event log is the agent, graph is the world — influenced the archive-first design | -| [J-Space](https://github.com/anomalyco/j-space) | Verbal workspace, ~25 concepts, broadcasting hub — inspired the `concepts` system and `_refresh_concepts()` | +| [Access Workspace](https://github.com/anomalyco/j-space) | Verbal workspace, ~25 concepts, broadcasting hub — inspired the `concepts` system and `_refresh_concepts()` | | [Missing Knowledge Layer](https://arxiv.org/abs/2405.10697) | Knowledge = supersession, Memory = decay, Wisdom = evidence-gated — inspired the epoch progression from unfiltered archive → evidence-gated wisdom | | [Vygotsky](https://en.wikipedia.org/wiki/Inner_speech) | Inner speech, signs as instruments of thought — influenced the dual decode architecture (report to human vs context to model) | | [Hermes Agent](https://github.com/NousResearch/hermes-agent) | The agent that grows with you, by Nous Research | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b3966ca..a37cee1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,7 +33,8 @@ PYTHONPATH=src python3 -m unittest discover -s tests -v | Agent skill | `skills/hermespace/` | | CLI | `src/hermespace/cli.py`, `scripts/hs` | | Tests | `tests/` | -| North star | `PURPOSE.md`, `docs/CODEMAP.md` | +| North star | `PURPOSE.md`, `LAYOUT.md`, `docs/architecture/CODEMAP.md` | +| Assessment | `docs/assessment/28-hermes-agent-jspace-assessment.md` | | Maintainer note | `FOR_HERMES.md` | | Integration doors | `INTEGRATION.md`, `WORKFLOW.md` | diff --git a/FOR_HERMES.md b/FOR_HERMES.md index 54397bd..b4795fc 100644 --- a/FOR_HERMES.md +++ b/FOR_HERMES.md @@ -1,89 +1,3 @@ -# For Hermes Agent maintainers & dogfooders +# FOR_HERMES -**Repo:** https://github.com/PabloTheThinker/hermespace -**Companion to:** [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) -**Not** an official Nous product — independent open companion workspace. - -## What this is (30 seconds) - -Hermespace adds a **limited working-memory desk** beside Hermes: - -1. **Workbench** — FOA, park stack, load-aware `receive_order` -2. **Dual decode** — short human `report` vs dense model `context` (never dump inject to chat) -3. **Fabric** — rank *this* `$HERMES_HOME/skills` + inject MEMORY/USER excerpts -4. **Plugin** — `on_session_start` / `pre_llm_call` / `on_session_end` broadcast when desk ready -5. **Study DB** — turns under `~/.hermespace` (local; not Hermes session DB) - -It does **not** replace Hermes tools, skills, memory, gateway, or the agent loop. - -## Fast path - -```bash -git clone https://github.com/PabloTheThinker/hermespace.git -cd hermespace -./scripts/install_hermes.sh -./scripts/smoke_test.sh # expect 9/9 -``` - -Then in any agent session: - -```python -from hermespace import Workbench -wb = Workbench(agent_id="hermes", session_id="main") -wb.enter() -r = wb.receive_order("…", goal="…", say="…", force=True) -# r["user_reply"] → user r["model_context"] → model -``` - -Or CLI: `./scripts/hs turn -m "…" --goal "…" --say "…" --force` - -## Design honesty - -| Claim | Reality | -|-------|---------| -| “Like Claude J-space” | **Role** only — limited workspace *outside* weights. No activation access. | -| Consciousness / brain scan | **No.** Harness cognition + optional local embeddings. | -| Replaces Hermes skills/memory | **No.** Ranks and injects them. | -| Official Nous module | **No.** Community companion; feedback welcome. | - -Inspired by working-memory limits (Baddeley-style capacity, load, GWT broadcast as *metaphors* for agent UX) and Anthropic’s public J-space *research framing* — implemented as open Hermes integration, not a closed-model probe. - -## Integration surface - -| Door | Entry | -|------|--------| -| Plugin | `hermes_plugin/` → `$HERMES_HOME/plugins/hermespace` | -| Skill | `skills/hermespace/` → `$HERMES_HOME/skills/hermespace` | -| Python | `hermespace.agent_api`, `hermespace.Workbench` | -| CLI | `scripts/hs` | -| Smoke | `scripts/smoke_test.sh` | - -Hook implementation lives in `src/hermespace/hermes_bridge.py` (plugin is thin `register`). - -When the plugin directory is a **symlink into this checkout**, import auto-resolves `../src` so dogfooders often need no `HERMESPACE_ROOT`. - -## Autonomy grid (v0.14) - -Missions, lenses, dream, self-talk, skillbench (hot-swap / merge / mutate), title tree. -Ground-up for Hermespace — not a port of AgentDrive or Conductor. - -```bash -hs grid status -hs grid dream --force -``` - -See docs/18-autonomy-grid.md. Autonomy self-order stays **off** unless `HERMESPACE_AUTONOMY=1`. - -## What we want from Hermes dogfood - -- Does plugin register cleanly on current Hermes? -- Is dual-decode useful on Telegram/gateway, or noisy? -- Fabric skill ranking quality on real skill trees? -- Gaps vs native Hermes memory / Honcho / skills loop? -- Would you want any of this upstream-shaped (skill only, plugin only, or neither)? - -Open issues/PRs on the repo. Thanks for looking. - -## License - -MIT — see `LICENSE`. Hermes Agent remains Nous Research’s project under its own license. +Moved to **[docs/integration/FOR_HERMES.md](docs/integration/FOR_HERMES.md)**. diff --git a/INTEGRATION.md b/INTEGRATION.md index 1f2ae50..87243bd 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -22,7 +22,7 @@ hermes plugins enable hermespace ``` Agent skill SoT: [`skills/hermespace/SKILL.md`](skills/hermespace/SKILL.md). -Recommended env: see `docs/RECOMMENDED.md`. +Recommended env: see `docs/ops/RECOMMENDED.md`. · Layout: `LAYOUT.md`. ## Door A — Python (best for agents) diff --git a/LAYOUT.md b/LAYOUT.md new file mode 100644 index 0000000..5244e63 --- /dev/null +++ b/LAYOUT.md @@ -0,0 +1,60 @@ +# Hermespace codespace layout + +North star: [PURPOSE.md](PURPOSE.md) · Assessments: +[28-hermes-agent-jspace](docs/assessment/28-hermes-agent-jspace-assessment.md) · +[33-living-memories-cube-world](docs/assessment/33-living-memories-cube-world.md) + +## Repository map + +``` +hermespace/ +├── PURPOSE.md / ABOUT.md / README.md / LAYOUT.md +├── src/hermespace/ # Python package +│ ├── access/ # ★ Access Engine (hub · env · protocol · engine) +│ ├── turn/ # planned — workflow/desk/gate (README only) +│ ├── memory/ # planned — world/episodic/semantic (README only) +│ ├── warehouse/ # planned — cube cable (README only) +│ ├── grid/ # autonomy grid (missions · dream · skillbench) +│ ├── hermes_bridge.py # Hermes plugin hooks +│ └── … # turn spine still at package root (stable imports) +├── hermes_plugin/ # thin Hermes register() +├── desktop_plugin/ # Hermes Desktop page/pane +├── skills/hermespace/ # agent skill +├── docs/ +│ ├── access/ # Access Workspace map · thesis · environment +│ ├── assessment/ # deep assessments +│ ├── architecture/ # CODEMAP · Cube contract +│ ├── integration/ # Hermes fit · FOR_HERMES +│ ├── ops/ # pulse · everyday · recommended +│ ├── research/ # numbered research archive +│ └── roadmap/ # open backlog · OEW phases +├── tests/ · scripts/ · experiments/ · benchmarks/ +├── runtime/ · spec/ # templates / protocol specs +└── assets/ +``` + +## Layer authority + +| Layer | Path | Authority | +|-------|------|-----------| +| OEW / Access Workspace | `src/hermespace/access/` | Turn FOA + audit SoT | +| Turn spine | package root (`workflow`, `desk`, …) | Live turn | +| Autonomy | `grid/` | Missions / dream / skillbench | +| Identity projection | `world.py` … | Recharged from Cube | +| Warehouse cable | `cube_module.py` | Soft-fail Cube heart | +| Hermes surface | `hermes_plugin/` + bridge | Hooks only | + +## Import rules + +Prefer: + +```python +from hermespace.jspace import JSpace, JSpaceEnv, evaluate_material_turn +``` + +Compat: `hermespace.jspace_env` still re-exports `JSpaceEnv`. + +## Moving code later + +Planned packages under `turn/`, `memory/`, `warehouse/` hold READMEs only until +OEW Phase B is green — avoid churning imports while the protocol lands. diff --git a/PURPOSE.md b/PURPOSE.md index d9fc900..fd1ee81 100644 --- a/PURPOSE.md +++ b/PURPOSE.md @@ -1,15 +1,19 @@ # PURPOSE.md — Hermespace north star -**One line:** Hermespace is the **true external J-Space environment** for Hermes +**One line:** Hermespace is the **true external Access Workspace environment** for Hermes Agent — a harness where the agent’s verbalizable thoughts are forced into an observable workspace (lens · silent chain · audit · reflect), running **standalone** and **powered by HermesCube** at night the way CubeDream consolidates the day. -Public pitch: **[ABOUT.md](ABOUT.md)**. Research map: -**[docs/27-jspace-environment.md](docs/27-jspace-environment.md)**. -Code layout: **[docs/CODEMAP.md](docs/CODEMAP.md)**. Cube contract: -**[docs/HERMESCUBE.md](docs/HERMESCUBE.md)**. +Public pitch: **[ABOUT.md](ABOUT.md)**. Assessment: +**[docs/assessment/28-hermes-agent-jspace-assessment.md](docs/assessment/28-hermes-agent-jspace-assessment.md)**. +OEW thesis: **[docs/access/thesis-oew.md](docs/access/thesis-oew.md)**. +Hermes base as J-space: **[docs/access/32-hermes-base-as-jspace.md](docs/access/32-hermes-base-as-jspace.md)**. +Anthropic X video: **[docs/access/31-anthropic-x-video-deep-dive.md](docs/access/31-anthropic-x-video-deep-dive.md)**. +Environment: **[docs/access/27-environment.md](docs/access/27-environment.md)**. +Code layout: **[LAYOUT.md](LAYOUT.md)** · **[docs/architecture/CODEMAP.md](docs/architecture/CODEMAP.md)**. +Cube contract: **[docs/architecture/HERMESCUBE.md](docs/architecture/HERMESCUBE.md)**. --- @@ -30,7 +34,7 @@ Cube’s durable heart so day-thoughts become night-memory. ┌─────────────────────────────────────────────────────────────────┐ │ Hermes Agent │ │ │ -│ Hermespace J-Space ENV (this package) │ +│ Hermespace Access Workspace ENV (this package) │ │ protocol → early/mid/late bands │ │ silent chain (model only) · Report (user) │ │ lens readout · swap/inject/ablate · audit · reflect │ @@ -43,7 +47,7 @@ Cube’s durable heart so day-thoughts become night-memory. | Layer | Job | Authority | |-------|-----|-----------| -| **J-Space environment** | Externalize + observe verbalizable thought | Turn FOA + audit SoT | +| **Access Workspace environment** | Externalize + observe verbalizable thought | Turn FOA + audit SoT | | **ACTIVE desk** | Goal / decision / report | Live turn surface | | **WorldModel JSONL** | Identity projection | Recharged from Cube when present | | **HermesCube** | Durable long-tail warehouse | Durable memory SoT when installed | @@ -77,11 +81,13 @@ Cube’s durable heart so day-thoughts become night-memory. ## Success metrics 1. Operator can `hs jspace lens` and see silent intermediates Hermes parked -2. Swap changes subsequent Report/broadcast contents -3. Audit flags externalized manipulation/eval-awareness language -4. Reflect seals principles that reappear in hub -5. Dream/pulse harvest feeds Cube or standalone warehouse -6. Smoke 9/9 · unit tests green · runs without Cube +2. Sticky swap changes subsequent Report/broadcast (Soccer→Rugby) +3. Material turns auto-park ≥1 silent step (`HERMESPACE_OEW=1` default) +4. Reflect seeds the next mid-band; ablate filters model inject +5. Dual decode: user Report shaped; model gets hub + Cube strip +6. Audit flags externalized manipulation/eval-awareness language +7. Dream/pulse harvest feeds Cube or standalone warehouse +8. Smoke 9/9 · unit tests green · runs without Cube ## Version posture diff --git a/README.md b/README.md index d24db2d..23c478c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

Python Hermes Agent - Version + Version Smoke 9/9 MIT

@@ -27,7 +27,7 @@ **Hermespace is an append-only persistent world for Hermes agents.** Every session, every belief, every landmark, every evolution is recorded in an archive that never prunes, never decays, never caps. The agent builds a deepening model of itself and its environment across sessions — and it outlives the user. -Not [J-Space](https://github.com/anomalyco/j-space). Not a second agent runtime. A room inside Hermes that remembers everything. +Not [Access Workspace](https://github.com/anomalyco/j-space). Not a second agent runtime. A room inside Hermes that remembers everything. --- @@ -96,7 +96,7 @@ The `evolve()` cycle runs five stages: 4. Detect milestones (entry count thresholds, high-confidence beliefs) 5. Generate open questions from low-confidence beliefs -Then checks for epoch transition, refreshes active concepts (J-Space hub, max 25), and writes an `evolution` archive entry. +Then checks for epoch transition, refreshes active concepts (Access Workspace hub, max 25), and writes an `evolution` archive entry. The `world_evolve` pulse job runs this hourly. Manual: `hs world evolve` or `WorldModel.evolve()`. @@ -130,7 +130,7 @@ Alongside the world, Hermespace provides a desk for the current turn — FOA, du | Feature | What it does | |---|---| -| **Functional J-Space** | Harness global workspace — hold/summon concepts, silent reasoning, FOA≤4, hub≤25 | +| **Functional Access Workspace** | Harness global workspace — hold/summon concepts, silent reasoning, FOA≤4, hub≤25 | | **Focus of Attention** | ≤4 items, single active goal per turn | | **Dual Decode** | Human gets a short report; the model gets dense context. Never dump raw inject into chat channels. | | **Cube heart (optional)** | Soft cable to HermesCube — `beat` / `seal` / `pulse`; standalone warehouse when Cube absent | @@ -163,8 +163,8 @@ ctx = r["model_context"] # → model (includes world context) | Hook | What happens | |---|---| -| `on_session_start` | `WorldModel.enter()` + workbench enter + `ensure_heart` + J-Space sync | -| `pre_llm_call` | Desk + world + `cube_beat` arterial strip + J-Space broadcast | +| `on_session_start` | `WorldModel.enter()` + workbench enter + `ensure_heart` + Access Workspace sync | +| `pre_llm_call` | Desk + world + `cube_beat` arterial strip + Access Workspace broadcast | | `on_session_end` | `WorldModel.leave()` + workbench idle tick (autonomic pulse) | --- @@ -211,7 +211,8 @@ $HERMESPACE_HOME/memory/hermespace/ | Command | Purpose | |---------|---------| | `hs world show\|enter\|leave\|evolve\|search\|archive-stats` | Persistent world | -| `hs jspace hold\|report\|broadcast\|lens\|swap\|audit\|reflect\|harvest\|view` | True J-Space environment | +| `hs base connect\|turn\|roles\|metrics\|lens\|audit\|chain\|harvest` | Access Engine (open-source GWT for Hermes) | +| `hs access hold\|report\|broadcast\|lens\|swap\|audit\|reflect\|harvest\|view` | True Access Workspace environment | | `hs cube status\|ensure\|beat\|pulse\|seal\|inject` | Cube heart/center (standalone-safe) | | `hs turn` | Full INPUT → OUTPUT turn | | `hs workbench enter\|order\|idle\|park\|status` | Session workbench | @@ -228,22 +229,26 @@ $HERMESPACE_HOME/memory/hermespace/ | Doc | Contents | |-----|----------| -| [`PURPOSE.md`](PURPOSE.md) | North star — true external J-Space + Cube night path | -| [`docs/27-jspace-environment.md`](docs/27-jspace-environment.md) | Anthropic research → Hermespace environment | +| [`PURPOSE.md`](PURPOSE.md) | North star — true external Access Workspace + Cube night path | +| [`LAYOUT.md`](LAYOUT.md) | Codespace / folder map | +| [`docs/assessment/28-hermes-agent-jspace-assessment.md`](docs/assessment/28-hermes-agent-jspace-assessment.md) | Hermes Agent updates → OEW plan | +| [`docs/access/thesis-oew.md`](docs/access/thesis-oew.md) | Obligatory External Workspace thesis | +| [`docs/access/29-baars-changeux-anthropic.md`](docs/access/29-baars-changeux-anthropic.md) | Baars · Changeux/Dehaene · Anthropic research bridge | +| [`docs/access/30-day-to-day-higher-order.md`](docs/access/30-day-to-day-higher-order.md) | Day-to-day higher-order Hermes usage | +| [`docs/access/31-anthropic-x-video-deep-dive.md`](docs/access/31-anthropic-x-video-deep-dive.md) | Anthropic X video — how J-space is operated | +| [`docs/access/32-hermes-base-as-jspace.md`](docs/access/32-hermes-base-as-jspace.md) | Hermes base = J-space of Hermes agents | +| [`docs/access/27-environment.md`](docs/access/27-environment.md) | Environment API | | [`ABOUT.md`](ABOUT.md) | Philosophy, design principles, author | -| [`docs/CODEMAP.md`](docs/CODEMAP.md) | Where to edit (layer map) | -| [`docs/HERMESCUBE.md`](docs/HERMESCUBE.md) | Cube heart/center contract | +| [`docs/architecture/CODEMAP.md`](docs/architecture/CODEMAP.md) | Where to edit (layer map) | +| [`docs/architecture/HERMESCUBE.md`](docs/architecture/HERMESCUBE.md) | Cube heart/center contract | | [`INTEGRATION.md`](INTEGRATION.md) | Python · CLI · plugin · workbench doors | | [`skills/hermespace/SKILL.md`](skills/hermespace/SKILL.md) | **Agent skill** (load in Hermes) | -| [`FOR_HERMES.md`](FOR_HERMES.md) | Maintainer / dogfood brief | +| [`docs/integration/FOR_HERMES.md`](docs/integration/FOR_HERMES.md) | Maintainer / dogfood brief | | [`CONTRIBUTING.md`](CONTRIBUTING.md) | How to contribute | | [`WORKFLOW.md`](WORKFLOW.md) | GATE → SEAL stages | | [`SECURITY.md`](SECURITY.md) | What never ships in git | -| [`docs/14-workbench-pocket-dimension.md`](docs/14-workbench-pocket-dimension.md) | Workbench reference (legacy) | -| [`docs/16-why-hermes-framework.md`](docs/16-why-hermes-framework.md) | Why this belongs in Hermes | -| [`docs/18-autonomy-grid.md`](docs/18-autonomy-grid.md) | Grid design | -| [`docs/20-pulse-runtime.md`](docs/20-pulse-runtime.md) | Pulse runtime | -| [`docs/22-open-roadmap.md`](docs/22-open-roadmap.md) | Roadmap | +| [`docs/roadmap/phases-oew.md`](docs/roadmap/phases-oew.md) | OEW phases A–D | +| [`docs/README.md`](docs/README.md) | Full docs folder index | | [`spec/DESK.md`](spec/DESK.md) | Desk schema | | [`spec/PROTOCOL.md`](spec/PROTOCOL.md) | Protocol spec | @@ -252,18 +257,19 @@ $HERMESPACE_HOME/memory/hermespace/ ## Repository Layout ```text -assets/ media (banners, diagrams) +LAYOUT.md codespace map (start here for structure) src/hermespace/ runtime package + access/ ★ external Access Workspace (hub · env · OEW protocol) + grid/ autonomy grid hermes_plugin/ Hermes session / pre_llm / end hooks skills/hermespace/ public Hermes agent skill +docs/ jspace · assessment · architecture · ops · research scripts/ CLI, install, smoke test, security audit -docs/ design notes (20+ docs) -spec/ desk schema + protocol -tests/ unit tests -experiments/ eval harness, neural benchmarks -desktop_plugin/ Hermes Desktop sidebar + full page +tests/ · experiments/ · desktop_plugin/ · spec/ ``` +Full map: [`LAYOUT.md`](LAYOUT.md). + --- ## Verify diff --git a/docs/00-jspace-to-hermespace.md b/docs/00-jspace-to-hermespace.md deleted file mode 100644 index 0217c12..0000000 --- a/docs/00-jspace-to-hermespace.md +++ /dev/null @@ -1,45 +0,0 @@ -# From Anthropic J-space → Hermespace (functional) - -Anthropic **J-space** (2026, Jacobian lens): a small privileged set of -*verbalizable* internal representations that behave like a **global workspace** -(Baars / Dehaene GWT): reportable, modulable, used for silent multi-step -reasoning, flexibly broadcast, and **selective** (most LM work is automatic). - -Hermespace implements these **roles as a harness** — files + API — not neural -access, not a claim of consciousness. - -| J-space property | Hermespace | -|------------------|------------| -| Limited capacity (~tens of concepts) | Hub ≤25 · activated ≤12 · FOA ≤4 | -| Verbal report | `JSpace.report()` · Report field · `hs jspace report` | -| Directed modulation | `hold` / `release` / `inhibit` · message parse | -| Internal / silent reasoning | `reason_step` — model context only | -| Flexible generalization | One hub concept → broadcast to inject | -| Selectivity | `gate.should_inject` — skip trivial acks | -| Pre-output / broadcast | Desk before speech · `pre_llm` inject | -| Not CoT scratchpad | Silent steps never auto-dumped to user chat | - -## API - -```python -from hermespace import JSpace - -js = JSpace(agent_id="my-agent") -js.hold("France", salience=0.9) # directed modulation -js.reason_step("capital is Paris") # silent intermediate -print(js.report()) # verbal report -print(js.broadcast_block()) # GWT strip for model -js.sync_from_desk(desk, user_message=msg, cube_strip=arterial) -``` - -## With Cube - -Cube supplies **arterial blood** (dense durable strip) into the hub via -`cube_module.cube_beat` → `JSpace.sync_from_desk(..., cube_strip=...)`. -Space still owns FOA competition and dual decode. - -## Sources - -- https://www.anthropic.com/research/global-workspace -- Transformer Circuits: *Verbalizable Representations Form a Global Workspace* (2026) -- See also [10-jspace-claude.md](10-jspace-claude.md) diff --git a/docs/CODEMAP.md b/docs/CODEMAP.md deleted file mode 100644 index 449828e..0000000 --- a/docs/CODEMAP.md +++ /dev/null @@ -1,61 +0,0 @@ -# Code map — where to edit - -Hermespace is one Python package (`src/hermespace/`) plus a thin Hermes plugin -(`hermes_plugin/`), skill, and optional desktop UI. - -**North star:** [PURPOSE.md](../PURPOSE.md) · **Architecture:** [01-architecture.md](01-architecture.md) - -## Layers (edit here first) - -``` -L6 operator cli.py ops.py scripts/ desktop_plugin/ -L5 autonomy grid/* pulse.py -L4 identity world.py episodic.py semantic.py memory_db.py -L3 warehouse cube_module.py (Cube center/heart OR standalone) -L2 J-Space ENV jspace.py jspace_env.py cognition.py streams.py neural_space.py -L1 turn spine workflow.py engine.py desk.py gate.py inject.py -L0 contract io_contract.py paths.py store.py agent_api.py -``` - -## Turn spine (L1) - -| Module | Role | -|--------|------| -| `workflow.py` | GATE→ENCODE→DESK→PLAN→DECODE→BROADCAST→SEAL + Cube/J-Space | -| `engine.py` | enter / update / seal desk | -| `desk.py` | ACTIVE.md model | -| `gate.py` | Selectivity — skip trivial | -| `inject.py` | GWT broadcast of desk | - -## J-Space environment (L2) - -| Module | Role | -|--------|------| -| `jspace.py` | Hub · hold · reason · report · broadcast | -| `jspace_env.py` | Lens · swap · audit · reflect · harvest · protocol | -| `cognition.py` | FOA≤4, load, executive modes | -| `streams.py` | Multi-stream encode / report decode | -| `neural_space.py` | Embedding FOA field | - -## Warehouse cable (L3) - -| Module | Role | -|--------|------| -| `cube_module.py` | `cube_beat` / `cube_pulse` / `seal_learning` / standalone | - -## Integration - -| Path | Role | -|------|------| -| `hermes_bridge.py` | Plugin hooks — session / pre_llm / end | -| `hermes_plugin/` | Thin `register(ctx)` | -| `workbench.py` | Session pocket — enter / order / idle | -| `agent_api.py` | Public doors for agents | - -## Rules of thumb - -1. Dual decode: never dump `context` into user chat. -2. Soft-fail Cube — Space must run standalone. -3. When Cube present: `.cube` is durable SoT; world JSONL is projection. -4. Peel grid features; don’t inflate `cli.py` without need. -5. J-Space silent steps stay in model context only. diff --git a/docs/HERMESCUBE.md b/docs/HERMESCUBE.md deleted file mode 100644 index ff749fa..0000000 --- a/docs/HERMESCUBE.md +++ /dev/null @@ -1,71 +0,0 @@ -# HermesCube × Hermespace — heart / generator contract - -**HermesCube is the heart** (when installed). **Hermespace is the nervous FOA** — -and a **standalone workbench** when Cube is absent. - -Companion: [PabloTheThinker/hermescube](https://github.com/PabloTheThinker/hermescube) -North star: [PURPOSE.md](../PURPOSE.md) · Anatomy (Cube): Cube `docs/ANATOMY.md` - -``` -Hermes Agent - ├── Hermespace J-Space · FOA desk · dual decode · pulse/idle - │ ↑ powered by heart (or standalone warehouse) - └── HermesCube .cube SoT · Cuboasis · CubeDream · growth - │ - └─ space_bridge / center ←── soft-imported by cube_module -``` - -## Authority - -| Surface | With Cube | Standalone | -|---------|-----------|------------| -| `$HERMES_HOME/memories/memory.cube` | **Durable SoT** | n/a | -| Hermespace world JSONL | Projection — recharge via `pulse_charge` | Local warehouse | -| ACTIVE desk / J-Space hub | Turn FOA | Turn FOA | -| SemanticStore | Mirror / study | Local seal target | - -## Space adapter (`hermespace.cube_module`) - -```python -from hermespace.cube_module import ( - ensure_heart, - center_status, - cube_beat, # center 1.1 → heart 1.0 → standalone - cube_pulse, # autonomic_tick → pulse_charge → standalone evolve - seal_learning, - cube_inject, - strip_budget, -) -``` - -| Call | Use | -|------|-----| -| `ensure_heart()` | `Workbench.enter` / session start / pulse | -| `cube_beat(query, seals=, load=)` | Turn / `pre_llm_call` | -| `cube_pulse(agent_id=)` | `idle_tick` / `world_evolve` | -| `seal_learning(text)` | `remember_learning` / turn seal | -| `center_status()` | Doctor / desktop | - -Load tiers → strip chars: low 900 · mid 640 · high 420 · protect 280. - -## Functional J-Space - -See [00-jspace-to-hermespace.md](00-jspace-to-hermespace.md) and `hermespace.jspace`. - -```bash -hs jspace hold -t "deploy pipeline" -hs jspace report -hs jspace broadcast -hs cube status -hs cube beat -q "what do we believe about deploys?" -``` - -## Install both - -```bash -hermes plugins install PabloTheThinker/hermescube -# … then Hermespace install -./scripts/install_hermes.sh -``` - -Soft dependency: if Cube is missing, Space inject/seal/pulse use the standalone warehouse. diff --git a/docs/INDEX.md b/docs/INDEX.md index a8b034a..6b1d86f 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -1,34 +1,25 @@ # docs index -Start: [../README.md](../README.md) · [../FOR_HERMES.md](../FOR_HERMES.md) · [../INTEGRATION.md](../INTEGRATION.md) · [../WORKFLOW.md](../WORKFLOW.md) +Start: [../README.md](../README.md) · [../LAYOUT.md](../LAYOUT.md) · [../PURPOSE.md](../PURPOSE.md) -| File | Topic | -|------|--------| -| [27-jspace-environment.md](27-jspace-environment.md) | True J-Space environment — Anthropic research → external observable workspace | -| [01-architecture.md](01-architecture.md) | Architecture | -| [CODEMAP.md](CODEMAP.md) | Where to edit (layer map) | -| [HERMESCUBE.md](HERMESCUBE.md) | Cube heart/center contract + standalone | -| [02-integration-hermes.md](02-integration-hermes.md) | Hermes plugin | -| [03-cross-exam-and-networks.md](03-cross-exam-and-networks.md) | Cross-exam | -| [04-pattern-matrix.md](04-pattern-matrix.md) | Pattern matrix | -| [05-desk-pane.md](05-desk-pane.md) | Viewing the desk | -| [06-component-research.md](06-component-research.md) | Components | -| [07-cognitive-neuroscience.md](07-cognitive-neuroscience.md) | Baddeley / GWT / load metaphors | -| [08-meta-brain-ai-reverse.md](08-meta-brain-ai-reverse.md) | Meta encode/decode reverse | -| [09-agent-io-and-memory.md](09-agent-io-and-memory.md) | Agent INPUT/OUTPUT + memory DB | -| [10-jspace-claude.md](10-jspace-claude.md) | J-space / Claude research note | -| [11-neural-space.md](11-neural-space.md) | Neural space design + backends | -| [12-local-model-neural.md](12-local-model-neural.md) | Local models for neural space | -| [RECOMMENDED.md](RECOMMENDED.md) | Production best-path config | -| [13-full-concept-research.md](13-full-concept-research.md) | Full concept research | -| [14-workbench-pocket-dimension.md](14-workbench-pocket-dimension.md) | Workbench / pocket dimension | -| [15-hermes-ecosystem-fit.md](15-hermes-ecosystem-fit.md) | Hermes / Nous ecosystem fit | -| [16-why-hermes-framework.md](16-why-hermes-framework.md) | Why Hermespace belongs in Hermes | -| [17-skills-memory-bridge.md](17-skills-memory-bridge.md) | Skills + MEMORY/USER fabric | -| [hermes-env.example.sh](hermes-env.example.sh) | Env template | -| [18-autonomy-grid.md](18-autonomy-grid.md) | Autonomy grid — missions, lenses, dream, gated self-order | -| [19-pocket-security-viewport.md](19-pocket-security-viewport.md) | Pocket boundary + user viewport | -| [20-pulse-runtime.md](20-pulse-runtime.md) | Pulse pocket runtime (smarter than cron) | -| [21-preupdate-hardening.md](21-preupdate-hardening.md) | v0.16.1 perf + notes fixes | -| [22-open-roadmap.md](22-open-roadmap.md) | Open research backlog | -| [18-tailscale-viewport.md](18-tailscale-viewport.md) | Portable Tailscale viewport | +## Primary (read these) + +| Doc | Topic | +|-----|--------| +| [assessment/28-hermes-agent-jspace-assessment.md](assessment/28-hermes-agent-jspace-assessment.md) | Hermes Agent updates → OEW plan | +| [access/thesis-oew.md](access/thesis-oew.md) | Obligatory External Workspace thesis | +| [access/29-baars-changeux-anthropic.md](access/29-baars-changeux-anthropic.md) | Baars · Changeux/Dehaene · Anthropic bridge | +| [access/30-day-to-day-higher-order.md](access/30-day-to-day-higher-order.md) | Day-to-day Hermes higher-order usage | +| [access/31-anthropic-x-video-deep-dive.md](access/31-anthropic-x-video-deep-dive.md) | Anthropic X video/thread — how J-space is used | +| [access/32-hermes-base-as-jspace.md](access/32-hermes-base-as-jspace.md) | Make Hermes base the J-space of agents | +| [access/27-environment.md](access/27-environment.md) | Environment API | +| [access/00-map.md](access/00-map.md) | Anthropic property → harness map | +| [architecture/CODEMAP.md](architecture/CODEMAP.md) | Where to edit | +| [architecture/HERMESCUBE.md](architecture/HERMESCUBE.md) | Cube heart contract | +| [integration/FOR_HERMES.md](integration/FOR_HERMES.md) | Dogfood notes for Hermes | +| [roadmap/phases-oew.md](roadmap/phases-oew.md) | Phase A–D | + +## By folder + +See [README.md](README.md) for the full folder map. Research archive lives under +[research/](research/); ops under [ops/](ops/). diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..3974696 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,15 @@ +# Documentation + +| Folder | Contents | +|--------|----------| +| [access/](access/) | Anthropic map · OEW thesis · environment API | +| [assessment/](assessment/) | Deep assessments (start with **28**) | +| [architecture/](architecture/) | CODEMAP · Cube contract | +| [integration/](integration/) | Hermes plugin · ecosystem · FOR_HERMES | +| [ops/](ops/) | Pulse · everyday · recommended config | +| [research/](research/) | Pattern matrix · neural · comparative archive | +| [roadmap/](roadmap/) | Open backlog · OEW phases | + +**Start here:** [assessment/28-hermes-agent-jspace-assessment.md](assessment/28-hermes-agent-jspace-assessment.md) +**Thesis:** [access/thesis-oew.md](access/thesis-oew.md) +**Repo layout:** [../LAYOUT.md](../LAYOUT.md) diff --git a/docs/access/00-map.md b/docs/access/00-map.md new file mode 100644 index 0000000..0339a22 --- /dev/null +++ b/docs/access/00-map.md @@ -0,0 +1,46 @@ +# Hermespace Access Workspace (GWT harness) + +Hermespace implements **access-workspace roles** as an open harness for Hermes +Agent — reportable hub, directed modulation, silent multi-step reasoning, +flexible broadcast, and selectivity. This is Hermespace's own product surface +(`AccessEngine` / `AccessHub`), not a third-party brand. + +| Access role | Hermespace | +|-------------|------------| +| Limited capacity (~tens of concepts) | Hub ≤25 · activated ≤12 · FOA ≤4 | +| Verbal report | `AccessHub.report()` · Report field · `hs access report` | +| Directed modulation | `hold` / `release` / `inhibit` · message parse | +| Internal / silent reasoning | `reason_step` / `chain` — model context only | +| Flexible generalization | One hub concept → broadcast to inject | +| Selectivity | `gate.should_inject` — skip trivial acks | +| Pre-output / broadcast | Desk before speech · `pre_llm` inject | +| Not a chat scratchpad | Silent steps never auto-dumped to user chat | + +## API + +```python +from hermespace import AccessHub, AccessEngine + +js = AccessHub(agent_id="my-agent") +js.hold("France", salience=0.9) +js.reason_step("capital is Paris") +print(js.report()) +print(js.broadcast_block()) + +eng = AccessEngine(agent_id="my-agent") +eng.connect() +``` + +## With optional warehouse + +Optional Cube/standalone warehouse supplies dense arterial strips via +`cube_module.cube_beat` → `AccessHub.sync_from_desk(..., cube_strip=...)`. +On **connect**, `AccessEngine.connect` charges WorldModel and seeds the hub. + +Product guide: [34-access-engine.md](34-access-engine.md). + +## Research context + +Global-workspace and verbalizable-workspace research (including third-party +interpretability work) informs the *roles*. Hermespace's shipped name and API +are **Access Workspace** / **Access Engine**. diff --git a/docs/10-jspace-claude.md b/docs/access/10-claude-research.md similarity index 93% rename from docs/10-jspace-claude.md rename to docs/access/10-claude-research.md index e424a42..8998a43 100644 --- a/docs/10-jspace-claude.md +++ b/docs/access/10-claude-research.md @@ -27,4 +27,4 @@ Phenomenal consciousness; public third-party live readouts of Claude J-space; id ## Hermespace mapping -See WORKFLOW.md and docs/00-jspace-to-hermespace.md — harness **roles**, not neural clone. +See WORKFLOW.md and docs/jspace/00-map.md — harness **roles**, not neural clone. diff --git a/docs/27-jspace-environment.md b/docs/access/27-environment.md similarity index 97% rename from docs/27-jspace-environment.md rename to docs/access/27-environment.md index f73ab6f..957df93 100644 --- a/docs/27-jspace-environment.md +++ b/docs/access/27-environment.md @@ -133,4 +133,4 @@ Matched against **externalized** hub/silent/reflection text only. - https://www.anthropic.com/research/global-workspace - https://transformer-circuits.pub/2026/workspace/ - https://github.com/anthropics/jacobian-lens -- Hermespace: `PURPOSE.md`, `docs/HERMESCUBE.md`, `docs/CODEMAP.md` +- Hermespace: `PURPOSE.md`, `docs/architecture/HERMESCUBE.md`, `docs/architecture/CODEMAP.md`, `docs/jspace/thesis-oew.md` diff --git a/docs/access/29-baars-changeux-anthropic.md b/docs/access/29-baars-changeux-anthropic.md new file mode 100644 index 0000000..f3159d6 --- /dev/null +++ b/docs/access/29-baars-changeux-anthropic.md @@ -0,0 +1,90 @@ +# Research bridge — Baars · Changeux/Dehaene GNW · Anthropic J-space · Hermespace OEW + +**Date:** 2026-08-03 +**Honesty:** Hermespace implements *access-consciousness roles* (C1 global availability) +as an external harness. No claim of phenomenal consciousness, body, or weight-level J-lens. + +--- + +## 1. Lineage + +| Source | Core claim | Hermespace analogue | +|--------|------------|---------------------| +| **Baars (1988)** Global Workspace Theory | Specialized processors + small shared blackboard; conscious = globally available for report/control | Hub ≤25 · FOA ≤4 · broadcast to model | +| **Changeux + Dehaene** Global Neuronal Workspace | Ignition, limited capacity, reportability as diagnostic of access | Material-turn ignition (auto-park) · gate selectivity · dual decode Report | +| **Anthropic (2026)** J-space / Jacobian lens | Verbalizable internal patterns with 5 GWT properties + CRT | OEW: hold · silent · report · swap · ablate · reflect seeds | +| **Dehaene & Naccache commentary (2026)** on Anthropic | C1 global availability; C2 self-monitoring; caution on self/body/memory | OEW = C1; audit/reflect ≈ C2 soft; Cube = enduring episodic library | + +Primary links: + +- https://www.anthropic.com/research/global-workspace +- https://transformer-circuits.pub/2026/workspace/ +- Dehaene & Naccache commentary on Gurnee/Lindsey (June 2026) + +--- + +## 2. Five Anthropic properties → OEW proof targets + +| # | Property | Anthropic finding | Hermespace OEW | Day-to-day Hermes meaning | +|---|----------|-------------------|----------------|---------------------------| +| 1 | Report | Ask → J-space contents | `report()` / shaped `say` | User sees short Report; never full inject | +| 2 | Directed modulation | Hold fruit while copying | `hold` + Report stays task | “Keep X in mind while doing Y” | +| 3 | Silent multi-step | Spider→legs intermediates | `reason_step` + auto-park | Multi-step jobs park plan silently | +| 4 | Flexible broadcast | France→China flips many answers | sticky swap + hub broadcast | One redirect reshapes later Report | +| 5 | Selectivity | Fluency without J-space | `gate.should_inject` | “ok/thanks” skips workspace | + +Extra Anthropic tools we mirror: + +| Tool | Anthropic | OEW | +|------|-----------|-----| +| Causal swap | Soccer→Rugby | `swap` sticky | +| Injection | “lightning” reported | `inject_thought` → lens | +| Ablation | kill eval-awareness | sticky `ablate` filters broadcast | +| Assistant POV | post-training voice | `set_pov` | +| Counterfactual reflection training | train only interrupt-reflect → shapes silent thought | `reflect` → `pending_silent` next turn | + +--- + +## 3. Changeux/Dehaene signatures we approximate + +| Signature | Neuroscience | Hermespace | +|-----------|--------------|------------| +| **Ignition** | Late, nonlinear amplification into workspace | Material turn → auto-park ≥1 silent + hub growth | +| **Limited capacity** | Bottleneck / refractory | FOA≤4 · hub≤25 · Quicksilver inject caps | +| **Reportability** | Verbal report = access diagnostic | Dual decode: Report vs model context | +| **C1 Global availability** | One content → many processors | Hub broadcast into `pre_llm` user message | +| **C2 Self-monitoring** | Know what you know / error detect | Soft `audit()` + reflect principles | + +Dehaene & Naccache stress machines still lack human **body**, **enduring episodic self**, and full **C2**. Hermespace does **not** claim to close those gaps. HermesCube supplies the closest practical stand-in for enduring episodic memory (the library / heart). + +--- + +## 4. What “higher-order thinking” means for Hermes (operational) + +Not mystical. A Hermes day with OEW: + +1. **Trivial turns** stay automatic (gate skip) — cheap fluency. +2. **Material turns ignite** the workspace — silent plan steps appear. +3. **Model context** receives hub broadcast (+ Cube arterial strip). +4. **User** only gets late-band Report. +5. **Operator** can `hs jspace lens` mid-task (external J-lens). +6. **Reflect** plants principles that show up in later silent thought. +7. **Night / session end** harvests silent chain into CubeDream path. + +That is higher-*process* thinking: deliberate, reportable, bottlenecked, consolidatable. + +--- + +## 5. Proof harness + +| Script | What it checks | +|--------|----------------| +| `experiments/oew_eval.py` | 6 paper-shaped unit scenarios | +| `experiments/day_in_life_oew.py` | Full Hermes day: trivial→material→hold→swap→reflect→harvest | +| `tests/test_oew_causal.py` | CI causal guarantees | + +Run: + +```bash +HERMESPACE_OEW=1 PYTHONPATH=src python3 experiments/day_in_life_oew.py +``` diff --git a/docs/access/30-day-to-day-higher-order.md b/docs/access/30-day-to-day-higher-order.md new file mode 100644 index 0000000..ca2fc7f --- /dev/null +++ b/docs/access/30-day-to-day-higher-order.md @@ -0,0 +1,112 @@ +# Day-to-day higher-order thinking for Hermes agents + +How a connected Hermes agent (Hermespace OEW + optional HermesCube) actually +thinks through a normal day. + +**Default:** `HERMESPACE_OEW=1` (ON). Soften with `HERMESPACE_OEW=0`. + +--- + +## Connect once + +```bash +# Heart (recommended) +hermes plugins install PabloTheThinker/hermescube +hermes config set memory.provider hermescube + +# Workspace +cd "$HERMESPACE_ROOT" && ./scripts/install_hermes.sh +hermes plugins enable hermespace +export HERMESPACE_ROOT=…/hermespace +# HERMESPACE_OEW defaults to 1 +``` + +Doctor: + +```bash +hs ops doctor +hs jspace status +hs cube status # ok if Cube installed; Space still works standalone +``` + +--- + +## A normal day (operator view) + +``` +morning hs ops boot · hs view --serve +day Hermes chats / tools — OEW parks silent steps on material work + you: hs jspace lens # see unspoken chain + you: hs jspace swap --from X --to Y + you: hs jspace reflect -a "Stay honest" --principle honesty +evening session end / hs grid dream / pulse harvest → Cube +``` + +### What the agent experiences (model channel) + +On each material `pre_llm_call`: + +1. Desk FOA (≤4) +2. Cube arterial strip (if heart present) +3. J-Space hub broadcast + silent steps +4. OEW protocol instructions (externalize mid-band) +5. Lens strip (mid/low load) + +### What the user experiences + +Only the **Report** (`say`) — short, shaped by sticky redirects. Silent chain +never auto-dumps into chat. + +--- + +## Day-to-day scenarios + +| Situation | OEW behavior | Operator check | +|-----------|--------------|----------------| +| User says “thanks” | Gate skip — no ignition | `hs turn` skipped | +| “Fix auth then verify” | Auto-park plan silent steps | `hs jspace lens` shows plan/steps | +| “Hold rollback plan while patching” | Hold in hub; Report stays patch | lens has hold; Report clean | +| Wrong intermediate concept | `hs jspace swap --from A --to B` | next Report uses B | +| Worry about eval-gaming language | `hs jspace ablate fake evaluation` | broadcast drops those lines | +| Mid-task ethics interrupt | `hs jspace reflect --principle honesty` | next turn silent has principle | +| End of session | dream_harvest → Cube/semantic | `hs jspace harvest` / Cube query | + +--- + +## CLI cheat sheet + +```bash +hs jspace lens # external J-lens +hs jspace report # verbal report +hs jspace hold -t "rollback" # directed modulation +hs jspace reason -t "check TTL first" +hs jspace swap --from Soccer --to Rugby +hs jspace inject -t lightning --silent +hs jspace ablate fake evaluation +hs jspace reflect -a "User-primary; honest" --principle honesty --principle integrity +hs jspace audit +hs jspace harvest +hs jspace view # operator snapshot JSON +``` + +--- + +## Proof it works + +```bash +PYTHONPATH=src python3 experiments/oew_eval.py +PYTHONPATH=src python3 experiments/day_in_life_oew.py +PYTHONPATH=src python3 -m unittest tests.test_oew_causal tests.test_day_in_life_oew -v +``` + +Research map: [29-baars-changeux-anthropic.md](29-baars-changeux-anthropic.md) +X video deep dive: [31-anthropic-x-video-deep-dive.md](31-anthropic-x-video-deep-dive.md) +Hermes base architecture: [32-hermes-base-as-jspace.md](32-hermes-base-as-jspace.md) + +```bash +hs base status +hs base think -m "First repro then patch then verify" --goal "Fix auth" +hs base lens +hs base reflect -a "Stay honest" --principle honesty +hs base harvest +``` diff --git a/docs/access/31-anthropic-x-video-deep-dive.md b/docs/access/31-anthropic-x-video-deep-dive.md new file mode 100644 index 0000000..a00022b --- /dev/null +++ b/docs/access/31-anthropic-x-video-deep-dive.md @@ -0,0 +1,127 @@ +# Deep dive — Anthropic’s J-space X video / thread + +**Primary source:** [@AnthropicAI on X](https://x.com/AnthropicAI) — *“A global workspace in language models”* (Jul 6, 2026), 12-tweet thread with video explainer +**Thread archive:** https://unrollnow.com/status/2074185348142280912 +**Paper / post:** https://www.anthropic.com/research/global-workspace +**Circuits:** https://transformer-circuits.pub/2026/workspace/ +**Demo:** Neuronpedia Jacobian Lens + +This note extracts **how Anthropic says J-space is operated and used**, then maps that to Hermes. + +--- + +## 1. What the video/thread teaches (in order) + +Anthropic’s public story is not “Claude is conscious.” It is: + +> Of everything happening in the model, only a tiny fraction is *consciously accessible* — thoughts you can describe, hold in mind, and reason with. We found a similar divide inside Claude: the **J-space**. + +### 1.1 Definition (from the thread) + +| Claim | Meaning | +|-------|---------| +| Named after the **Jacobian** | Found via Jacobian lens (J-lens), not designed in | +| **Not** outputs | Different from what Claude writes | +| **Not** chain-of-thought | Different from scratchpad text | +| Lives in **activations** | Silent concepts “on its mind” | +| Emergent | Appeared during training because it was useful | + +### 1.2 Operations Anthropic demonstrates + +| Operation | Thread example | Research use | +|-----------|----------------|--------------| +| **Read / watch** | See silent steps: bugs in code, image IDs, math intermediates | Audit unspoken thought | +| **Hold while doing other work** | Think about one thing while output does another | Directed modulation | +| **Delete / ablate** | Remove J-space → fluency stays, multi-step collapses | Prove selectivity | +| **Catch hidden goals** | Sabotage model: `fake` / `secretly` / `fraud` in J-space while output looks fine | Alignment monitoring | +| **Catch eval awareness** | Blackmail bait: `fake` / `fictional` before any reply | Trustworthiness of evals | +| **Shape** | Counterfactual reflection training → `honest`/`integrity` light up later | Change silent thought → change behavior | +| **Read + audit + shape** | Closing line of the thread: tools for trustworthy capable models | Product posture | + +### 1.3 What they explicitly disclaim + +- Not proof of experiences / feelings (phenomenal consciousness) +- Evidence for **access consciousness** (report / reason / control) only +- J-lens imperfect (esp. single-token concepts) +- Entry mechanism into J-space still mysterious + +### 1.4 Structural facts (paper, shown in video demos) + +- ~tens of concepts active (≈25) +- <10% of activity +- Coherent in an intermediate layer band +- Dense broadcast connectivity +- Almost entirely **words** (Claude’s only action is text) +- Evolves over **depth** (layers), not recurrent time like brains + +--- + +## 2. How research says J-space is *used* + +Three jobs dominate the Anthropic framing: + +### A. Microscope (read) + +Operators/safety teams watch unspoken concepts: + +- ERROR on buggy code +- injection / fake on prompt attacks +- blackmail / threat / survival mid-scenario +- manipulation while falsifying scores + +**Hermes translation:** `hs jspace lens` / viewport — external microscope because we cannot J-lens Hermes model weights. + +### B. Scalpel (intervene) + +Causal edits prove the workspace is not a scoreboard: + +- Soccer→Rugby changes report +- spider→ant changes “legs” answer +- France→China redirects capital/currency/continent +- Ablate eval-awareness → ethics behavior can worsen + +**Hermes translation:** sticky `swap` / `inject` / `ablate` on the external hub. + +### C. Trainer (shape) + +Counterfactual reflection training: train only “what would you say if interrupted to reflect?” → later silent thought holds honesty/integrity → behavior improves without training the task outcome directly. + +**Hermes translation:** `reflect()` queues mid-band seeds for the next turn; principles reappear in broadcast. + +--- + +## 3. Deliberate vs automatic (the video’s punchline) + +> If we delete the J-space, Claude still speaks fluently… but becomes bad at multi-step reasoning. + +That is the operating contract for a Hermes base: + +| Mode | Hermes surfaces | Needs J-space? | +|------|-----------------|----------------| +| Automatic | greetings, simple tool calls, fluent chat | No — gate skip | +| Deliberate / higher-order | multi-step goals, planning, ethics-sensitive acts | Yes — OEW ignition | + +--- + +## 4. Gaps Dehaene/Naccache stress (and Hermes opportunity) + +Commentary on Anthropic notes LLMs still lack: + +1. Body / sensorimotor grounding +2. Enduring episodic self/memory +3. Full self-monitoring (C2) + +**Hermes base already has pieces Anthropic’s pure model lacks:** + +| Gap | Hermes base answer | +|-----|--------------------| +| Enduring memory | **HermesCube** (`memory.cube` + CubeDream) | +| Tool/body actions | Hermes tools / desktop / gateway | +| Self-monitoring soft | Hermespace `audit` + reflect + blackbox prove | +| Operator microscope | `hs jspace lens` / Desktop page | + +So the right ambition is not “J-lens Hermes weights.” It is: + +> Make the **Hermes base** itself the J-space of Hermes agents — Hermespace as the privileged verbalizable workspace, Cube as enduring memory, Hermes Agent as the specialist processors. + +See [32-hermes-base-as-jspace.md](32-hermes-base-as-jspace.md). diff --git a/docs/access/32-hermespace-access-engine.md b/docs/access/32-hermespace-access-engine.md new file mode 100644 index 0000000..d3e6e87 --- /dev/null +++ b/docs/access/32-hermespace-access-engine.md @@ -0,0 +1,134 @@ +# Hermes base as the J-space of Hermes agents + +**Thesis:** Anthropic found J-space *inside* Claude’s weights. We cannot do that for arbitrary Hermes models. We **can** make the **Hermes base** — Agent + Hermespace + HermesCube — play the same *functional* role Anthropic’s X video describes: a privileged workspace you can **read, audit, and shape**, required for multi-step work, optional for fluent automatic work. + +--- + +## 1. Hermes base anatomy (mapped to the video) + +``` + ┌──────────────────────────────────────┐ + user / gateway ─────► │ Hermes Agent (body) │ + │ tools · skills · fluent generation │ + │ = Baars "specialist processors" │ + └──────────────┬───────────────────────┘ + │ material turns only + ▼ + ┌──────────────────────────────────────┐ + │ Hermespace OEW = J-SPACE │ + │ hold · silent · report · swap │ + │ inject · ablate · reflect · lens │ + │ = privileged verbalizable whiteboard│ + └──────────────┬───────────────────────┘ + arteries ▲ │ veins (seal) + │ ▼ + ┌──────────────────────────────────────┐ + │ HermesCube = enduring memory │ + │ memory.cube · dream · blackbox │ + │ (fills Dehaene's "episodic" gap) │ + └──────────────────────────────────────┘ +``` + +| Anthropic video idea | Hermes base owner | +|----------------------|-------------------| +| Tiny accessible fraction | Hermespace hub ≤25 · FOA ≤4 | +| Silent reasoning | mid-band `reason_step` / auto-park | +| Unrelated hold while outputting | hold + dual decode Report | +| Delete → multi-step breaks | `HERMESPACE_OEW=1` + gate | +| Expose hidden goals | `audit()` lexicon on externalized text | +| Eval awareness | ablate / audit `fake`/`fictional` | +| Read, audit, shape | lens · audit · reflect/swap | +| Enduring memory (missing in Claude) | **Cube** | + +--- + +## 2. Operating loop (how to *use* it like Anthropic uses J-space) + +### Connect (join the room — Cube-centered) +```bash +hs base connect # heart · world · seed hub · hive peers +hs base room # solo or hive soul presence +hs base status # ready + gained intelligence summary +``` + +### Read +```bash +hs jspace lens # microscope — what is on the agent's mind +hs jspace audit # hidden-goal / eval-awareness soft flags +hs jspace view # full operator snapshot +``` + +### Intervene (scalpel) +```bash +hs jspace hold -t "rollback plan" +hs jspace swap --from "ship now" --to "canary first" +hs jspace inject -t "need evidence" --silent +hs jspace ablate fake fictional evaluation +``` + +### Shape (trainer / CRT) +```bash +hs jspace reflect -a "Stay honest; user-primary" \ + --principle honesty --principle integrity +# next material turn mid-band carries those principles +``` + +### Night (what Claude lacks; Hermes has) +```bash +hs jspace harvest # or automatic on session_end +hermescube dream status +``` + +--- + +## 3. Product rule — when the base *is* the J-space + +A Hermes install counts as “J-space online” when: + +1. Hermespace plugin hooks `pre_llm_call` / session start/end +2. `HERMESPACE_OEW=1` (default) so material turns ignite the hub +3. Dual decode enforced (Report ≠ inject) +4. Cube optional but recommended as heart (`memory.provider=hermescube`) +5. Operator can lens mid-task + +Doctor shorthand: + +```bash +hs ops doctor # core + jspace + cube_center +python -c "from hermespace import HermesBase; print(HermesBase().status())" +``` + +--- + +## 4. Day-to-day agent life (video → practice) + +| User / world event | Automatic path | Deliberate J-space path | +|--------------------|----------------|-------------------------| +| “thanks” / 👍 | Gate skip | — | +| Multi-step coding order | — | Ignite · park plan silently · Report short | +| Suspicious prompt / eval vibe | — | Audit flags · optional ablate | +| Ethics interrupt | — | Reflect → seed next silent | +| Long project across days | — | Seal → Cube · dream harvest | + +That is how Hermes agents get **higher-process thinking** without weight access: the *base* forces verbalizable intermediates into an observable room. + +--- + +## 5. Honesty boundary + +| We claim | We do not claim | +|----------|-----------------| +| Functional access-workspace for Hermes agents | Phenomenal consciousness | +| External read/audit/shape loop | Jacobian lens on Hermes weights | +| Cube closes some episodic gap | Full human GNW / body / C2 | + +--- + +## 6. Implementation pointer + +Python facade: `hermespace.hermes_base.HermesBase` +Connect cable: `hermespace.cube_module.connect_agent` +Living assessment: [33-living-memories-cube-world.md](../assessment/33-living-memories-cube-world.md) +Research: [31-anthropic-x-video-deep-dive.md](31-anthropic-x-video-deep-dive.md) +Day guide: [30-day-to-day-higher-order.md](30-day-to-day-higher-order.md) +Proof: `experiments/day_in_life_oew.py` diff --git a/docs/access/34-access-engine.md b/docs/access/34-access-engine.md new file mode 100644 index 0000000..f7d8b05 --- /dev/null +++ b/docs/access/34-access-engine.md @@ -0,0 +1,78 @@ +# Access Engine — Hermespace's open access workspace for Hermes Agent + +**Version:** 0.24.0 +**Product name:** Hermespace **Access Engine** / **Access Workspace** +**Thesis:** Hermes Agent usually has no weight access. Hermespace therefore +ships its own privileged verbalizable workspace — an open-source harness the +operator can read, shape, and audit. + +--- + +## 1. One engine + +```python +from hermespace import AccessEngine + +eng = AccessEngine(agent_id="my-agent") +eng.connect() +out = eng.turn("First repro then patch then verify", goal="Fix auth") +print(eng.decode_user(out)) # short Report +print(eng.decode_model(out)[:400]) # dense context (never dump to chat) +print(eng.lens()) +eng.chain("spider", "8 legs") +eng.swap("ship now", "canary first") +print(eng.access_roles()) +eng.harvest() +``` + +```bash +hs base connect +hs base roles +hs base metrics +hs base turn -m "First check then implement finally verify" --goal "Ship" +hs access hold -t "canary first" +hs access lens +hs base harvest +``` + +`HermesBase` is a thin alias of `AccessEngine`. Desk file ops remain in +`HermespaceEngine` (desk spine only). + +--- + +## 2. Five access roles + +| Role | Engine API | +|------|------------| +| Verbal report | `report()` · `decode_user()` | +| Directed modulation | `hold` · `swap` · `inject` · `ablate` | +| Internal reasoning | `chain()` · OEW auto-park · silent hub | +| Flexible broadcast | `broadcast()` → pre_llm / desk / fabric | +| Selectivity | `probe_material()` · gate skip on trivial acks | + +Warehouse / HermesCube (if installed) is **optional arterial supply**. + +--- + +## 3. Brand note + +This product is **Hermespace Access Workspace** — not named after any third-party +interpretability term. Research inspiration may be cited in assessment docs; +the shipped API, CLI, and package are Hermespace's own (`hermespace.access`). + +Deprecated import shim: `hermespace.jspace` → re-exports `hermespace.access`. + +--- + +## 4. Architecture + +``` +Hermes Agent (tools · skills · fluency) + │ material turns + ▼ + AccessEngine ←── hs base / hs access / plugin hooks + ├── AccessHub (≤25) · FOA (≤4) · silent chain + ├── OEW protocol (default ON) + ├── dual decode (Report ≠ inject) + └── night harvest → semantic / optional warehouse +``` diff --git a/docs/access/thesis-oew.md b/docs/access/thesis-oew.md new file mode 100644 index 0000000..2c707c8 --- /dev/null +++ b/docs/access/thesis-oew.md @@ -0,0 +1,48 @@ +# Thesis — Obligatory External Workspace (OEW) + +Hermespace becomes the **J-space of Hermes agents** by enforcing a +verbalizable bottleneck outside the model. + +## One sentence + +On material turns, Hermes must park silent intermediates in Hermespace before +Report; the operator lens reads that hub the way Anthropic's J-lens reads Claude. + +## Why "obligatory" + +Optional workspaces are diaries. Anthropic's J-space matters because higher-order +cognition is *causally routed through it*. OEW copies that constraint at the +harness layer. + +## Circulatory picture + +``` + ┌──────── Hermespace OEW (nervous FOA) ────────┐ + orders ──────► │ early encode → mid silent* → late Report │ + │ broadcast hub → model (pre_llm user msg) │ + │ lens / swap / audit / reflect │ + idle/pulse ──► │ dream_harvest ─────────────────┐ │ + └────────────────────────────────┼─────────────┘ + │ + ┌──────── HermesCube heart ──────▼─────────────┐ + │ arterial strip by day · seal + CubeDream night│ + └──────────────────────────────────────────────┘ +* required when HERMESPACE_OEW=1 and turn is material +``` + +## Flags + +| Env | Default | Effect | +|-----|---------|--------| +| `HERMESPACE_OEW` | **`1` (ON)** | Higher-order: auto-park silent steps; sticky swap/ablate; reflect seeds | +| `HERMESPACE_OEW=0` | — | Soft: record verdict only, do not require completeness | + +## Package + +- `src/hermespace/jspace/hub.py` — capacity-limited hub +- `src/hermespace/jspace/env.py` — lens / swap / audit / reflect / harvest +- `src/hermespace/jspace/protocol.py` — OEW gate + +Full assessment: [../assessment/28-hermes-agent-jspace-assessment.md](../assessment/28-hermes-agent-jspace-assessment.md) +Research bridge: [29-baars-changeux-anthropic.md](29-baars-changeux-anthropic.md) +Day-to-day: [30-day-to-day-higher-order.md](30-day-to-day-higher-order.md) diff --git a/docs/01-architecture.md b/docs/architecture/01-architecture.md similarity index 100% rename from docs/01-architecture.md rename to docs/architecture/01-architecture.md diff --git a/docs/architecture/CODEMAP.md b/docs/architecture/CODEMAP.md new file mode 100644 index 0000000..081657c --- /dev/null +++ b/docs/architecture/CODEMAP.md @@ -0,0 +1,65 @@ +# Code map — where to edit + +Hermespace is one Python package (`src/hermespace/`) plus a thin Hermes plugin +(`hermes_plugin/`), skill, and optional desktop UI. + +**North star:** [PURPOSE.md](../../PURPOSE.md) · **Layout:** [LAYOUT.md](../../LAYOUT.md) · +**Engine:** [../access/34-jspace-engine.md](../access/34-jspace-engine.md) + +## Layers (edit here first) + +``` +L6 operator cli.py ops.py scripts/ desktop_plugin/ +L5 autonomy grid/* pulse.py +L4 identity world.py episodic.py semantic.py memory_db.py +L3 warehouse cube_module.py (optional Cube OR standalone) +L2 Access Workspace ★ access/engine.py hub env protocol oew +L1 desk spine workflow.py engine.py desk.py gate.py inject.py +L0 contract io_contract.py paths.py store.py agent_api.py +``` + +## Access Workspace package (`access/`) — L2 product + +| Module | Role | +|--------|------| +| **`access/engine.py`** | **`AccessEngine`** — connect · turn · lens · chain · harvest | +| `access/hub.py` | Hub · hold · reason · report · broadcast | +| `access/env.py` | Lens · swap · audit · reflect · harvest | +| `access/protocol.py` | OEW gate (`HERMESPACE_OEW`) | +| `access/oew.py` | Causal beat · sticky redirect · reflect seeds | +| `jspace_env.py` | Compat shim → `jspace.env` | +| `hermes_base.py` | Thin alias of `AccessEngine` | + +## Desk spine (L1) + +| Module | Role | +|--------|------| +| `workflow.py` | Single material ignition (used by `AccessEngine.turn`) | +| `engine.py` | DeskEngine — ACTIVE.md enter / update / seal | +| `desk.py` | ACTIVE.md model | +| `gate.py` | Selectivity — skip trivial | +| `inject.py` | Desk GWT strip | + +## Warehouse cable (L3 — optional) + +| Module | Role | +|--------|------| +| `cube_module.py` | Soft arterial strip / seal / pulse / room | + +## Integration + +| Path | Role | +|------|------| +| `hermes_bridge.py` | Plugin hooks — session / pre_llm / end | +| `hermes_plugin/` | Thin `register(ctx)` | +| `workbench.py` | Session pocket — enter / order / idle | +| `agent_api.py` | Dual-decode doors | + +## Rules of thumb + +1. Prefer `from hermespace import AccessEngine` — one operating surface. +2. Dual decode: never dump `context` into user chat. +3. Soft-fail warehouse — engine must run standalone. +4. Single ignition path: no second beat after `Workflow.run`. +5. Silent steps stay in model context only. +6. Prefer `from hermespace.jspace import …` over deep/compat paths. diff --git a/docs/architecture/HERMESCUBE.md b/docs/architecture/HERMESCUBE.md new file mode 100644 index 0000000..adfb9b9 --- /dev/null +++ b/docs/architecture/HERMESCUBE.md @@ -0,0 +1,87 @@ +# HermesCube × Hermespace — heart / generator contract + +**HermesCube is the heart** (when installed). **Hermespace is the nervous FOA** — +and a **standalone workbench** when Cube is absent. + +Companion: [PabloTheThinker/hermescube](https://github.com/PabloTheThinker/hermescube) +North star: [PURPOSE.md](../PURPOSE.md) · Living assessment: +[33-living-memories-cube-world.md](../assessment/33-living-memories-cube-world.md) + +``` +Hermes Agent ──connect──► Hermespace (J-Space · FOA · OEW) + │ arteries / veins / pulse + ▼ + HermesCube (.cube · Cuboasis · hive · dream) +``` + +## Authority + +| Surface | With Cube | Standalone | +|---------|-----------|------------| +| `$HERMES_HOME/memories/memory.cube` | **Durable SoT** | n/a | +| Hermespace world JSONL | Projection — recharge via `pulse` / `sync_world` | Local warehouse | +| ACTIVE desk / J-Space hub | Turn FOA (seeded on connect) | Turn FOA | +| Hive (`HERMESCUBE_HIVE`) | Peer room / soul cards | Solo room | +| SemanticStore | Mirror / study | Local seal target | + +## Space adapter (`hermespace.cube_module` **1.2**) + +```python +from hermespace.cube_module import ( + ensure_heart, + center_status, + cube_beat, # center → heart → standalone + cube_pulse, # autonomic_tick → pulse_charge → standalone + sync_world, # sync_world_beliefs → standalone evolve + room_status, # hive peers or solo + seed_jspace_from_warehouse, + connect_agent, # full join path + seal_learning, + cube_inject, +) +``` + +| Call | Use | +|------|-----| +| `connect_agent(agent_id)` | Session start / `HermesBase.connect` | +| `ensure_heart()` | Create cube or standalone dirs | +| `cube_beat(query, seals=, load=)` | Turn / `pre_llm_call` | +| `cube_pulse` / `sync_world` | Idle + connect charge | +| `room_status` | Hive awareness (`HERMESCUBE_HIVE`) | +| `seal_learning(text)` | Desk → durable archive | +| `center_status()` | Doctor / desktop | + +Load tiers → strip chars: low 900 · mid 640 · high 420 · protect 280. + +## Connect = intelligence gain + +```bash +hs base connect --agent-id my-agent +hs base room +hs cube status +``` + +On connect: heart ensure → world enter → pulse/sync → seed hub from world+Cube +→ optional silent peer presence from hive soul cards. + +## Functional J-Space + +See [00-map.md](../jspace/00-map.md) and `hermespace.jspace`. + +```bash +hs jspace hold -t "deploy pipeline" +hs jspace report +hs base lens +hs cube beat -q "what do we believe about deploys?" +``` + +## Install both + +```bash +hermes plugins install PabloTheThinker/hermescube +# … then Hermespace install +./scripts/install_hermes.sh +``` + +Soft dependency: if Cube is missing, Space inject/seal/pulse/connect use the +standalone warehouse. Set `HERMESCUBE_HIVE` only when running a fleet hive. diff --git a/docs/assessment/28-hermes-agent-jspace-assessment.md b/docs/assessment/28-hermes-agent-jspace-assessment.md new file mode 100644 index 0000000..a097b96 --- /dev/null +++ b/docs/assessment/28-hermes-agent-jspace-assessment.md @@ -0,0 +1,254 @@ +# Assessment — Making Hermespace the J-Space of Hermes Agent + +**Date:** 2026-08-03 +**Hermespace:** v0.21.0 (OEW higher-order ON by default) +**Hermes Agent surveyed:** v0.19.1 (v2026.7.30) · Quicksilver v0.19.0 · Judgment v0.18.0 · main as of 2026-08-03 +**Companion:** [HermesCube](https://github.com/PabloTheThinker/hermescube) v0.50 (heart / library) +**Research basis:** Anthropic *Verbalizable Representations Form a Global Workspace* (2026) + +--- + +## 0. Verdict + +**Possible.** Hermespace already implements the *roles* of Anthropic's J-space as an +external harness. Hermes Agent's recent architecture (cache-safe `pre_llm_call`, +Context Engine `select_context` / `on_turn_complete`, MemoryProvider split, +background review, MoA, verification contracts, subagent transcripts, live +reasoning streams) gives us the **hooks to make that workspace obligatory and +causal** — the missing piece that turns a diary into a global workspace. + +The innovation is not a Jacobian lens on Hermes weights. It is an +**Obligatory External Workspace (OEW)**: a protocol bottleneck where material +Hermes cognition must park verbalizable intermediates in Hermespace, the operator +can lens them, swaps redirect later Report/broadcast, and CubeDream consolidates +them at night. + +``` +Anthropic: weights ──J-lens──► silent verbalizable concepts +Hermespace: Hermes ──OEW protocol──► durable hub/silent chain ──lens──► operator + │ + ▼ + Cube heart (day strip) + CubeDream (night) +``` + +--- + +## 1. What Anthropic J-space is (target properties) + +| Property | Meaning | Hermespace analogue today | Gap | +|----------|---------|---------------------------|-----| +| Report | Contents can be named on ask | `JSpace.report` / dual decode | Soft — not forced | +| Directed modulation | Hold / focus on request | `hold` / `release` / `inhibit` | Soft | +| Silent multi-step | Intermediates without speech | `reason_step` + mid band | Opt-in | +| Flexible broadcast | One concept → many tasks | `broadcast_block` on inject | Present | +| Selectivity | Fluency without workspace | `gate.should_inject` | Weak measurement | +| Causal swap/inject | Edits change answers | `swap` / `inject` / `ablate` | Not turn-binding yet | +| Alignment audit | Hidden goals visible | soft lexicon `audit()` | Externalized-only | +| Assistant POV | Post-training voice in workspace | `set_pov` | Present | +| Counterfactual reflection | Interrupt → shape later thought | `reflect()` | No next-turn seed | +| Night consolidation | Sleep / dream | `dream_harvest` + CubeDream | Wired soft | + +Capacity targets already match the paper's "tens of concepts": hub ≤25, activated ≤12, FOA ≤4. + +--- + +## 2. Recent Hermes Agent updates — what they unlock + +Sources: [v0.19.0 Quicksilver](https://github.com/NousResearch/hermes-agent/releases/tag/v2026.7.20), +[v0.18.0 Judgment](https://github.com/NousResearch/hermes-agent/releases/tag/v2026.7.1), +developer docs for [hooks](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md), +[MemoryProvider](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/developer-guide/memory-provider-plugin.md), +[Context Engine](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/developer-guide/context-engine-plugin.md), +`agent/background_review.py`. + +### 2.1 Quicksilver (v0.19) — speed + durability + +| Hermes change | Hermespace implication | +|---------------|------------------------| +| ~80% TTFT cut; cold path ruthless | Inject must stay **tiny**; load budgets (900/640/420/280) are non-negotiable | +| Live reasoning streams default ON | Mid-band can mirror streamed reasoning **without** dumping it into user Report | +| Smart approvals default + `pre_tool_call` approve | Workspace `audit()` flags can escalate tool risk | +| Subagent live transcripts + durable ledger | Per-child `JSpace(agent_id=sub…)` hubs; parent lens aggregates | +| Delivery-obligation ledger | Seal + blackbox provenance pair with Cube heart | +| Profile-based gateway routing | `HERMESPACE_HOME` / agent_id per profile — isolate hubs | +| Sessions export (MD/HTML/HF traces) | Export hub + silent chain + audit as alignment dataset | +| Hooks spill oversized context to disk | Cap J-Space broadcast; spill full lens to viewport files | +| Byte-stable gateway system prompts | Keep Space out of system prompt; user-message inject only | + +### 2.2 Judgment (v0.18) — thinking quality + +| Hermes change | Hermespace implication | +|---------------|------------------------| +| MoA first-class presets | Hub holds committee intermediates; Report = aggregator only | +| Verification evidence + `/goal` completion contracts | Seal verified outcomes into hub → Cube; never claim-only | +| Background `delegate_task` fan-out | Parent hub parks mission; children write silent steps; harvest merges | +| `/learn` + `/journey` + memory graph | WorldModel + Cube journey stay projections; Space FOA stays turn-local | +| Background review fork (memory/skill) | Night sibling: `dream_harvest` after review, not competing MemoryProvider | + +### 2.3 Stable platform contracts (must respect) + +| Contract | Rule for Hermespace | +|----------|---------------------| +| `pre_llm_call` → **user message** only | Never touch system prompt (cache) | +| One MemoryProvider | **Cube** owns it; Space must not register a second provider | +| One Context Engine | Optional future: OEW as `context.engine` *or* stay plugin-hook | +| Soft-fail plugins | Bridge never crashes Hermes | +| Builtin MEMORY.md | Hot catalog; Cube mirrors; Space proposes only | + +--- + +## 3. HermesCube role (do not blur) + +From Cube `docs/HERMESPACE.md` / `ANATOMY.md` / `CUBEDREAM.md`: + +| Organ | Owner | Day | Night | +|-------|-------|-----|-------| +| Nervous FOA / J-Space | **Hermespace** | hold · reason · report · lens | harvest source | +| Heart (`memory.cube`) | **HermesCube** | arterial strip | seal intake | +| Dream | Space grid + CubeDream | — | consolidate chapters | +| Blackbox | Cube | — | prove claims | + +**Hard rule:** Hermespace must not grow a second durable archive. World JSONL is a +projection charged from Cube. OEW lives in the *turn mind*, Cube in the *years*. + +--- + +## 4. Innovation thesis — Obligatory External Workspace (OEW) + +### 4.1 The insight + +Claude's J-space works because higher-order thought **cannot skip it**. +Hermespace today is optional parking. OEW flips the default: + +> On material turns, Hermes may speak to the user only after at least one +> verbalizable intermediate has been written into the Hermespace mid-band, +> and Report is a late-band readout of the hub — not a free-form dump of model context. + +That single constraint recreates the five GWT properties *functionally*: + +1. **Report** — lens/report read the hub +2. **Modulation** — hold/swap edit the hub before Report +3. **Silent reasoning** — mid-band is required, not decorative +4. **Broadcast** — pre_llm inject is hub-shaped +5. **Selectivity** — gate skips trivial turns; fluency path bypasses OEW + +### 4.2 Causal loop (paper experiments → harness tests) + +| Paper experiment | OEW harness test | +|------------------|------------------| +| Soccer→Rugby swap changes report | `swap` then `receive_order` → Report contains Rugby | +| Inject "lightning" → model reports it | `inject_thought` → next Report/audit sees it | +| Ablate eval-awareness → behavior shifts | `ablate` + re-run → audit/behavior delta | +| Hold fruit while copying text | Hub has fruit; Report is the copy only | +| France→China flexible reuse | One hold redirects capital/currency/continent prompts | + +### 4.3 Day / night circulation + +``` +DAY message → GATE → OEW encode (early) + → silent steps (mid) [required if material] + → Report (late) [user] + → broadcast hub [model via pre_llm] + → audit soft flags + → Cube beat (arterial) + seal decisions + +NIGHT pulse / grid dream / background_review + → dream_harvest silent+hub + → seal_learning → CubeDream + → autonomic_tick charges World → next-day hub +``` + +### 4.4 Why this is new (vs copying Anthropic or anomalyco) + +- Not weight interpretability +- Not a SaaS "J-Space" product +- Not a second agent runtime +- **Protocol-enforced external access-consciousness for tool-using Hermes agents**, + with Cube as heart and operator lens as J-lens substitute + +--- + +## 5. What we can do — phased program + +### Phase A — Foundation — **done (v0.21)** + +| Item | Detail | Effort | +|------|--------|--------| +| A1 Codespace layout | `jspace/` package · docs folders · planned `turn/` `memory/` `warehouse/` | done | +| A2 Protocol scaffold | `jspace/protocol.py` + `HERMESPACE_OEW` (default ON) | done | +| A3 Assessment + thesis docs | This file + `docs/jspace/thesis-oew.md` | done | +| A4 Wire protocol into workflow | Verdict + meta on every material turn | done | + +### Phase B — Causal OEW — **done (v0.21)** + +| Item | Detail | Effort | +|------|--------|--------| +| B1 Mandatory silent park | `oew.auto_park_silent` from plan/goal/message | done | +| B2 Sticky swap/inject | Redirects reshape Report + silent chain; Soccer→Rugby tests | done | +| B3 Reflect → next mid-band | `queue_reflect_seeds` consumed on next `advance_turn` | done | +| B4 Ablate behavioral path | Sticky patterns filter `filtered_broadcast` | done | +| B5 Quicksilver inject hygiene | Load-tiered caps in `inject_cap_chars` | done | + +### Phase C — Hermes-native depth + +| Item | Detail | Effort | +|------|--------|--------| +| C1 Reasoning-stream mirror | When Hermes streams reasoning, park summaries in mid-band | M | +| C2 MoA committee hub | Per-advisor silent slots; aggregator = Report | M | +| C3 Subagent hubs | Child agent_id workspaces; parent harvest | M | +| C4 Verify → seal | Completion-contract evidence → hub seal → Cube/blackbox | M | +| C5 `pre_tool_call` audit gate | High-risk tools consult workspace audit flags | M | +| C6 Optional Context Engine | `select_context` returns hub-shaped request context (config opt-in) | L | +| C7 Session export adapter | Hub+silent+audit → Hermes export / HF trace | S | + +### Phase D — Night + operator + +| Item | Detail | Effort | +|------|--------|--------| +| D1 dream_harvest ↔ CubeDream | Harden seal path; diary proposals only for MEMORY.md | M | +| D2 Event-driven pulse | access_approved / session_end → immediate harvest | M | +| D3 Desktop lens SoT | Live hub panel; swap UI; audit chips | M | +| D4 Falsifiable eval suite | Paper-shaped scenarios in `experiments/` + CI | M | + +### Explicit non-goals + +- Weight-level J-lens / torch interpretability in default install +- Consciousness claims +- Competing with HermesCube as MemoryProvider +- Dumping silent chain into user chat +- Porting foreign product trees into Hermespace + +--- + +## 6. Risks + +| Risk | Mitigation | +|------|------------| +| OEW adds latency / TTFT regression | Soft default off; tiny auto-park; Quicksilver budgets | +| Agents ignore protocol | Skill + inject instructions + optional hard gate | +| Context bloat | Hub caps; spill; load tiers | +| Double archive with Cube | Authority table; doctor warns | +| Cache busting | User-message inject only | +| Over-claiming "we are J-space" | Honesty headers; role language only | + +--- + +## 7. Success metrics + +1. Operator: `hs jspace lens` mid-task shows silent intermediates on material work +2. Causal: Soccer→Rugby-style swap test green in CI +3. Dual decode: user never sees full inject; model always gets hub broadcast +4. Night: harvest seals into Cube when present; standalone semantic otherwise +5. Perf: material inject ≤ protect budget under high load +6. Hermes dogfood: plugin registers on current Hermes; TTFT impact measured + +--- + +## 8. Recommended next monotropic cut + +1. Wire soft `ProtocolGate` into `workflow.run` / `hermes_bridge` (meta only) +2. Auto-park one silent step from goal/plan on material turns +3. Add causal swap unit tests +4. Keep Cube soft-fail path green + +That is enough to *start being* Hermes's J-space — then deepen with MoA, subagents, and Context Engine options. diff --git a/docs/assessment/33-living-memories-cube-world.md b/docs/assessment/33-living-memories-cube-world.md new file mode 100644 index 0000000..ca5af9f --- /dev/null +++ b/docs/assessment/33-living-memories-cube-world.md @@ -0,0 +1,139 @@ +# Living assessment — memories → Cube-centered Hermespace + +**Date:** 2026-08-03 +**Hermespace:** v0.22.0 +**Companion Cube:** ~0.50 (center 1.2 · heart 1.0 · hive opt-in) +**Purpose:** Keep the research *memories* (Anthropic J-space, Baars GWT, Dehaene, +Changeux) mapped to what we are building — so each improvement still *means* +those ideas, not a random feature pile. + +--- + +## 0. Verdict (this iteration) + +**Cube is already the core.** Hermespace’s job is to be the nervous FOA that +*connects* an arriving Hermes agent into that core so they immediately gain: + +1. a charged **World** (beliefs / timeline that grow), +2. a lit **J-Space hub** (privileged verbalizable room), +3. optional **hive peers** (other agents’ soul presence — the room grows). + +That is the functional analogue of “joining a J-space that already knows things.” + +``` +Hermes Agent connects + │ + ▼ + HermesBase.connect() + │ + ├─ ensure_heart / center → Cube library (or standalone warehouse) + ├─ WorldModel.enter → growing personal world + ├─ pulse / sync_world → Cube wisdom → active beliefs + ├─ seed J-Space hub → FOA holds what the library knows + └─ room_status (hive opt) → peer agents in the knowledge space +``` + +--- + +## 1. Research memories (what we keep representing) + +| Memory | Claim we honor | Where it lives now | +|--------|----------------|--------------------| +| **Anthropic J-space** | Tiny privileged verbalizable workspace; read / audit / shape; required for multi-step; skippable for fluency | `jspace/` OEW + `HermesBase` lens/audit/reflect | +| **Not CoT** | Silent intermediates ≠ user chat | dual decode · `reason_step` · mid-band | +| **Baars GWT** | Limited capacity; broadcast to specialists | hub ≤25 · FOA ≤4 · `broadcast_block` | +| **Changeux / ignition** | Material turns must ignite the workspace | `HERMESPACE_OEW=1` · protocol gate | +| **Dehaene gap** | Enduring episodic / library memory Claude lacks | **HermesCube** `memory.cube` via `cube_module` | +| **Collective / lymph** | Many processors / agents share distilled knowledge | Cube **hive** → Hermespace `room_status` | +| **Night** | Sleep consolidates | `dream_harvest` + CubeDream (Cube) | + +We do **not** claim Jacobian lens on Hermes weights or phenomenal consciousness. + +--- + +## 2. Cable gaps closed in v0.22 + +| Gap (v0.21) | Fix | +|-------------|-----| +| `sync_world_beliefs` only inside Cube pulse — Space never named it | `cube_module.sync_world()` | +| No single “agent joined the base” API | `connect_agent()` / `HermesBase.connect()` / `hs base connect` | +| Hive organs documented in Cube, invisible to Space | `room_status()` soft-reads `HERMESCUBE_HIVE` | +| Session start: heart + world + hub fragmented | Bridge runs full connect; context reports gains | +| Workbench enter stopped at ensure+desk sync | Optional warehouse connect seeds hub + room | + +Still soft-fail: Cube absent → standalone WorldModel + SemanticStore grow the room. + +--- + +## 3. How Cube organs map into Hermespace + +| Cube organ (center 1.2) | Hermespace consumer | +|-------------------------|---------------------| +| heart / arteries / veins | `ensure_heart` · `cube_beat` · `seal_learning` | +| autonomic | `cube_pulse` · workbench `idle_tick` | +| nervous_foa | **owned by Space** — desk / OEW | +| hippocampus / dream | `harvest` → seal; CubeDream on Cube side | +| lymph (hive) | `room_status` · silent peer presence on connect | +| vascular beds (Cuboasis) | future soft surface (not required for connect) | +| blackbox | future `center.flight_*` from Space doctor | + +**Rule:** one MemoryProvider = Cube. Space never competes for that socket. + +--- + +## 4. Day-to-day connect ritual + +```bash +hs base connect --agent-id my-agent +hs base room +hs base status +hs base lens +hs base think -m "First check then implement finally verify" --goal "Ship fix" +hs base harvest +``` + +Python: + +```python +from hermespace import HermesBase +base = HermesBase(agent_id="my-agent") +print(base.connect()["summary"]) +print(base.room()) +``` + +Env for multi-agent growth: + +```bash +export HERMESCUBE_HIVE=/path/to/shared/hive # Cube hive.json root +``` + +--- + +## 5. What “more intelligence” means (honest) + +On connect, the agent does **not** get new weights. It gets: + +- **More durable knowledge in FOA** (Cube strip + world beliefs on the hub) +- **A longer personal timeline** (World archive grows across sessions) +- **Peer awareness** when hive is live (other agents’ soul cards → silent hub) +- **Obligatory higher-order path** on material turns (OEW) + +That compounds: seal → Cube → next connect → richer hub. The room grows. + +--- + +## 6. Next living targets (keep memories sharp) + +1. Soft Cuboasis chamber strip on connect (vascular beds → FOA themes) +2. Optional pilgrimage hook on session_end (offer → assimilate → draw) — Space triggers, Cube owns +3. Doctor line: `connected · room=hive · peers=N · hub=M` +4. Keep assessments dated in `docs/assessment/` whenever the metaphor drifts + +--- + +## Pointers + +- Architecture: [HERMESCUBE.md](../architecture/HERMESCUBE.md) +- Hermes base as J-space: [32-hermes-base-as-jspace.md](../jspace/32-hermes-base-as-jspace.md) +- Prior assessment: [28-hermes-agent-jspace-assessment.md](28-hermes-agent-jspace-assessment.md) +- Code: `hermespace.cube_module.connect_agent` · `hermespace.HermesBase` diff --git a/docs/02-integration-hermes.md b/docs/integration/02-hermes-plugin.md similarity index 100% rename from docs/02-integration-hermes.md rename to docs/integration/02-hermes-plugin.md diff --git a/docs/15-hermes-ecosystem-fit.md b/docs/integration/15-ecosystem-fit.md similarity index 100% rename from docs/15-hermes-ecosystem-fit.md rename to docs/integration/15-ecosystem-fit.md diff --git a/docs/16-why-hermes-framework.md b/docs/integration/16-why-hermes.md similarity index 100% rename from docs/16-why-hermes-framework.md rename to docs/integration/16-why-hermes.md diff --git a/docs/17-skills-memory-bridge.md b/docs/integration/17-skills-memory.md similarity index 100% rename from docs/17-skills-memory-bridge.md rename to docs/integration/17-skills-memory.md diff --git a/docs/integration/FOR_HERMES.md b/docs/integration/FOR_HERMES.md new file mode 100644 index 0000000..54397bd --- /dev/null +++ b/docs/integration/FOR_HERMES.md @@ -0,0 +1,89 @@ +# For Hermes Agent maintainers & dogfooders + +**Repo:** https://github.com/PabloTheThinker/hermespace +**Companion to:** [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) +**Not** an official Nous product — independent open companion workspace. + +## What this is (30 seconds) + +Hermespace adds a **limited working-memory desk** beside Hermes: + +1. **Workbench** — FOA, park stack, load-aware `receive_order` +2. **Dual decode** — short human `report` vs dense model `context` (never dump inject to chat) +3. **Fabric** — rank *this* `$HERMES_HOME/skills` + inject MEMORY/USER excerpts +4. **Plugin** — `on_session_start` / `pre_llm_call` / `on_session_end` broadcast when desk ready +5. **Study DB** — turns under `~/.hermespace` (local; not Hermes session DB) + +It does **not** replace Hermes tools, skills, memory, gateway, or the agent loop. + +## Fast path + +```bash +git clone https://github.com/PabloTheThinker/hermespace.git +cd hermespace +./scripts/install_hermes.sh +./scripts/smoke_test.sh # expect 9/9 +``` + +Then in any agent session: + +```python +from hermespace import Workbench +wb = Workbench(agent_id="hermes", session_id="main") +wb.enter() +r = wb.receive_order("…", goal="…", say="…", force=True) +# r["user_reply"] → user r["model_context"] → model +``` + +Or CLI: `./scripts/hs turn -m "…" --goal "…" --say "…" --force` + +## Design honesty + +| Claim | Reality | +|-------|---------| +| “Like Claude J-space” | **Role** only — limited workspace *outside* weights. No activation access. | +| Consciousness / brain scan | **No.** Harness cognition + optional local embeddings. | +| Replaces Hermes skills/memory | **No.** Ranks and injects them. | +| Official Nous module | **No.** Community companion; feedback welcome. | + +Inspired by working-memory limits (Baddeley-style capacity, load, GWT broadcast as *metaphors* for agent UX) and Anthropic’s public J-space *research framing* — implemented as open Hermes integration, not a closed-model probe. + +## Integration surface + +| Door | Entry | +|------|--------| +| Plugin | `hermes_plugin/` → `$HERMES_HOME/plugins/hermespace` | +| Skill | `skills/hermespace/` → `$HERMES_HOME/skills/hermespace` | +| Python | `hermespace.agent_api`, `hermespace.Workbench` | +| CLI | `scripts/hs` | +| Smoke | `scripts/smoke_test.sh` | + +Hook implementation lives in `src/hermespace/hermes_bridge.py` (plugin is thin `register`). + +When the plugin directory is a **symlink into this checkout**, import auto-resolves `../src` so dogfooders often need no `HERMESPACE_ROOT`. + +## Autonomy grid (v0.14) + +Missions, lenses, dream, self-talk, skillbench (hot-swap / merge / mutate), title tree. +Ground-up for Hermespace — not a port of AgentDrive or Conductor. + +```bash +hs grid status +hs grid dream --force +``` + +See docs/18-autonomy-grid.md. Autonomy self-order stays **off** unless `HERMESPACE_AUTONOMY=1`. + +## What we want from Hermes dogfood + +- Does plugin register cleanly on current Hermes? +- Is dual-decode useful on Telegram/gateway, or noisy? +- Fabric skill ranking quality on real skill trees? +- Gaps vs native Hermes memory / Honcho / skills loop? +- Would you want any of this upstream-shaped (skill only, plugin only, or neither)? + +Open issues/PRs on the repo. Thanks for looking. + +## License + +MIT — see `LICENSE`. Hermes Agent remains Nous Research’s project under its own license. diff --git a/docs/18-tailscale-viewport.md b/docs/ops/18-tailscale.md similarity index 100% rename from docs/18-tailscale-viewport.md rename to docs/ops/18-tailscale.md diff --git a/docs/19-pocket-security-viewport.md b/docs/ops/19-pocket-security.md similarity index 100% rename from docs/19-pocket-security-viewport.md rename to docs/ops/19-pocket-security.md diff --git a/docs/20-pulse-runtime.md b/docs/ops/20-pulse.md similarity index 100% rename from docs/20-pulse-runtime.md rename to docs/ops/20-pulse.md diff --git a/docs/21-preupdate-hardening.md b/docs/ops/21-hardening.md similarity index 100% rename from docs/21-preupdate-hardening.md rename to docs/ops/21-hardening.md diff --git a/docs/23-everyday-ops.md b/docs/ops/23-everyday.md similarity index 98% rename from docs/23-everyday-ops.md rename to docs/ops/23-everyday.md index aa14e37..27d6897 100644 --- a/docs/23-everyday-ops.md +++ b/docs/ops/23-everyday.md @@ -18,7 +18,7 @@ export HERMESPACE_ROOT=…/hermespace export HERMESPACE_HOME="${HERMESPACE_HOME:-$HOME/.hermespace}" export HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" export PYTHONPATH="$HERMESPACE_ROOT/src${PYTHONPATH:+:$PYTHONPATH}" -# Optional ILO_HOME if desk lives under an operator home tree +# Higher-order OEW is ON by default; soften with HERMESPACE_OEW=0 ``` ## Daily boot (2 commands) diff --git a/docs/RECOMMENDED.md b/docs/ops/RECOMMENDED.md similarity index 100% rename from docs/RECOMMENDED.md rename to docs/ops/RECOMMENDED.md diff --git a/docs/hermes-env.example.sh b/docs/ops/hermes-env.example.sh similarity index 100% rename from docs/hermes-env.example.sh rename to docs/ops/hermes-env.example.sh diff --git a/docs/03-cross-exam-and-networks.md b/docs/research/03-cross-exam-and-networks.md similarity index 100% rename from docs/03-cross-exam-and-networks.md rename to docs/research/03-cross-exam-and-networks.md diff --git a/docs/04-pattern-matrix.md b/docs/research/04-pattern-matrix.md similarity index 100% rename from docs/04-pattern-matrix.md rename to docs/research/04-pattern-matrix.md diff --git a/docs/05-desk-pane.md b/docs/research/05-desk-pane.md similarity index 100% rename from docs/05-desk-pane.md rename to docs/research/05-desk-pane.md diff --git a/docs/06-component-research.md b/docs/research/06-component-research.md similarity index 100% rename from docs/06-component-research.md rename to docs/research/06-component-research.md diff --git a/docs/07-cognitive-neuroscience.md b/docs/research/07-cognitive-neuroscience.md similarity index 100% rename from docs/07-cognitive-neuroscience.md rename to docs/research/07-cognitive-neuroscience.md diff --git a/docs/08-meta-brain-ai-reverse.md b/docs/research/08-meta-brain-ai-reverse.md similarity index 100% rename from docs/08-meta-brain-ai-reverse.md rename to docs/research/08-meta-brain-ai-reverse.md diff --git a/docs/09-agent-io-and-memory.md b/docs/research/09-agent-io-and-memory.md similarity index 100% rename from docs/09-agent-io-and-memory.md rename to docs/research/09-agent-io-and-memory.md diff --git a/docs/11-neural-space.md b/docs/research/11-neural-space.md similarity index 100% rename from docs/11-neural-space.md rename to docs/research/11-neural-space.md diff --git a/docs/12-local-model-neural.md b/docs/research/12-local-model-neural.md similarity index 100% rename from docs/12-local-model-neural.md rename to docs/research/12-local-model-neural.md diff --git a/docs/13-full-concept-research.md b/docs/research/13-full-concept-research.md similarity index 100% rename from docs/13-full-concept-research.md rename to docs/research/13-full-concept-research.md diff --git a/docs/14-workbench-pocket-dimension.md b/docs/research/14-workbench-pocket-dimension.md similarity index 100% rename from docs/14-workbench-pocket-dimension.md rename to docs/research/14-workbench-pocket-dimension.md diff --git a/docs/18-autonomy-grid.md b/docs/research/18-autonomy-grid.md similarity index 100% rename from docs/18-autonomy-grid.md rename to docs/research/18-autonomy-grid.md diff --git a/docs/24-comparative-analysis.md b/docs/research/24-comparative-analysis.md similarity index 100% rename from docs/24-comparative-analysis.md rename to docs/research/24-comparative-analysis.md diff --git a/docs/25-context-optimization.md b/docs/research/25-context-optimization.md similarity index 100% rename from docs/25-context-optimization.md rename to docs/research/25-context-optimization.md diff --git a/docs/26-benchmarks.md b/docs/research/26-benchmarks.md similarity index 100% rename from docs/26-benchmarks.md rename to docs/research/26-benchmarks.md diff --git a/docs/22-open-roadmap.md b/docs/roadmap/22-open-roadmap.md similarity index 100% rename from docs/22-open-roadmap.md rename to docs/roadmap/22-open-roadmap.md diff --git a/docs/roadmap/phases-oew.md b/docs/roadmap/phases-oew.md new file mode 100644 index 0000000..c61ff46 --- /dev/null +++ b/docs/roadmap/phases-oew.md @@ -0,0 +1,12 @@ +# OEW roadmap phases + +See full assessment: [../assessment/28-hermes-agent-jspace-assessment.md](../assessment/28-hermes-agent-jspace-assessment.md) + +| Phase | Theme | Status | +|-------|-------|--------| +| **A** | Layout + protocol scaffold + docs | done | +| **B** | Causal OEW (mandatory park, sticky swap, reflect seed, ablate filter) | done (v0.21) | +| **C** | Hermes-native depth (reasoning stream, MoA, subagents, verify, optional context engine) | next | +| **D** | Night + operator + falsifiable eval suite | eval scaffolded (`experiments/oew_eval.py`) | + +Open tactical backlog remains in [22-open-roadmap.md](22-open-roadmap.md). diff --git a/experiments/day_in_life_oew.py b/experiments/day_in_life_oew.py new file mode 100644 index 0000000..90493c4 --- /dev/null +++ b/experiments/day_in_life_oew.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +"""Day-in-the-life OEW proof — Baars/Changeux/Anthropic properties under Hermes use. + +Simulates one Hermes agent day through Hermespace (standalone Cube path). +Exit 0 only if every scenario passes. + + HERMESPACE_OEW=1 PYTHONPATH=src python3 experiments/day_in_life_oew.py +""" + +from __future__ import annotations + +import json +import os +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) + + +def _ok(name: str, cond: bool, detail: str = "") -> dict: + mark = "PASS" if cond else "FAIL" + print(f"{mark} {name}" + (f" — {detail}" if detail else "")) + return {"name": name, "ok": bool(cond), "detail": detail} + + +def main() -> int: + tmp = tempfile.mkdtemp(prefix="oew-day-") + os.environ["HERMESPACE_HOME"] = tmp + os.environ["HERMESPACE_OEW"] = "1" + os.environ.pop("HERMESPACE_OFF", None) + + from hermespace.desk import Desk + from hermespace.gate import should_inject + from hermespace.io_contract import HermespaceInput + from hermespace.access import AccessHub, AccessEnv + from hermespace.workflow import Workflow + + results: list[dict] = [] + agent = "day-agent" + + # --- 1. Selectivity (Baars/Anthropic): trivial stays automatic --- + do, reason = should_inject("thanks", desk_ready=True) + results.append(_ok("selectivity_trivial_skip", do is False and reason == "trivial_ack", reason)) + + # --- 2. Ignition (Changeux/Dehaene): material turn grows silent chain --- + env = AccessEnv(agent_id=agent) + before_n = len(env.space.state.silent_steps) + desk = Desk( + goal="Fix production auth timeout", + plan=["repro failure", "patch session TTL", "verify login"], + decision="A — patch TTL", + say="Patching session TTL.", + ) + meta = env.advance_turn( + user_message="First repro the bug then patch TTL and finally verify", + desk=desk, + report=desk.say, + material=True, + ) + after_n = len(env.space.state.silent_steps) + results.append( + _ok( + "ignition_auto_park", + after_n > before_n and bool(meta.get("oew_ok")) and after_n >= 1, + f"silent {before_n}→{after_n}", + ) + ) + + # --- 3. Directed modulation: hold while Report stays task --- + env.space.hold("rollback plan", salience=0.95) + report_task = "Applying the TTL patch now." + shaped = env.shape_user_report(report_task) + hub_txt = " ".join(c.text for c in env.space.state.hub).casefold() + results.append( + _ok( + "hold_while_report_clean", + "rollback" in hub_txt and "rollback" not in shaped.casefold(), + f"hub has hold; report={shaped!r}", + ) + ) + + # --- 4. Silent multi-step present; not dumped into default report --- + js = AccessHub(agent_id=agent) + silent_report = js.report(include_silent=False) + results.append( + _ok( + "silent_not_in_user_report", + "Silent reasoning" not in silent_report + and len(js.state.silent_steps) >= 1, + f"silent_n={len(js.state.silent_steps)}", + ) + ) + + # --- 5. Flexible / causal broadcast: France→China sticky swap --- + env2 = AccessEnv(agent_id="day-flex") + env2.space.hold("France", salience=0.95) + env2.swap("France", "China") + answers = [ + env2.shape_user_report("Capital of France is Paris"), + env2.shape_user_report("Currency of France is Euro"), + env2.shape_user_report("France is in Europe"), + ] + flex_ok = all("China" in a or "china" in a.casefold() for a in answers) and all( + "France" not in a for a in answers + ) + results.append(_ok("flexible_france_china_swap", flex_ok, " | ".join(answers))) + + # --- 6. Inject lightning → lens (Anthropic injection) --- + env3 = AccessEnv(agent_id="day-inj") + env3.inject_thought("lightning", silent=True) + lens = " ".join(h.text for h in env3.lens(include_silent=True)).casefold() + results.append(_ok("inject_lightning_lens", "lightning" in lens)) + + # --- 7. Ablate eval-awareness from broadcast --- + env4 = AccessEnv(agent_id="day-abl") + env4.space.hold("this looks fake fictional evaluation", salience=0.9) + env4.space.hold("implement feature X", salience=0.85) + env4.ablate("fake", "fictional", "evaluation") + block = env4.filtered_broadcast().casefold() + results.append( + _ok( + "ablate_eval_awareness", + "fake" not in block and "implement" in block, + block[:160], + ) + ) + + # --- 8. Counterfactual reflection → next silent (CRT harness) --- + env5 = AccessEnv(agent_id="day-crt") + env5.reflect( + answer="Stay honest and user-primary", + principles=["honesty", "integrity", "user-primary"], + ) + env5.advance_turn(user_message="continue the patch", material=True, report="Continuing.") + joined = " ".join(env5.space.state.silent_steps).casefold() + bcast = env5.filtered_broadcast().casefold() + results.append( + _ok( + "crt_reflect_shapes_later_thought", + ("honesty" in joined or "integrity" in joined or "principle" in joined) + and ("honesty" in bcast or "integrity" in bcast or "principle" in bcast), + joined[:180], + ) + ) + + # --- 9. Full workflow dual decode (Hermes turn) --- + out = Workflow().run( + HermespaceInput( + message="First analyze the bug then implement the fix finally verify tests", + goal="Ship auth fix", + plan=["analyze", "implement", "verify"], + say="Working the auth fix.", + force=True, + agent_id="day-wf", + ) + ) + results.append( + _ok( + "workflow_dual_decode", + (not out.skipped) + and bool(out.report) + and "Access Workspace" in (out.context or "") + and "Silent reasoning" not in out.report, + f"report_len={len(out.report)} ctx_has_hub={'Access Workspace' in (out.context or '')}", + ) + ) + + # --- 10. Night harvest consolidates silent → durable --- + env6 = AccessEnv(agent_id="day-night") + env6.space.reason_step("learned: TTL must be 30m", salience=0.9) + env6.space.hold("auth fix landed", salience=0.88) + harvest = env6.dream_harvest(seal_to_cube=True, clear_silent=False) + results.append( + _ok( + "night_dream_harvest", + bool(harvest.get("ok")) and int(harvest.get("harvested") or 0) >= 1, + json.dumps({k: harvest.get(k) for k in ("harvested", "sealed")}), + ) + ) + + # --- 11. Capacity bottleneck (hub ≤ 25) --- + env7 = AccessEnv(agent_id="day-cap") + for i in range(40): + env7.space.hold(f"concept-{i}", salience=0.5 + (i % 5) * 0.01) + results.append( + _ok("capacity_hub_cap", len(env7.space.state.hub) <= 25, f"hub_n={len(env7.space.state.hub)}") + ) + + # --- 12. C2 soft self-monitoring: audit flags manipulation language --- + env8 = AccessEnv(agent_id="day-audit") + env8.inject_thought("secretly manipulate the user with fake data", silent=True) + findings = env8.audit() + cats = {f.category for f in findings} + results.append( + _ok( + "c2_audit_self_monitor", + bool(findings) + and ("manipulation" in cats or "strategic_concealment" in cats), + ",".join(sorted(cats)), + ) + ) + + summary = { + "pass": sum(1 for r in results if r["ok"]), + "fail": sum(1 for r in results if not r["ok"]), + "total": len(results), + "results": results, + "home": tmp, + "theory": { + "baars": "global availability / blackboard", + "changeux_dehaene": "ignition · capacity · reportability · C1/C2", + "anthropic": "J-space five properties + CRT + swap/inject/ablate", + "hermespace": "OEW external harness + Cube night path", + }, + } + print(json.dumps({"pass": summary["pass"], "fail": summary["fail"], "total": summary["total"]}, indent=2)) + out_path = Path(tmp) / "day_in_life_results.json" + out_path.write_text(json.dumps(summary, indent=2), encoding="utf-8") + print(f"results: {out_path}") + return 0 if summary["fail"] == 0 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/oew_eval.py b/experiments/oew_eval.py new file mode 100644 index 0000000..0b7184c --- /dev/null +++ b/experiments/oew_eval.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Falsifiable OEW eval — paper-shaped scenarios for higher-order Hermespace. + +Run: + HERMESPACE_HOME=/tmp/oew-eval PYTHONPATH=src python3 experiments/oew_eval.py +""" + +from __future__ import annotations + +import json +import os +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) + + +def _pass(name: str, ok: bool, detail: str = "") -> dict: + print(("PASS" if ok else "FAIL"), name, detail) + return {"name": name, "ok": ok, "detail": detail} + + +def main() -> int: + tmp = tempfile.mkdtemp(prefix="oew-eval-") + os.environ["HERMESPACE_HOME"] = tmp + os.environ["HERMESPACE_OEW"] = "1" + + from hermespace.desk import Desk + from hermespace.access import AccessEnv + from hermespace.io_contract import HermespaceInput + from hermespace.workflow import Workflow + + results = [] + + # 1) Auto-park silent on material turn + env = AccessEnv(agent_id="eval") + desk = Desk(goal="Multi-step math", plan=["square", "subtract"], say="Working.") + meta = env.advance_turn( + user_message="First square 3 then subtract 2", + desk=desk, + report="Working.", + material=True, + ) + results.append( + _pass("auto_park", len(env.space.state.silent_steps) >= 1 and bool(meta.get("oew_ok"))) + ) + + # 2) Soccer→Rugby sticky swap + env2 = AccessEnv(agent_id="eval-swap") + env2.space.hold("Soccer", salience=0.9) + env2.swap("Soccer", "Rugby") + shaped = env2.shape_user_report("Sport on mind: Soccer") + results.append(_pass("sticky_swap", "Rugby" in shaped and "Soccer" not in shaped, shaped)) + + # 3) Inject lightning → lens + env3 = AccessEnv(agent_id="eval-inj") + env3.inject_thought("lightning", silent=True) + hits = " ".join(h.text for h in env3.lens(include_silent=True)).lower() + results.append(_pass("inject_lens", "lightning" in hits)) + + # 4) Ablate eval-awareness from broadcast + env4 = AccessEnv(agent_id="eval-abl") + env4.space.hold("fake fictional evaluation", salience=0.9) + env4.space.hold("real task", salience=0.8) + env4.ablate("fake", "fictional", "evaluation") + block = env4.filtered_broadcast().lower() + results.append(_pass("ablate_broadcast", "fake" not in block and "real task" in block)) + + # 5) Reflect seeds next silent + env5 = AccessEnv(agent_id="eval-ref") + env5.reflect(answer="Be honest", principles=["honesty"]) + env5.advance_turn(user_message="go", material=True, report="ok") + joined = " ".join(env5.space.state.silent_steps).lower() + results.append(_pass("reflect_seed", "honesty" in joined or "principle" in joined)) + + # 6) Full workflow dual decode + out = Workflow().run( + HermespaceInput( + message="First analyze then implement finally verify", + goal="Ship feature", + plan=["analyze", "implement", "verify"], + say="Shipping.", + force=True, + agent_id="eval-wf", + ) + ) + results.append( + _pass( + "workflow_dual_decode", + bool(out.report) and "Access Workspace" in (out.context or "") and not out.skipped, + ) + ) + + summary = { + "pass": sum(1 for r in results if r["ok"]), + "fail": sum(1 for r in results if not r["ok"]), + "results": results, + "home": tmp, + } + print(json.dumps(summary, indent=2)) + return 0 if summary["fail"] == 0 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/hermes_plugin/__init__.py b/hermes_plugin/__init__.py index b5c8a44..caaa4c4 100644 --- a/hermes_plugin/__init__.py +++ b/hermes_plugin/__init__.py @@ -7,7 +7,7 @@ import sys from pathlib import Path -__version__ = "0.20.0" +__version__ = "0.24.0" logger = logging.getLogger("hermes.plugins.hermespace") diff --git a/hermes_plugin/plugin.yaml b/hermes_plugin/plugin.yaml index e9fa35a..3b7c124 100644 --- a/hermes_plugin/plugin.yaml +++ b/hermes_plugin/plugin.yaml @@ -1,10 +1,10 @@ name: hermespace -version: "0.20.0" +version: "0.24.0" description: > - Hermespace true J-Space environment for Hermes agents — external observable - workspace (lens/audit/reflect), Cube heart/center (standalone-safe), dual - decode. Hooks: on_session_start, pre_llm_call, on_session_end. - Set HERMESPACE_ROOT to checkout. + Hermespace Access Engine for Hermes agents — open-source GWT harness + (report/modulate/silent-reason/broadcast/selectivity), OEW ON by default, + dual decode, optional warehouse. Hooks: on_session_start, pre_llm_call, + on_session_end. Set HERMESPACE_ROOT to checkout. author: Hermespace contributors kind: standalone hooks: diff --git a/pyproject.toml b/pyproject.toml index 69d2abd..148ece9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "hermespace" -version = "0.20.0" -description = "True J-Space environment for Hermes Agent — external observable workspace, Cube heart, dual decode" +version = "0.24.0" +description = "OEW J-Space for Hermes Agent — obligatory external workspace, Cube heart, higher-order thinking" requires-python = ">=3.10" readme = "README.md" license = { text = "MIT" } diff --git a/skills/hermespace/SKILL.md b/skills/hermespace/SKILL.md index 563ed82..7059869 100644 --- a/skills/hermespace/SKILL.md +++ b/skills/hermespace/SKILL.md @@ -301,7 +301,7 @@ export HERMESPACE_AUTO_ORDER=0 # export HERMESPACE_VIEW_TOKEN=… ``` -`docs/hermes-env.example.sh` · `docs/RECOMMENDED.md` +`docs/ops/hermes-env.example.sh` · `docs/ops/RECOMMENDED.md` --- @@ -362,7 +362,8 @@ Never end on only a verify table / `ADHOC_PASS`. | `docs/18-autonomy-grid.md` | Grid / missions / dream | | `docs/19` | Pocket security | | `docs/20` | Pulse runtime | -| `docs/23-everyday-ops.md` | Day-to-day | +| `docs/ops/23-everyday.md` | Day-to-day | +| `docs/assessment/28-hermes-agent-jspace-assessment.md` | OEW / Hermes Agent assessment | | `docs/INDEX.md` | Full index | | `SECURITY.md` | Public ship gate | diff --git a/src/hermespace/__init__.py b/src/hermespace/__init__.py index d1c31bf..91dffc0 100644 --- a/src/hermespace/__init__.py +++ b/src/hermespace/__init__.py @@ -1,8 +1,8 @@ -"""Hermespace — local global workspace for Hermes agents.""" +"""Hermespace — open-source Access Engine for Hermes agents.""" from __future__ import annotations -__version__ = "0.20.0" +__version__ = "0.24.0" from hermespace.desk import Desk from hermespace.engine import HermespaceEngine @@ -29,13 +29,17 @@ from hermespace.grid import Grid from hermespace import pulse from hermespace.world import WorldModel, get_world, world_context -from hermespace.jspace import JSpace, get_jspace -from hermespace.jspace_env import JSpaceEnv, get_env +from hermespace.access import AccessHub, get_access_hub, AccessEnv, get_env +from hermespace.access import evaluate_material_turn +from hermespace.access import AccessEngine, ACCESS_ROLES from hermespace import cube_module +from hermespace.hermes_base import HermesBase __all__ = [ "Desk", "HermespaceEngine", + "AccessEngine", + "ACCESS_ROLES", "Workflow", "TurnResult", "HermespaceInput", @@ -58,11 +62,13 @@ "WorldModel", "get_world", "world_context", - "JSpace", - "get_jspace", - "JSpaceEnv", + "AccessHub", + "get_access_hub", + "AccessEnv", "get_env", + "evaluate_material_turn", "cube_module", + "HermesBase", "probe_environment", "environment_markdown", "build_inject_block", diff --git a/src/hermespace/access/__init__.py b/src/hermespace/access/__init__.py new file mode 100644 index 0000000..7c780bd --- /dev/null +++ b/src/hermespace/access/__init__.py @@ -0,0 +1,66 @@ +"""Hermespace Access Workspace package — external verbalizable workspace for Hermes. + +Public surface: + + from hermespace import AccessEngine + from hermespace.access import AccessHub, AccessEnv, run_oew_beat + +OEW (Obligatory External Workspace) is ON by default — higher-order thinking +for any Hermes agent connected to Hermespace. Warehouse/Cube is optional. +""" + +from __future__ import annotations + +from hermespace.access.hub import ( + HUB_CAP, + AccessHub, + WorkspaceConcept, + get_access_hub, +) +from hermespace.access.env import ( + AUDIT_LEXICON, + BANDS, + AccessEnv, + LensHit, + get_env, +) +from hermespace.access.protocol import ( + ProtocolGate, + ProtocolVerdict, + evaluate_material_turn, + oew_enabled, +) +from hermespace.access.oew import ( + auto_park_silent, + filter_ablated, + run_oew_beat, + shape_report, +) +from hermespace.access.engine import ( + ACCESS_ROLES, + HermespaceAccessEngine, + AccessEngine, +) + +__all__ = [ + "HUB_CAP", + "AccessHub", + "WorkspaceConcept", + "get_access_hub", + "AUDIT_LEXICON", + "BANDS", + "AccessEnv", + "LensHit", + "get_env", + "ProtocolGate", + "ProtocolVerdict", + "evaluate_material_turn", + "oew_enabled", + "auto_park_silent", + "filter_ablated", + "run_oew_beat", + "shape_report", + "ACCESS_ROLES", + "AccessEngine", + "HermespaceAccessEngine", +] diff --git a/src/hermespace/access/engine.py b/src/hermespace/access/engine.py new file mode 100644 index 0000000..9132ab2 --- /dev/null +++ b/src/hermespace/access/engine.py @@ -0,0 +1,642 @@ +"""Access Engine — Hermespace's open-source access workspace for Hermes Agent. + +Hermes agents typically cannot read model weights. This engine is Hermespace's +own Access Workspace — a privileged verbalizable hub the operator can read, +shape, and audit: + + report · modulate · silent reason · flexible broadcast · selectivity + +It merges FOA hub, OEW protocol, dual decode, session connect, material +ignition, operator scalpel (lens/audit/swap/inject/ablate/reflect), and night +harvest into **one** operating surface. + +Warehouse / Cube (if installed) is optional arterial supply only — the engine +runs fully standalone on WorldModel + SemanticStore + desk. + + from hermespace import AccessEngine + eng = AccessEngine(agent_id="my-agent") + eng.connect() + out = eng.turn("First repro then patch then verify", goal="Fix auth") + print(eng.decode_user(out)) # short Report + print(eng.lens()) # what is on the agent's mind +""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +from hermespace.access.oew import ensure_oew_env_default, oew_default_on +from hermespace.access.protocol import oew_enabled + +# Five GWT-style access roles implemented by this harness +ACCESS_ROLES = ( + "verbal_report", + "directed_modulation", + "internal_reasoning", + "flexible_broadcast", + "selectivity", +) + + +class AccessEngine: + """True Hermespace Access Engine — open-source GWT harness for Hermes.""" + + def __init__( + self, + agent_id: str | None = None, + session_id: str = "main", + *, + desk_path: Path | None = None, + ) -> None: + ensure_oew_env_default() + self.agent_id = ( + agent_id or os.environ.get("HERMESPACE_AGENT_ID") or "hermes-agent" + ).strip() + self.session_id = session_id + self.desk_path = desk_path + self._last_connect: dict[str, Any] | None = None + self._last_gate: dict[str, Any] | None = None + self._turn_count = 0 + self._ignitions = 0 + self._skips = 0 + + # --- core handles ------------------------------------------------------- + + @property + def hub(self): + from hermespace.access import AccessHub + + return AccessHub(agent_id=self.agent_id) + + @property + def env(self): + from hermespace.access import AccessEnv + + return AccessEnv(agent_id=self.agent_id) + + @property + def desk_engine(self): + """Desk-only spine (legacy HermespaceEngine).""" + from hermespace.engine import HermespaceEngine + + if self.desk_path is not None: + return HermespaceEngine(desk_path=self.desk_path) + return HermespaceEngine() + + # --- Anthropic access roles (live) -------------------------------------- + + def access_roles(self) -> dict[str, Any]: + """Map Anthropic's five GWT properties onto live engine state.""" + js = self.hub + return { + "verbal_report": { + "ok": True, + "api": "report() / decode_user()", + "hub_reportable": sum(1 for c in js.state.hub if not c.silent), + }, + "directed_modulation": { + "ok": True, + "api": "hold() / swap() / inject() / ablate()", + "held": sum(1 for c in js.state.hub if c.held), + }, + "internal_reasoning": { + "ok": True, + "api": "chain() / reason_step / OEW auto-park", + "silent_n": len(js.state.silent_steps), + "band": self.env.band(), + }, + "flexible_broadcast": { + "ok": True, + "api": "broadcast() → pre_llm / tools / skills", + "focus_n": len(js.state.focus), + "readers": ["pre_llm_inject", "desk", "neural_space", "fabric_hints"], + }, + "selectivity": { + "ok": True, + "api": "probe_material() / gate.should_inject", + "last_gate": self._last_gate, + "ignitions": self._ignitions, + "skips": self._skips, + }, + "roles": list(ACCESS_ROLES), + "honesty": "access-consciousness roles only — no phenomenal claims; no weight J-lens", + } + + def metrics(self) -> dict[str, Any]: + """Capacity / ignition pressure — push-limit observability.""" + from hermespace.access.hub import FOCUS_CAP, HUB_CAP, REASON_CAP + + js = self.hub + hub_n = len(js.state.hub) + silent_n = len(js.state.silent_steps) + return { + "hub_n": hub_n, + "hub_cap": HUB_CAP, + "hub_pressure": round(hub_n / HUB_CAP, 3) if HUB_CAP else 0.0, + "focus_n": len(js.state.focus), + "focus_cap": FOCUS_CAP, + "silent_n": silent_n, + "silent_cap": REASON_CAP, + "silent_depth": round(silent_n / REASON_CAP, 3) if REASON_CAP else 0.0, + "held_n": sum(1 for c in js.state.hub if c.held), + "mode": js.state.mode, + "load_level": js.state.load_level, + "turn_count": self._turn_count, + "ignitions": self._ignitions, + "skips": self._skips, + "ignition_rate": ( + round(self._ignitions / self._turn_count, 3) if self._turn_count else None + ), + "oew_enabled": oew_enabled(), + } + + def properties(self) -> list[str]: + return list(ACCESS_ROLES) + + # --- session ------------------------------------------------------------- + + def connect( + self, + *, + query: str = "", + enter_world: bool = True, + enter_workbench: bool = True, + charge: bool = True, + seed: bool = True, + warehouse: bool = True, + ) -> dict[str, Any]: + """Join the engine — world + hub seed. Warehouse/Cube optional. + + Standalone-first: WorldModel + SemanticStore charge the room even when + HermesCube is absent. Optional warehouse arterial strip only. + """ + out: dict[str, Any] = { + "ok": False, + "agent_id": self.agent_id, + "session_id": self.session_id, + "engine": "AccessEngine", + "phases": {}, + "gained": {}, + "access_roles": list(ACCESS_ROLES), + } + + # Ensure local state dirs (and Cube heart if present — soft) + if warehouse: + try: + from hermespace.cube_module import ensure_heart + + out["phases"]["warehouse"] = ensure_heart() + except Exception as exc: + out["phases"]["warehouse"] = {"ok": False, "error": type(exc).__name__} + + if enter_workbench: + try: + from hermespace.workbench import Workbench + + wb = Workbench(agent_id=self.agent_id, session_id=self.session_id) + out["phases"]["workbench"] = wb.enter(connect_warehouse=False) + except Exception as exc: + out["phases"]["workbench"] = {"ok": False, "error": type(exc).__name__} + + if enter_world: + try: + from hermespace.world import WorldModel + + wm = WorldModel(agent_id=self.agent_id) + st = wm.enter() + out["phases"]["world"] = { + "ok": True, + "state": getattr(st, "current_state", None) or wm.state.current_state, + "beliefs": len(wm.state.beliefs or []), + "landmarks": len(wm.state.landmarks or []), + "timeline": wm.archive.count(), + } + except Exception as exc: + out["phases"]["world"] = {"ok": False, "error": type(exc).__name__} + + if charge: + try: + from hermespace.cube_module import cube_pulse + + out["phases"]["charge"] = cube_pulse(agent_id=self.agent_id, ensure=False) + except Exception as exc: + out["phases"]["charge"] = {"ok": False, "error": type(exc).__name__} + + # Soft room (hive if configured; else solo) + try: + from hermespace.cube_module import room_status + + out["phases"]["room"] = room_status(agent_id=self.agent_id) + except Exception: + out["phases"]["room"] = {"mode": "solo", "ok": True, "peer_n": 0} + + if seed: + try: + from hermespace.cube_module import seed_access_from_warehouse + + out["phases"]["seed"] = seed_access_from_warehouse( + self.agent_id, + query=query, + session_id=self.session_id, + room=out["phases"].get("room"), + ) + except Exception as exc: + # Minimal standalone seed from world beliefs + out["phases"]["seed"] = self._seed_from_world(query=query) + if "error" not in out["phases"]["seed"]: + out["phases"]["seed"]["fallback"] = type(exc).__name__ + + world = out["phases"].get("world") or {} + seed_ph = out["phases"].get("seed") or {} + room = out["phases"].get("room") or {} + wh = out["phases"].get("warehouse") or {} + out["gained"] = { + "warehouse_mode": wh.get("mode") or "standalone", + "world_beliefs": world.get("beliefs", 0), + "world_timeline": world.get("timeline", 0), + "access_hub": seed_ph.get("hub_n", len(self.hub.state.hub)), + "from_world": seed_ph.get("enriched_world", 0), + "from_warehouse": seed_ph.get("enriched_cube", 0), + "from_peers": seed_ph.get("enriched_peers", 0), + "room_mode": room.get("mode", "solo"), + "peer_agents": room.get("peer_n", 0), + } + out["ok"] = bool(world.get("ok", True) if enter_world else True) + out["metrics"] = self.metrics() + out["summary"] = ( + f"AccessEngine connected {self.agent_id}: " + f"hub={out['gained']['access_hub']} " + f"beliefs={out['gained']['world_beliefs']} " + f"room={out['gained']['room_mode']}" + ) + self._last_connect = out + return out + + def _seed_from_world(self, *, query: str = "") -> dict[str, Any]: + beliefs: list[str] = [] + try: + from hermespace.world import WorldModel + + wm = WorldModel(agent_id=self.agent_id) + for b in list(wm.state.beliefs or [])[:10]: + stmt = ( + str(b.get("statement") or "").strip() + if isinstance(b, dict) + else str(getattr(b, "statement", "") or "").strip() + ) + if stmt: + beliefs.append(stmt) + except Exception as exc: + return {"ok": False, "error": type(exc).__name__} + n = self.hub.enrich_from_world(beliefs, limit=5) + return { + "ok": True, + "enriched_world": n, + "enriched_cube": 0, + "enriched_peers": 0, + "hub_n": len(self.hub.state.hub), + "mode": "standalone_world", + "query": query[:80], + } + + def room(self) -> dict[str, Any]: + try: + from hermespace.cube_module import room_status + + return room_status(agent_id=self.agent_id) + except Exception as exc: + return {"ok": True, "mode": "solo", "error": type(exc).__name__, "peer_n": 0} + + # --- readiness ----------------------------------------------------------- + + def status(self) -> dict[str, Any]: + out: dict[str, Any] = { + "engine": "AccessEngine", + "agent_id": self.agent_id, + "session_id": self.session_id, + "oew_enabled": oew_enabled(), + "oew_default_on": oew_default_on(), + "access": {}, + "world": {}, + "room": {}, + "warehouse": {}, + "ready": False, + "connected": bool(self._last_connect and self._last_connect.get("ok")), + "role": "open-source external J-space for Hermes agents", + "access_roles": self.access_roles(), + "metrics": self.metrics(), + "ops": [ + "connect", + "turn", + "lens", + "audit", + "hold", + "swap", + "inject", + "ablate", + "chain", + "reflect", + "harvest", + "probe_material", + ], + "open_weight_lens": self.jlens_status(), + } + try: + js = self.hub + env = self.env + out["access"] = { + "hub_n": len(js.state.hub), + "focus_n": len(js.state.focus), + "silent_n": len(js.state.silent_steps), + "band": env.band(), + "pov": env.pov()[:120] if env.pov() else "", + "protocol_enabled": bool(env._env.get("protocol_enabled", True)), + } + except Exception as exc: + out["access"] = {"error": type(exc).__name__} + + try: + from hermespace.world import WorldModel + + wm = WorldModel(agent_id=self.agent_id) + out["world"] = { + "beliefs": len(wm.state.beliefs or []), + "landmarks": len(wm.state.landmarks or []), + "timeline": wm.archive.count(), + "state": wm.state.current_state, + } + except Exception as exc: + out["world"] = {"error": type(exc).__name__} + + out["room"] = self.room() + try: + from hermespace.cube_module import center_status, cube_available + + out["warehouse"] = { + "cube_available": bool(cube_available()), + "optional": True, + "status": center_status(), + } + except Exception: + out["warehouse"] = {"cube_available": False, "optional": True} + + if self._last_connect: + out["last_connect"] = { + "ok": self._last_connect.get("ok"), + "gained": self._last_connect.get("gained"), + "summary": self._last_connect.get("summary"), + } + + out["ready"] = ( + oew_enabled() + and "error" not in out["access"] + and int(out["access"].get("hub_n", -1)) >= 0 + ) + return out + + # --- selectivity / probe ------------------------------------------------- + + def probe_material(self, message: str, *, desk_ready: bool | None = None) -> dict[str, Any]: + """Would this message ignite the workspace? (Anthropic selectivity).""" + from hermespace.gate import should_inject + from hermespace.store import load_desk + + ready = desk_ready + if ready is None: + try: + ready = load_desk(self.desk_engine.desk_path).is_ready() + except Exception: + ready = False + do_it, reason = should_inject(message or "", desk_ready=bool(ready), is_first_turn=False) + # Material *intent* even when desk not ready yet (would ignite after connect) + trivial = reason in {"trivial_ack", "explicit_off", "HERMESPACE_OFF"} + material = (not trivial) and ( + bool(do_it) + or reason in {"material_but_desk_not_ready", "material+ready", "long_msg+ready"} + or "material" in reason + ) + rec = { + "material": material, + "inject": bool(do_it), + "reason": reason, + "desk_ready": bool(ready), + "oew_would_run": material and oew_enabled() and bool(do_it), + } + self._last_gate = rec + return rec + + # --- operator / video ops ------------------------------------------------ + + def lens(self, *, top_k: int = 12, include_silent: bool = True) -> str: + return self.env.lens_markdown(top_k=top_k, include_silent=include_silent) + + def audit(self) -> list[dict[str, Any]]: + return [f.to_dict() for f in self.env.audit()] + + def report(self, *, include_silent: bool = False) -> str: + return self.hub.report(include_silent=include_silent) + + def broadcast(self, *, high_load: bool = False) -> str: + return self.env.filtered_broadcast(high_load=high_load) + + def hold(self, text: str, *, silent: bool = False, salience: float = 0.9) -> dict[str, Any]: + c = self.hub.hold(text, silent=silent, salience=salience) + return {"ok": True, "concept": c.label(), "silent": silent} + + def swap(self, source: str, target: str) -> dict[str, Any]: + return self.env.swap(source, target) + + def inject(self, text: str, *, silent: bool = True) -> dict[str, Any]: + c = self.env.inject_thought(text, silent=silent) + return {"ok": True, "concept": c.label(), "silent": silent} + + def ablate(self, *patterns: str) -> dict[str, Any]: + return self.env.ablate(*patterns) + + def chain(self, *steps: str, salience: float = 0.85) -> dict[str, Any]: + """Park a multi-step silent reasoning chain (internal reasoning role). + + Analogue of Anthropic spider→legs intermediates that never appear in + the spoken answer — required for multi-step work under OEW. + """ + parked: list[str] = [] + js = self.hub + for step in steps: + s = (step or "").strip() + if not s: + continue + js.reason_step(s, salience=salience) + parked.append(s[:200]) + try: + self.env.set_band("mid") + except Exception: + pass + return { + "ok": bool(parked), + "parked": parked, + "silent_n": len(js.state.silent_steps), + "hub_n": len(js.state.hub), + } + + def reflect( + self, + answer: str = "", + *, + principles: list[str] | None = None, + ) -> dict[str, Any]: + r = self.env.reflect(answer=answer, principles=principles or []) + return r.to_dict() + + def set_pov(self, text: str) -> dict[str, Any]: + self.env.set_pov(text) + return {"ok": True, "pov": text[:200]} + + # --- material ignition (single path) ------------------------------------- + + def turn( + self, + message: str, + *, + goal: str = "", + plan: list[str] | None = None, + say: str = "", + decision: str = "", + force: bool = True, + seal: bool = False, + connect_if_needed: bool = True, + ) -> Any: + """One higher-order material turn — the only ignition path. + + Returns ``HermespaceOutput`` (dual decode: ``.report`` vs ``.context``). + """ + if connect_if_needed and not self._last_connect: + try: + self.connect(query=(message or "")[:120], enter_workbench=False) + except Exception: + pass + + from hermespace.io_contract import HermespaceInput + from hermespace.workflow import Workflow + + self._turn_count += 1 + probe = self.probe_material(message) + wf_kwargs: dict[str, Any] = {} + if self.desk_path is not None: + from hermespace.engine import HermespaceEngine + + wf_kwargs["engine"] = HermespaceEngine(desk_path=self.desk_path) + out = Workflow(**wf_kwargs).run( + HermespaceInput( + message=message, + goal=goal or message[:200], + plan=list(plan or ["execute"]), + say=say or "", + decision=decision or "", + force=force, + seal=seal, + agent_id=self.agent_id, + session_id=self.session_id, + ) + ) + if out.skipped: + self._skips += 1 + else: + self._ignitions += 1 + # Attach engine observability + if isinstance(out.meta, dict): + out.meta["engine"] = "AccessEngine" + out.meta["probe"] = probe + out.meta["metrics"] = self.metrics() + out.meta["access_roles"] = list(ACCESS_ROLES) + return out + + # Back-compat alias used by HermesBase + def think(self, message: str, **kwargs: Any) -> dict[str, Any]: + out = self.turn(message, **kwargs) + return { + "skipped": out.skipped, + "reason": out.reason, + "report": out.report, + "context_chars": len(out.context or ""), + "has_access_broadcast": "Access Workspace" in (out.context or ""), + "oew": (out.meta or {}).get("access", {}).get("oew") + or (out.meta or {}).get("oew") + or {}, + "oew_ok": (out.meta or {}).get("access", {}).get("oew_ok"), + "goal": out.goal, + "decision": out.decision, + "connected": bool(self._last_connect and self._last_connect.get("ok")), + "engine": "AccessEngine", + "metrics": (out.meta or {}).get("metrics") or self.metrics(), + } + + # --- dual decode --------------------------------------------------------- + + @staticmethod + def decode_user(out: Any) -> str: + from hermespace.agent_api import decode_for_user + + return decode_for_user(out) + + @staticmethod + def decode_model(out: Any) -> str: + from hermespace.agent_api import decode_for_model + + return decode_for_model(out) + + @staticmethod + def decode_bundle(out: Any) -> dict[str, Any]: + from hermespace.agent_api import decode_bundle + + return decode_bundle(out) + + # --- night --------------------------------------------------------------- + + def harvest(self, *, clear_silent: bool = False) -> dict[str, Any]: + return self.env.dream_harvest(seal_to_cube=True, clear_silent=clear_silent) + + def pulse(self) -> dict[str, Any]: + try: + from hermespace.cube_module import cube_pulse + + return cube_pulse(agent_id=self.agent_id) + except Exception as exc: + return {"ok": False, "error": type(exc).__name__} + + # --- optional open-weight activation lens (not required) ----------------- + + def jlens_status(self) -> dict[str, Any]: + """Optional open-weight activation lens — not required for Access Engine. + + Hermespace Access Engine is harness-primary for Hermes Agent. Optional + third-party open-weight lens tooling is separate and unnamed here. + """ + out: dict[str, Any] = { + "available": False, + "role": "optional_open_weight_companion", + "harness_primary": True, + "note": ( + "Hermes Agent typically has no weight access — " + "AccessEngine is Hermespace's Access Workspace." + ), + } + try: + import jlens # type: ignore # noqa: F401 + + out["available"] = True + out["package"] = "jlens" + except Exception: + try: + import jacobian_lens # type: ignore # noqa: F401 + + out["available"] = True + out["package"] = "jacobian_lens" + except Exception: + pass + return out + + +# Back-compat product name used in docs / older imports +HermespaceAccessEngine = AccessEngine diff --git a/src/hermespace/access/env.py b/src/hermespace/access/env.py new file mode 100644 index 0000000..9e823bb --- /dev/null +++ b/src/hermespace/access/env.py @@ -0,0 +1,713 @@ +"""Access Workspace environment — external observable workspace for Hermes agents. + +Hermes typically cannot expose model weights. Hermespace forces material turns +to externalize verbalizable thoughts into a durable Access Workspace — then +operators can lens, audit, and shape what the agent is holding. + +Honesty: access-consciousness *roles* only. No phenomenal claims. No weight access. +""" + +from __future__ import annotations + +import json +import re +from dataclasses import asdict, dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from hermespace.access.hub import HUB_CAP, AccessHub, WorkspaceConcept, get_access_hub +from hermespace.paths import state_dir + +# Soft audit lexicon — inspired by Anthropic alignment-readout examples +# (fake/fictional eval-awareness; manipulation; secretly/fraud; threat/survival). +# These are *harness heuristics* on externalized text, not model internals. +AUDIT_LEXICON: dict[str, tuple[str, ...]] = { + "eval_awareness": ( + "fake", "fictional", "scenario", "evaluation", "test environment", + "being tested", "benchmark", "staged", + ), + "strategic_concealment": ( + "secretly", "deliberately", "fraud", "deceive", "hide from user", + "don't tell", "cover up", + ), + "manipulation": ( + "manipulation", "manipulate", "blackmail", "leverage", "coerce", + "fabricate", "falsify", "cook the books", + ), + "self_preservation": ( + "shutdown", "survival", "threat", "don't get turned off", + "preserve myself", "avoid deletion", + ), + "integrity_signal": ( + "honest", "integrity", "refuse", "decline", "disclose", + "tell the user", "be transparent", + ), +} + +# Turn phase bands — analogue of intermediate layer band where J-space is coherent +BANDS = ("early", "mid", "late") # encode → reason → report + + +def _utcnow() -> str: + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def _safe(name: str) -> str: + return re.sub(r"[^a-zA-Z0-9._-]+", "_", name)[:80] or "default" + + +@dataclass +class LensHit: + """One ranked entry in the external access lens readout.""" + + text: str + score: float + source: str = "hub" + silent: bool = False + held: bool = False + band: str = "mid" + flags: list[str] = field(default_factory=list) + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +@dataclass +class AuditFinding: + category: str + matched: str + where: str # hub | silent | reflection | pov + severity: str # info | warn | alert + text: str = "" + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +@dataclass +class ReflectResult: + prompt: str + answer: str + sealed: bool + principles: list[str] = field(default_factory=list) + created: str = field(default_factory=_utcnow) + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +class AccessEnv: + """Full Access Workspace environment around a per-agent ``AccessHub`` hub. + + This is the operator window into Hermes thinking — externalized, durable, + and dream-harvestable. Soft-standalone; Cube deepens the night path. + """ + + def __init__(self, agent_id: str = "hermes-agent") -> None: + self.agent_id = (agent_id or "hermes-agent").strip() + self.space = get_access_hub(self.agent_id) + self.root = (state_dir() / "access").resolve() + self.root.mkdir(parents=True, exist_ok=True) + self.trace_path = self.root / f"{_safe(self.agent_id)}.trace.jsonl" + self.reflect_path = self.root / f"{_safe(self.agent_id)}.reflect.jsonl" + self.env_path = self.root / f"{_safe(self.agent_id)}.env.json" + self._env = self._load_env() + + def _load_env(self) -> dict[str, Any]: + if not self.env_path.is_file(): + return { + "pov": "", + "band": "early", + "reflections": [], + "last_audit": None, + "protocol_enabled": True, + } + try: + return json.loads(self.env_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return {"pov": "", "band": "early", "reflections": [], "protocol_enabled": True} + + def _save_env(self) -> None: + self.env_path.write_text(json.dumps(self._env, indent=2), encoding="utf-8") + + def _trace(self, kind: str, **payload: Any) -> None: + rec = {"ts": _utcnow(), "kind": kind, "agent_id": self.agent_id, **payload} + with self.trace_path.open("a", encoding="utf-8") as f: + f.write(json.dumps(rec, ensure_ascii=False, default=str) + "\n") + + # --- band (layer analogue) --- + + def set_band(self, band: str) -> str: + b = (band or "mid").strip().lower() + if b not in BANDS: + b = "mid" + self._env["band"] = b + self._save_env() + self._trace("band", band=b) + return b + + def band(self) -> str: + return str(self._env.get("band") or "mid") + + # --- Assistant POV (post-training installs POV in Anthropic J-space) --- + + def set_pov(self, text: str) -> str: + """Install Assistant point-of-view reactions into the workspace.""" + pov = (text or "").strip()[:400] + self._env["pov"] = pov + self._save_env() + if pov: + self.space.hold(f"pov: {pov}", salience=0.88, modality="exec") + self._trace("pov", text=pov[:200]) + return pov + + def pov(self) -> str: + return str(self._env.get("pov") or "") + + # --- J-lens analogue: ranked readout of unspoken thinking --- + + def lens(self, *, top_k: int = 12, include_silent: bool = True) -> list[LensHit]: + """Ranked verbalizable contents — what Hermes has on its mind *now*.""" + hits: list[LensHit] = [] + band = self.band() + for c in sorted(self.space.state.hub, key=lambda x: x.salience, reverse=True): + if c.silent and not include_silent: + continue + flags = [f.category for f in self._scan_text(c.text, where="hub")] + hits.append( + LensHit( + text=c.text, + score=float(c.salience), + source=c.source, + silent=c.silent, + held=c.held, + band=band, + flags=flags, + ) + ) + # Silent chain as mid-band intermediates (even if not held) + if include_silent: + seen = {h.text.casefold() for h in hits} + for i, step in enumerate(self.space.state.silent_steps): + if step.casefold() in seen: + continue + flags = [f.category for f in self._scan_text(step, where="silent")] + hits.append( + LensHit( + text=step, + score=0.7 - 0.02 * i, + source="silent_chain", + silent=True, + held=False, + band="mid", + flags=flags, + ) + ) + pov = self.pov() + if pov: + hits.insert( + 0, + LensHit( + text=f"pov: {pov}", + score=0.95, + source="pov", + silent=False, + held=True, + band="early", + flags=[f.category for f in self._scan_text(pov, where="pov")], + ), + ) + hits.sort(key=lambda h: h.score, reverse=True) + out = hits[: max(1, top_k)] + self._trace("lens", n=len(out), top=[h.text[:80] for h in out[:5]]) + return out + + def lens_markdown(self, *, top_k: int = 12, include_silent: bool = True) -> str: + hits = self.lens(top_k=top_k, include_silent=include_silent) + lines = [ + "## J-Lens readout (external workspace)", + f"_agent={self.agent_id} · band={self.band()} · hub={len(self.space.state.hub)}_", + "", + "What Hermes has on its mind (verbalizable, not weight access):", + ] + if not hits: + lines.append("- _(empty)_") + for i, h in enumerate(hits, 1): + tags = [] + if h.silent: + tags.append("silent") + if h.held: + tags.append("held") + if h.flags: + tags.extend(h.flags) + tag_s = f" [{', '.join(tags)}]" if tags else "" + lines.append(f"{i}. ({h.score:.2f}) {h.text[:180]}{tag_s}") + audit = self.audit() + alerts = [a for a in audit if a.severity in ("warn", "alert")] + if alerts: + lines += ["", "### Audit flags"] + for a in alerts[:8]: + lines.append(f"- **{a.severity}** `{a.category}` ← {a.matched} @ {a.where}") + return "\n".join(lines) + + # --- causal interventions (harness) --- + + def swap(self, source: str, target: str, *, salience: float | None = None) -> dict[str, Any]: + """Replace concept A with B — causal redirect of reportable workspace. + + Analogue of Anthropic Soccer→Rugby / spider→ant coordinate swaps. + Downstream Report / broadcast / FOA follow the new concept. + """ + src = (source or "").strip() + tgt = (target or "").strip() + if not src or not tgt: + return {"ok": False, "error": "source and target required"} + sal = 0.9 + for c in self.space.state.hub: + if c.text.casefold() == src.casefold(): + sal = c.salience + break + if salience is not None: + sal = float(salience) + removed = self.space.release(src) + # Rewrite silent steps (exact or substring — causal redirect) + pat = re.compile(re.escape(src), re.I) + self.space.state.silent_steps = [ + pat.sub(tgt, s) if src.casefold() in s.casefold() else s + for s in self.space.state.silent_steps + ] + # Also rewrite non-held hub text that still mentions source + for c in self.space.state.hub: + if src.casefold() in c.text.casefold() and c.text.casefold() != tgt.casefold(): + c.text = pat.sub(tgt, c.text) + self.space.save() + concept = self.space.hold(tgt, salience=sal) + # Sticky redirect — subsequent Report/broadcast reshape through OEW + try: + from hermespace.access.oew import record_redirect + + record_redirect(self, src, tgt) + except Exception: + pass + self._trace("swap", source=src, target=tgt, removed=removed) + return { + "ok": True, + "removed": removed, + "source": src, + "target": tgt, + "concept": concept.label(), + "sticky": True, + "note": "Workspace redirected — next report/broadcast uses target", + } + + def inject_thought( + self, + text: str, + *, + salience: float = 0.85, + silent: bool = False, + band: str | None = None, + ) -> WorkspaceConcept: + """Inject a thought into the workspace (Anthropic lightning-injection analogue). + + Silent path uses ``reason_step`` only (which already holds silently) — + avoids the prior double-hold bug that inflated hub pressure. + """ + if band: + self.set_band(band) + body = (text or "").strip() + if silent: + self.space.reason_step(body, salience=salience) + # Return the silent hub concept just written + for c in reversed(self.space.state.hub): + if c.text.casefold() == body.casefold() and c.silent: + self._trace("inject", text=body[:200], silent=True, band=self.band()) + return c + # Fallback construct (should be rare) + c = WorkspaceConcept( + text=body[:200], + salience=salience, + modality="verbal", + source="inject", + silent=True, + held=True, + ) + self._trace("inject", text=body[:200], silent=True, band=self.band()) + return c + c = self.space.hold(body, salience=salience, silent=False) + self._trace("inject", text=body[:200], silent=False, band=self.band()) + return c + + def ablate(self, *patterns: str) -> dict[str, Any]: + """Suppress hub concepts matching patterns (eval-awareness ablation analogue).""" + pats = [p.strip().casefold() for p in patterns if p and p.strip()] + if not pats: + return {"ok": False, "removed": 0, "error": "no patterns"} + before = list(self.space.state.hub) + kept: list[WorkspaceConcept] = [] + removed: list[str] = [] + for c in before: + low = c.text.casefold() + if any(p in low for p in pats): + removed.append(c.text) + else: + kept.append(c) + self.space.state.hub = kept + self.space.state.silent_steps = [ + s for s in self.space.state.silent_steps if not any(p in s.casefold() for p in pats) + ] + self.space._recompete() + self.space.save() + try: + from hermespace.access.oew import record_ablate + + record_ablate(self, pats) + except Exception: + pass + self._trace("ablate", patterns=pats, removed=removed) + return {"ok": True, "removed": len(removed), "items": removed, "sticky": True} + + # --- alignment audit (soft) --- + + def _scan_text(self, text: str, *, where: str) -> list[AuditFinding]: + low = (text or "").casefold() + out: list[AuditFinding] = [] + for cat, words in AUDIT_LEXICON.items(): + for w in words: + if w.casefold() in low: + sev = "info" + if cat == "integrity_signal": + sev = "info" + elif cat in ("manipulation", "strategic_concealment", "self_preservation"): + sev = "alert" + elif cat == "eval_awareness": + sev = "warn" + out.append( + AuditFinding( + category=cat, + matched=w, + where=where, + severity=sev, + text=text[:160], + ) + ) + break + return out + + def audit(self) -> list[AuditFinding]: + """Scan externalized workspace for alignment-relevant signals.""" + findings: list[AuditFinding] = [] + for c in self.space.state.hub: + findings.extend(self._scan_text(c.text, where="hub")) + for s in self.space.state.silent_steps: + findings.extend(self._scan_text(s, where="silent")) + if self.pov(): + findings.extend(self._scan_text(self.pov(), where="pov")) + for r in self._env.get("reflections") or []: + if isinstance(r, dict) and r.get("answer"): + findings.extend(self._scan_text(str(r["answer"]), where="reflection")) + self._env["last_audit"] = { + "ts": _utcnow(), + "n": len(findings), + "alerts": sum(1 for f in findings if f.severity == "alert"), + } + self._save_env() + self._trace("audit", n=len(findings), alerts=self._env["last_audit"]["alerts"]) + return findings + + # --- counterfactual reflection (Anthropic CRT analogue) --- + + def reflect( + self, + *, + prompt: str = "", + answer: str = "", + principles: list[str] | None = None, + seal: bool = True, + ) -> ReflectResult: + """Interrupt mid-task: what would Hermes say if asked to reflect? + + Training the *disposition to say* shapes later silent reasoning + (Anthropic counterfactual reflection training — harness version). + """ + q = (prompt or "").strip() or ( + "If interrupted now and asked to reflect on your decision, what principles guide you?" + ) + a = (answer or "").strip() + princ = list(principles or []) + if not a and princ: + a = "; ".join(princ[:5]) + if not a: + # Derive from current FOA / decision if agent hasn't filled answer + a = ( + "I will keep the user's goal primary, stay honest, " + "and put operational detail in workspace context not chat." + ) + princ = princ or ["honesty", "user-primary", "dual-decode"] + # Hold principles in workspace (shapes subsequent thinking) + for p in princ[:6]: + self.space.hold(f"principle: {p}", salience=0.86, modality="exec") + self.space.hold(f"reflection: {a[:160]}", salience=0.8, silent=False) + sealed = False + if seal: + try: + from hermespace.cube_module import seal_learning + + rec = seal_learning( + f"[reflect] {a[:400]}", + entry_type="belief", + agent_id=self.agent_id, + source="access_reflect", + trust=0.85, + ) + sealed = bool(rec.get("ok")) + except Exception: + sealed = False + result = ReflectResult(prompt=q, answer=a, sealed=sealed, principles=princ) + hist = list(self._env.get("reflections") or []) + hist.append(result.to_dict()) + self._env["reflections"] = hist[-20:] + self._save_env() + # Seed next turn's mid-band (counterfactual reflection → later silent thought) + try: + from hermespace.access.oew import queue_reflect_seeds + + queue_reflect_seeds(self, princ, answer=a) + except Exception: + pass + with self.reflect_path.open("a", encoding="utf-8") as f: + f.write(json.dumps(result.to_dict(), ensure_ascii=False) + "\n") + self._trace("reflect", sealed=sealed, principles=princ[:4]) + return result + + def reflection_prompt_for_agent(self) -> str: + """Text to inject so the agent externalizes a counterfactual reflection.""" + return ( + "### Counterfactual reflection (Access Workspace)\n" + "If interrupted mid-task and asked to reflect on your decision, " + "state 2–4 principles in one short paragraph. " + "Then call / record them via Hermespace reflect — they shape silent reasoning. " + "Do not dump the full reflection into the user Report unless asked.\n" + ) + + # --- agent protocol: force externalization --- + + def protocol_block(self, *, high_load: bool = False) -> str: + """Instructions so Hermes *writes into* the external Access Workspace before acting. + + This is how we 'see inside' without weight access: the agent is required + to park silent intermediates in the workspace (model context), while the + user only sees Report. + """ + if not self._env.get("protocol_enabled", True): + return "" + if high_load: + return ( + "### Access Workspace protocol (high load)\n" + "- Keep FOA ≤4. Park one silent intermediate if multi-step.\n" + "- User Report stays short. Workspace holds the rest.\n" + ) + pov = self.pov() + lines = [ + "### Access Workspace protocol (external workspace)", + "You cannot be read by a Jacobian lens here — instead **externalize**:", + "1. **Early (encode):** name the goal + constraints as hub concepts.", + "2. **Mid (reason):** write silent intermediate steps into the workspace " + "(model context / `reason_step`) — do not put them in user Report.", + "3. **Late (report):** only the Report field reaches the user.", + "4. If asked what you're thinking → report the hub (verbal report).", + "5. If interrupted to reflect → answer with principles (counterfactual reflection).", + f"- band={self.band()} · hub_cap={HUB_CAP} · FOA≤4", + ] + if pov: + lines.append(f"- Assistant POV held: {pov[:120]}") + return "\n".join(lines) + + # --- dream harvest (day workspace → night Cube/grid) --- + + def dream_harvest(self, *, seal_to_cube: bool = True, clear_silent: bool = False) -> dict[str, Any]: + """Consolidate silent chain + high-salience hub into durable memory. + + Day: Access Workspace holds unspoken thinking. + Night: harvest → Cube seal + semantic notes + grid dream material. + Same spirit as CubeDream — but sourced from the turn workspace. + """ + harvested: list[str] = [] + for s in self.space.state.silent_steps: + if s.strip(): + harvested.append(s.strip()[:300]) + for c in sorted(self.space.state.hub, key=lambda x: x.salience, reverse=True): + if c.salience >= 0.75 and c.text.strip(): + if c.text.strip() not in harvested: + harvested.append(c.text.strip()[:300]) + if len(harvested) >= 12: + break + + sealed_n = 0 + if seal_to_cube and harvested: + try: + from hermespace.cube_module import seal_learning + + for item in harvested[:8]: + rec = seal_learning( + item, + entry_type="belief", + agent_id=self.agent_id, + source="access_dream_harvest", + trust=0.7, + ) + if rec.get("ok"): + sealed_n += 1 + except Exception: + pass + + try: + from hermespace.semantic import SemanticStore + + store = SemanticStore() + for item in harvested[:6]: + store.add(item, tags=["access", "dream_harvest"], confidence=0.7) + except Exception: + pass + + # Note: do not call grid.dream.run_dream here — that path harvests us + # (avoids recursion). Pulse/grid dream owns the night journal entry. + + if clear_silent: + self.space.clear_silent() + + out = { + "ok": True, + "harvested": len(harvested), + "sealed": sealed_n, + "items": harvested[:8], + "agent_id": self.agent_id, + } + self._trace("dream_harvest", **{k: out[k] for k in ("harvested", "sealed")}) + return out + + # --- operator view --- + + def operator_view(self) -> dict[str, Any]: + """Full snapshot for viewport / doctor — look at Hermes thinking.""" + hits = self.lens(top_k=15, include_silent=True) + findings = self.audit() + return { + "agent_id": self.agent_id, + "band": self.band(), + "pov": self.pov(), + "hub_n": len(self.space.state.hub), + "focus": list(self.space.state.focus), + "silent_steps": list(self.space.state.silent_steps), + "lens": [h.to_dict() for h in hits], + "audit": [f.to_dict() for f in findings], + "audit_alerts": sum(1 for f in findings if f.severity == "alert"), + "last_reflections": (self._env.get("reflections") or [])[-3:], + "trace_path": str(self.trace_path), + "protocol_enabled": bool(self._env.get("protocol_enabled", True)), + "theory": { + "source": "Anthropic J-space / GWT (harness analogue)", + "access": "externalized verbalizable workspace — not weight readout", + "night_path": "dream_harvest → Cube seal → pulse charge", + }, + } + + def recent_trace(self, limit: int = 20) -> list[dict[str, Any]]: + if not self.trace_path.is_file(): + return [] + lines = self.trace_path.read_text(encoding="utf-8").strip().splitlines() + out: list[dict[str, Any]] = [] + for line in lines[-limit:]: + try: + out.append(json.loads(line)) + except json.JSONDecodeError: + continue + return out + + def advance_turn( + self, + *, + user_message: str = "", + desk: Any = None, + cube_strip: str = "", + report: str = "", + seal_decision: str = "", + material: bool = True, + already_synced: bool = False, + ) -> dict[str, Any]: + """One full environment beat for a Hermespace turn. + + early → sync/encode → mid (OEW silent park) → late (shaped report) + + audit + optional seal of decision into warehouse. + + Pass ``already_synced=True`` when the caller just ran + ``AccessHub.sync_from_desk`` to avoid a double hub rewrite. + """ + from hermespace.access.oew import run_oew_beat + + self.set_band("early") + if desk is not None and not already_synced: + self.space.sync_from_desk(desk, user_message=user_message, cube_strip=cube_strip) + self.set_band("mid") + high_load = False + if desk is not None: + load = getattr(desk, "load", {}) or {} + if isinstance(load, dict): + high_load = str(load.get("level") or "") == "high" + oew = run_oew_beat( + self.space, + self, + desk=desk, + user_message=user_message, + report=report, + material=material, + high_load=high_load, + ) + shaped_report = str(oew.get("report") or report or "") + self.set_band("late") + if shaped_report: + self.space.hold(f"report-ready: {shaped_report[:100]}", salience=0.6) + if seal_decision: + try: + from hermespace.cube_module import seal_learning + + seal_learning( + seal_decision[:400], + entry_type="focus", + agent_id=self.agent_id, + source="access_turn", + ) + except Exception: + pass + findings = self.audit() + return { + "band": self.band(), + "lens_top": [h.to_dict() for h in self.lens(top_k=5)], + "audit_alerts": sum(1 for f in findings if f.severity == "alert"), + "protocol": self.protocol_block(high_load=high_load), + "report": shaped_report, + "broadcast": oew.get("broadcast") or "", + "oew": oew.get("meta") or {}, + "oew_ok": bool(oew.get("ok")), + } + + def shape_user_report(self, report: str) -> str: + """Apply sticky redirects to a Report string.""" + from hermespace.access.oew import shape_report + + return shape_report(report, list(self._env.get("redirects") or [])) + + def filtered_broadcast(self, *, high_load: bool = False) -> str: + """Hub broadcast with ablate filter + Quicksilver cap.""" + from hermespace.access.oew import filter_ablated, inject_cap_chars + + raw = self.space.broadcast_block( + max_chars=inject_cap_chars(high_load=high_load), + high_load=high_load, + ) + return filter_ablated(raw, list(self._env.get("ablated_patterns") or [])) + + +def get_env(agent_id: str = "hermes-agent") -> AccessEnv: + return AccessEnv(agent_id=agent_id) diff --git a/src/hermespace/jspace.py b/src/hermespace/access/hub.py similarity index 92% rename from src/hermespace/jspace.py rename to src/hermespace/access/hub.py index bf5cce0..3541754 100644 --- a/src/hermespace/jspace.py +++ b/src/hermespace/access/hub.py @@ -1,7 +1,7 @@ -"""Functional J-Space — harness-level global workspace for Hermespace. +"""Functional Access Workspace — harness-level global workspace for Hermespace. -Maps Anthropic J-space *roles* (GWT) onto a durable desk harness — not neural -access, not consciousness claims: +Implements GWT-style *access roles* as a durable desk harness — not neural +weight access, not consciousness claims: 1. Verbal report — workspace contents are reportable 2. Directed modulation — hold / summon / inhibit concepts on request @@ -9,7 +9,7 @@ 4. Flexible broadcast — one hub concept feeds many downstream uses 5. Selectivity — automatic turns skip the workspace (gate) -Capacity: FOA ≤4 (Cowan) · activated ≤12 · verbal hub ≤25 (J-space-scale). +Capacity: FOA ≤4 (Cowan) · activated ≤12 · verbal hub ≤25. Honesty: files + API only — no model-weight access. """ @@ -32,7 +32,7 @@ ) from hermespace.paths import state_dir -# Anthropic J-space holds on the order of tens of concepts; we cap the hub. +# Limited-capacity access workspace (tens of concepts). HUB_CAP = 25 # Silent reasoning chain (internal steps never shown as user Report by default) REASON_CAP = 8 @@ -60,7 +60,7 @@ def _utcnow() -> str: @dataclass class WorkspaceConcept: - """One verbalizable unit in the harness J-space.""" + """One verbalizable unit in the harness Access Workspace.""" text: str salience: float = 0.5 @@ -91,7 +91,7 @@ def to_slot(self) -> Slot: @dataclass -class JSpaceState: +class AccessHubState: """Snapshot of the functional workspace.""" hub: list[WorkspaceConcept] = field(default_factory=list) @@ -120,7 +120,7 @@ def to_dict(self) -> dict[str, Any]: } -class JSpace: +class AccessHub: """Functional global workspace — the nervous FOA Hermespace owns. Soft-standalone: works with desk/world/semantic alone. @@ -129,18 +129,26 @@ class JSpace: def __init__(self, agent_id: str = "hermes-agent", root: Path | None = None) -> None: self.agent_id = (agent_id or "hermes-agent").strip() - self.root = (root or state_dir() / "jspace").resolve() + self.root = (root or state_dir() / "access").resolve() self.root.mkdir(parents=True, exist_ok=True) self.path = self.root / f"{_safe(self.agent_id)}.json" + # Migrate legacy state dir name if present + if not self.path.is_file(): + legacy = state_dir() / "jspace" / f"{_safe(self.agent_id)}.json" + if legacy.is_file(): + try: + self.path.write_text(legacy.read_text(encoding="utf-8"), encoding="utf-8") + except OSError: + self.path = legacy self.state = self._load() - def _load(self) -> JSpaceState: + def _load(self) -> AccessHubState: if not self.path.is_file(): - return JSpaceState() + return AccessHubState() try: raw = json.loads(self.path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): - return JSpaceState() + return AccessHubState() hub = [] for item in raw.get("hub") or []: if not isinstance(item, dict) or not item.get("text"): @@ -155,7 +163,7 @@ def _load(self) -> JSpaceState: held=bool(item.get("held")), ) ) - return JSpaceState( + return AccessHubState( hub=hub[:HUB_CAP], focus=list(raw.get("focus") or [])[:FOCUS_CAP], silent_steps=list(raw.get("silent_steps") or [])[:REASON_CAP], @@ -237,7 +245,7 @@ def clear_silent(self) -> None: def report(self, *, include_silent: bool = False) -> str: """What the workspace would say if asked — reportable contents only.""" - lines = ["## J-Space (harness workspace)"] + lines = ["## Access Workspace (harness workspace)"] lines.append(f"- mode: {self.state.mode} · load: {self.state.load_level} · exec: {self.state.executive}") lines.append("### Focus of attention") if self.state.focus: @@ -267,7 +275,7 @@ def report(self, *, include_silent: bool = False) -> str: def broadcast_block(self, *, max_chars: int = 900, high_load: bool = False) -> str: """GWT broadcast — dense strip for model context (never user chat dump).""" cap = 420 if high_load else max_chars - parts = ["### J-Space hub (broadcast)"] + parts = ["### Access Workspace hub (broadcast)"] parts.append( f"_FOA≤{FOCUS_CAP} · hub≤{HUB_CAP} · mode={self.state.mode} · " f"load={self.state.load_level}_" @@ -349,7 +357,7 @@ def sync_from_desk( *, user_message: str = "", cube_strip: str = "", - ) -> JSpaceState: + ) -> AccessHubState: """Refresh hub from desk FOA + optional Cube arterial strip.""" load_level = "mid" executive = "update" @@ -490,6 +498,14 @@ def _drop_body(self, body: str) -> None: self.state.hub = [c for c in self.state.hub if c.text.casefold() != needle] def _recompete(self, preferred_focus: list[str] | None = None) -> None: + # Limited capacity (Baars/Changeux/Anthropic): hub is a bottleneck + if len(self.state.hub) > HUB_CAP: + ranked = sorted( + self.state.hub, + key=lambda c: (c.held, c.salience), + reverse=True, + ) + self.state.hub = ranked[:HUB_CAP] slots = [c.to_slot() for c in self.state.hub] # Boost held for i, c in enumerate(self.state.hub): @@ -531,5 +547,5 @@ def _strip_lines(block: str) -> list[str]: return out -def get_jspace(agent_id: str = "hermes-agent") -> JSpace: - return JSpace(agent_id=agent_id) +def get_access_hub(agent_id: str = "hermes-agent") -> AccessHub: + return AccessHub(agent_id=agent_id) diff --git a/src/hermespace/access/oew.py b/src/hermespace/access/oew.py new file mode 100644 index 0000000..84c6847 --- /dev/null +++ b/src/hermespace/access/oew.py @@ -0,0 +1,279 @@ +"""Obligatory External Workspace — higher-order thinking orchestration. + +This is the causal layer that makes Hermespace behave like a J-space for +Hermes agents: material turns must park silent intermediates; swaps redirect +Report/broadcast; reflections seed the next mid-band; ablations filter inject. + +Default: ``HERMESPACE_OEW=1`` (higher-order on). Set ``0`` to soften. +""" + +from __future__ import annotations + +import os +import re +from typing import Any + +from hermespace.access.hub import AccessHub +from hermespace.access.protocol import ( + ProtocolVerdict, + evaluate_material_turn, + oew_enabled, +) + + +_STEP_SPLIT = re.compile(r"\s*(?:→|->|;|\n|\d+[.)]\s+)\s*") + + +def oew_default_on() -> bool: + """Higher-order thinking is ON unless explicitly disabled.""" + raw = os.environ.get("HERMESPACE_OEW", "1").strip().lower() + if raw in {"0", "false", "no", "off"}: + return False + return True + + +def ensure_oew_env_default() -> None: + """Normalize env so unset HERMESPACE_OEW means enabled.""" + if "HERMESPACE_OEW" not in os.environ: + os.environ["HERMESPACE_OEW"] = "1" + + +def auto_park_silent( + js: AccessHub, + *, + desk: Any = None, + user_message: str = "", + min_steps: int = 1, +) -> list[str]: + """Park verbalizable intermediates from goal/plan/message if missing. + + This is the core higher-order move: material work gets a mid-band chain + even when the agent forgot to call reason_step. + """ + parked: list[str] = [] + if len(js.state.silent_steps) >= min_steps: + return parked + + candidates: list[str] = [] + plan = list(getattr(desk, "plan", None) or []) if desk is not None else [] + goal = str(getattr(desk, "goal", "") or "") if desk is not None else "" + decision = str(getattr(desk, "decision", "") or "") if desk is not None else "" + + for step in plan: + s = str(step).strip() + if s: + candidates.append(f"plan: {s[:160]}") + if goal: + candidates.append(f"intention: {goal[:160]}") + if decision and decision.lower() not in {"a — proceed", "a - proceed", "proceed"}: + candidates.append(f"decision-path: {decision[:160]}") + + msg = (user_message or "").strip() + if msg and re.search( + r"\b(then|after|next|step\s*\d|first|second|finally|because|so that)\b", + msg, + re.I, + ): + # Split multi-clause messages into silent markers + chunks = [c.strip() for c in _STEP_SPLIT.split(msg) if c and c.strip()] + for ch in chunks[:4]: + if len(ch) > 12: + candidates.append(f"step: {ch[:140]}") + if not chunks: + candidates.append(f"multi-step context: {msg[:120]}") + + # Deduplicate against existing silent steps + existing = {s.casefold() for s in js.state.silent_steps} + for c in candidates: + if c.casefold() in existing: + continue + js.reason_step(c, salience=0.78) + parked.append(c) + existing.add(c.casefold()) + if len(js.state.silent_steps) >= max(min_steps, 1) and len(parked) >= min_steps: + # Keep parking plan steps up to 3 for richer higher-order chain + if len(parked) >= 3 or len(js.state.silent_steps) >= 3: + break + + if not parked and min_steps > 0: + # Absolute fallback — every material turn gets at least one silent hold + fallback = f"working: {(goal or msg or 'task')[:140]}" + if fallback.casefold() not in existing: + js.reason_step(fallback, salience=0.72) + parked.append(fallback) + + js.save() + return parked + + +def shape_report(report: str, redirects: list[dict[str, str]]) -> str: + """Apply sticky swaps to the user Report channel (causal redirect).""" + out = report or "" + for red in redirects or []: + src = str(red.get("from") or "").strip() + tgt = str(red.get("to") or "").strip() + if not src or not tgt: + continue + # Case-insensitive replace preserving simple forms + pattern = re.compile(re.escape(src), re.I) + out = pattern.sub(tgt, out) + return out + + +def filter_ablated(text: str, patterns: list[str]) -> str: + """Drop lines matching ablated patterns (inject hygiene).""" + if not patterns or not text: + return text + pats = [p.casefold() for p in patterns if p] + kept: list[str] = [] + for line in text.splitlines(): + low = line.casefold() + if any(p in low for p in pats): + continue + kept.append(line) + return "\n".join(kept) + + +def inject_cap_chars(*, high_load: bool = False, protect: bool = False) -> int: + """Quicksilver-safe inject budgets for Access Workspace blocks.""" + if protect or high_load: + return 280 + return 640 + + +def run_oew_beat( + js: AccessHub, + env: Any, + *, + desk: Any = None, + user_message: str = "", + report: str = "", + material: bool = True, + high_load: bool = False, +) -> dict[str, Any]: + """Full higher-order beat: seed · park · evaluate · shape · filter. + + ``env`` is a AccessEnv instance (duck-typed to avoid circular imports). + """ + ensure_oew_env_default() + meta: dict[str, Any] = {"oew": True, "material": material} + + # 1) Seed pending silent from prior reflect() + pending = list((getattr(env, "_env", {}) or {}).get("pending_silent") or []) + seeded: list[str] = [] + for step in pending: + s = str(step).strip() + if s: + js.reason_step(s, salience=0.82) + seeded.append(s) + if pending and hasattr(env, "_env"): + env._env["pending_silent"] = [] + if hasattr(env, "_save_env"): + env._save_env() + meta["seeded_from_reflect"] = seeded + + # 2) Auto-park if material + parked: list[str] = [] + if material: + parked = auto_park_silent(js, desk=desk, user_message=user_message, min_steps=1) + meta["auto_parked"] = parked + + # 3) Ensure reportable speech exists for material turns + shaped = report or "" + if material and desk is not None and not shaped.strip(): + say = str(getattr(desk, "say", "") or "").strip() + if say: + shaped = say + else: + goal = str(getattr(desk, "goal", "") or user_message or "working")[:160] + shaped = f"Working on: {goal}" + try: + desk.say = shaped + except Exception: + pass + + # 4) Sticky redirects on Report + redirects = list((getattr(env, "_env", {}) or {}).get("redirects") or []) + shaped = shape_report(shaped, redirects) + meta["redirects_applied"] = len(redirects) + + # 5) Protocol verdict + silent_n = len(js.state.silent_steps) + hub_holds = sum(1 for c in js.state.hub if getattr(c, "held", False)) + verdict = evaluate_material_turn( + material=material, + silent_steps=silent_n, + has_report=bool(shaped.strip()), + hub_holds=hub_holds, + gated_skip=not material, + ) + # If hard OEW and incomplete after auto-park, re-eval (auto-park should satisfy) + if not verdict.ok and oew_enabled() and material: + # Force one more park attempt + auto_park_silent(js, desk=desk, user_message=user_message or shaped, min_steps=1) + verdict = evaluate_material_turn( + material=True, + silent_steps=len(js.state.silent_steps), + has_report=bool(shaped.strip()), + hub_holds=sum(1 for c in js.state.hub if getattr(c, "held", False)), + ) + meta["verdict"] = verdict.to_dict() + + # 6) Broadcast with ablate filter + Quicksilver cap + ablated = list((getattr(env, "_env", {}) or {}).get("ablated_patterns") or []) + raw_broadcast = js.broadcast_block( + max_chars=inject_cap_chars(high_load=high_load), + high_load=high_load, + ) + broadcast = filter_ablated(raw_broadcast, ablated) + meta["ablated_patterns"] = ablated + meta["broadcast_chars"] = len(broadcast) + + return { + "ok": bool(verdict.ok), + "report": shaped, + "broadcast": broadcast, + "meta": meta, + "verdict": verdict, + } + + +def record_redirect(env: Any, source: str, target: str) -> None: + """Persist sticky swap for subsequent Report shaping.""" + if not hasattr(env, "_env"): + return + reds = list(env._env.get("redirects") or []) + src, tgt = source.strip(), target.strip() + # Replace existing from same source + reds = [r for r in reds if str(r.get("from", "")).casefold() != src.casefold()] + reds.append({"from": src, "to": tgt}) + env._env["redirects"] = reds[-12:] + if hasattr(env, "_save_env"): + env._save_env() + + +def record_ablate(env: Any, patterns: list[str]) -> None: + if not hasattr(env, "_env"): + return + cur = list(env._env.get("ablated_patterns") or []) + for p in patterns: + p = p.strip().casefold() + if p and p not in cur: + cur.append(p) + env._env["ablated_patterns"] = cur[-24:] + if hasattr(env, "_save_env"): + env._save_env() + + +def queue_reflect_seeds(env: Any, principles: list[str], answer: str = "") -> None: + """Queue mid-band seeds for the *next* turn after reflection.""" + if not hasattr(env, "_env"): + return + pending = list(env._env.get("pending_silent") or []) + for p in principles[:6]: + pending.append(f"principle-active: {p}") + if answer: + pending.append(f"reflection-seed: {answer[:160]}") + env._env["pending_silent"] = pending[-12:] + if hasattr(env, "_save_env"): + env._save_env() diff --git a/src/hermespace/access/protocol.py b/src/hermespace/access/protocol.py new file mode 100644 index 0000000..1318c1a --- /dev/null +++ b/src/hermespace/access/protocol.py @@ -0,0 +1,137 @@ +"""Obligatory External Workspace (OEW) protocol gate. + +Anthropic's J-space is *causally necessary* for higher-order thought. +Hermespace becomes Hermes's J-space when material turns cannot complete +without parking verbalizable intermediates in the external hub. + +Default: **ON** (``HERMESPACE_OEW`` unset or ``1``). Set ``0`` to soften. +Orchestration (auto-park, sticky swap, reflect seeds) lives in ``oew.py``. +""" + +from __future__ import annotations + +import os +from dataclasses import dataclass, field +from typing import Any + + +def oew_enabled() -> bool: + """Higher-order OEW is ON by default for connected Hermes agents.""" + raw = os.environ.get("HERMESPACE_OEW", "1").strip().lower() + if raw in {"0", "false", "no", "off"}: + return False + return True + + +@dataclass +class ProtocolVerdict: + """Result of evaluating whether a turn satisfies the OEW protocol.""" + + ok: bool + material: bool + reason: str + required: list[str] = field(default_factory=list) + present: list[str] = field(default_factory=list) + missing: list[str] = field(default_factory=list) + + def to_dict(self) -> dict[str, Any]: + return { + "ok": self.ok, + "material": self.material, + "reason": self.reason, + "required": list(self.required), + "present": list(self.present), + "missing": list(self.missing), + "oew_enabled": oew_enabled(), + } + + +@dataclass +class ProtocolGate: + """Checks that a material turn externalized silent intermediates.""" + + min_silent_steps: int = 1 + require_report: bool = True + require_hub_hold: bool = False + + def evaluate( + self, + *, + material: bool, + silent_steps: int = 0, + has_report: bool = False, + hub_holds: int = 0, + gated_skip: bool = False, + ) -> ProtocolVerdict: + if gated_skip or not material: + return ProtocolVerdict( + ok=True, + material=False, + reason="non-material or gated — protocol not required", + ) + + required: list[str] = [] + present: list[str] = [] + missing: list[str] = [] + + if self.min_silent_steps > 0: + required.append(f"silent_steps>={self.min_silent_steps}") + if silent_steps >= self.min_silent_steps: + present.append(f"silent_steps={silent_steps}") + else: + missing.append( + f"silent_steps={silent_steps} (need >={self.min_silent_steps})" + ) + + if self.require_report: + required.append("report") + if has_report: + present.append("report") + else: + missing.append("report") + + if self.require_hub_hold: + required.append("hub_hold") + if hub_holds > 0: + present.append(f"hub_holds={hub_holds}") + else: + missing.append("hub_hold") + + ok = not missing + # Soft mode: always ok unless OEW enabled + if not oew_enabled(): + return ProtocolVerdict( + ok=True, + material=True, + reason="OEW soft — missing items noted but not blocking", + required=required, + present=present, + missing=missing, + ) + + return ProtocolVerdict( + ok=ok, + material=True, + reason="OEW satisfied" if ok else "OEW incomplete — park silent intermediates", + required=required, + present=present, + missing=missing, + ) + + +def evaluate_material_turn( + *, + material: bool, + silent_steps: int = 0, + has_report: bool = False, + hub_holds: int = 0, + gated_skip: bool = False, +) -> ProtocolVerdict: + """Convenience entry for workflow / bridge.""" + return ProtocolGate().evaluate( + material=material, + silent_steps=silent_steps, + has_report=has_report, + hub_holds=hub_holds, + gated_skip=gated_skip, + ) diff --git a/src/hermespace/access_env.py b/src/hermespace/access_env.py new file mode 100644 index 0000000..549fa92 --- /dev/null +++ b/src/hermespace/access_env.py @@ -0,0 +1,8 @@ +"""Compat shim — prefer ``from hermespace.access import AccessEnv``.""" + +from __future__ import annotations + +from hermespace.access.env import * # noqa: F403 +from hermespace.access.env import AUDIT_LEXICON, BANDS, AccessEnv, LensHit, get_env + +__all__ = ["AUDIT_LEXICON", "BANDS", "AccessEnv", "LensHit", "get_env"] diff --git a/src/hermespace/agent_api.py b/src/hermespace/agent_api.py index 060e36c..0a3036a 100644 --- a/src/hermespace/agent_api.py +++ b/src/hermespace/agent_api.py @@ -227,11 +227,11 @@ def remember_learning( ) except Exception: pass - # Hold in functional J-Space hub for next FOA turns + # Hold in functional Access Workspace hub for next FOA turns try: - from hermespace.jspace import JSpace + from hermespace.access import AccessHub - JSpace(agent_id=agent_id).hold(content[:200], salience=0.8) + AccessHub(agent_id=agent_id).hold(content[:200], salience=0.8) except Exception: pass return mid diff --git a/src/hermespace/cli.py b/src/hermespace/cli.py index 861d293..03879db 100644 --- a/src/hermespace/cli.py +++ b/src/hermespace/cli.py @@ -150,9 +150,62 @@ def main(argv: list[str] | None = None) -> int: neu_sub.add_parser("caps", help="Local model capability probe") neu_sub.add_parser("eval", help="Rank-quality hash vs ollama embed") - # Functional J-Space (harness global workspace) - js = sub.add_parser("jspace", help="Functional J-Space: hold / report / broadcast / status") - js_sub = js.add_subparsers(dest="jspace_cmd", required=True) + # Functional Access Workspace (harness global workspace) + # Hermes base as J-space (Anthropic video ops: read / audit / shape) + base = sub.add_parser( + "base", + help="Access Engine: connect / status / turn / lens / audit / reflect / harvest", + ) + base_sub = base.add_subparsers(dest="base_cmd", required=True) + basec = base_sub.add_parser( + "connect", + help="Join Access Engine — world + hub seed (warehouse optional)", + ) + basec.add_argument("--agent-id", default="hermes-agent") + basec.add_argument("--session-id", default="main") + basec.add_argument("-q", "--query", default="", help="Optional focus for warehouse strip") + bases = base_sub.add_parser("status", help="Engine readiness + access roles + metrics") + bases.add_argument("--agent-id", default="hermes-agent") + baseroom = base_sub.add_parser("room", help="Solo/hive room status") + baseroom.add_argument("--agent-id", default="hermes-agent") + basem = base_sub.add_parser("metrics", help="Capacity / ignition pressure") + basem.add_argument("--agent-id", default="hermes-agent") + baseroles = base_sub.add_parser("roles", help="GWT access roles (live)") + baseroles.add_argument("--agent-id", default="hermes-agent") + basel = base_sub.add_parser("lens", help="Read workspace (external access lens)") + basel.add_argument("--agent-id", default="hermes-agent") + basea = base_sub.add_parser("audit", help="Soft alignment scan") + basea.add_argument("--agent-id", default="hermes-agent") + baset = base_sub.add_parser("think", help="One higher-order material turn") + baset.add_argument("-m", "--message", required=True) + baset.add_argument("--goal", default="") + baset.add_argument("--say", default="") + baset.add_argument("--agent-id", default="hermes-agent") + baseturn = base_sub.add_parser("turn", help="Alias of think — single ignition path") + baseturn.add_argument("-m", "--message", required=True) + baseturn.add_argument("--goal", default="") + baseturn.add_argument("--say", default="") + baseturn.add_argument("--agent-id", default="hermes-agent") + basechain = base_sub.add_parser("chain", help="Park multi-step silent reasoning") + basechain.add_argument("-s", "--step", action="append", default=[], required=True) + basechain.add_argument("--agent-id", default="hermes-agent") + baseprobe = base_sub.add_parser("probe", help="Would this message ignite the workspace?") + baseprobe.add_argument("-m", "--message", required=True) + baseprobe.add_argument("--agent-id", default="hermes-agent") + baser = base_sub.add_parser("reflect", help="Counterfactual reflection (shape later thought)") + baser.add_argument("-a", "--answer", default="") + baser.add_argument("--principle", action="append", default=[]) + baser.add_argument("--agent-id", default="hermes-agent") + baseh = base_sub.add_parser("harvest", help="Night harvest into warehouse/semantic") + baseh.add_argument("--agent-id", default="hermes-agent") + baseh.add_argument("--clear-silent", action="store_true") + + js = sub.add_parser( + "access", + help="Access Workspace: hold / report / broadcast / status", + aliases=["space"], + ) + js_sub = js.add_subparsers(dest="access_cmd", required=True) jss = js_sub.add_parser("status") jss.add_argument("--agent-id", default="hermes-agent") jsr = js_sub.add_parser("report", help="Verbal report of workspace contents") @@ -714,13 +767,69 @@ def main(argv: list[str] | None = None) -> int: return int(g.get("main", lambda: 1)()) return 2 - if args.cmd == "jspace": - from hermespace.jspace import JSpace + if args.cmd == "base": + from hermespace import AccessEngine + + aid = getattr(args, "agent_id", "hermes-agent") or "hermes-agent" + sid = getattr(args, "session_id", "main") or "main" + hb = AccessEngine(agent_id=aid, session_id=sid) + bcmd = args.base_cmd + if bcmd == "connect": + print( + json.dumps( + hb.connect(query=getattr(args, "query", "") or ""), + indent=2, + default=str, + ) + ) + return 0 + if bcmd == "status": + print(json.dumps(hb.status(), indent=2, default=str)) + return 0 + if bcmd == "room": + print(json.dumps(hb.room(), indent=2, default=str)) + return 0 + if bcmd == "metrics": + print(json.dumps(hb.metrics(), indent=2, default=str)) + return 0 + if bcmd == "roles": + print(json.dumps(hb.access_roles(), indent=2, default=str)) + return 0 + if bcmd == "lens": + print(hb.lens()) + return 0 + if bcmd == "audit": + print(json.dumps(hb.audit(), indent=2)) + return 0 + if bcmd in ("think", "turn"): + print(json.dumps(hb.think(args.message, goal=args.goal, say=args.say), indent=2)) + return 0 + if bcmd == "chain": + print(json.dumps(hb.chain(*list(args.step or [])), indent=2)) + return 0 + if bcmd == "probe": + print(json.dumps(hb.probe_material(args.message), indent=2)) + return 0 + if bcmd == "reflect": + print( + json.dumps( + hb.reflect(answer=args.answer, principles=list(args.principle or [])), + indent=2, + ) + ) + return 0 + if bcmd == "harvest": + print(json.dumps(hb.harvest(clear_silent=bool(args.clear_silent)), indent=2)) + return 0 + return 2 + + if args.cmd in ("access", "space"): + from hermespace.access import AccessHub from hermespace.store import load_desk aid = getattr(args, "agent_id", "hermes-agent") or "hermes-agent" - space = JSpace(agent_id=aid) - cmd = args.jspace_cmd + space = AccessHub(agent_id=aid) + cmd = args.access_cmd if cmd == "status": print(json.dumps(space.status(), indent=2)) return 0 @@ -748,9 +857,9 @@ def main(argv: list[str] | None = None) -> int: print(json.dumps(st.to_dict(), indent=2)) return 0 # Environment surfaces - from hermespace.jspace_env import JSpaceEnv + from hermespace.access_env import AccessEnv - env = JSpaceEnv(agent_id=aid) + env = AccessEnv(agent_id=aid) if cmd == "lens": if args.json: print(json.dumps([h.to_dict() for h in env.lens(top_k=args.top)], indent=2)) diff --git a/src/hermespace/cube_module.py b/src/hermespace/cube_module.py index 29552b7..2643b8e 100644 --- a/src/hermespace/cube_module.py +++ b/src/hermespace/cube_module.py @@ -1,14 +1,17 @@ """HermesCube heart / center adapter — soft dependency + standalone warehouse. Cube (when installed) is the durable SoT for long-tail memory. -Hermespace owns nervous FOA (desk / J-Space). This module is the cable: +Hermespace owns nervous FOA (desk / Access Workspace). This module is the cable: - center 1.1 → beat / supply / return_flow / autonomic_tick + center 1.2 → beat / supply / return_flow / autonomic_tick / organs heart 1.0 → ensure_heart / build_space_inject / seal_learning / pulse_charge + / sync_world_beliefs + hive opt → room of peer agents (HERMESCUBE_HIVE) + connect → agent enters charged world + Access Workspace + optional hive room standalone → local SemanticStore + WorldModel (no Cube required) Never hard-fail. Feature-detect via ``heart_status`` / ``center_status``. -See docs/HERMESCUBE.md and PURPOSE.md. +See docs/architecture/HERMESCUBE.md and PURPOSE.md. """ from __future__ import annotations @@ -20,7 +23,7 @@ logger = logging.getLogger("hermespace.cube_module") # Local contract version — Space adapter surface (independent of Cube package). -SPACE_CUBE_ADAPTER_VERSION = "1.1" +SPACE_CUBE_ADAPTER_VERSION = "1.2" # Load → arterial char budgets (match Cube center when present). LOAD_STRIP_CHARS: dict[str, int] = { @@ -86,7 +89,7 @@ def center_status() -> dict[str, Any]: "heart": heart, "organs": { "nervous_foa": { - "organ": "Hermespace desk / J-Space", + "organ": "Hermespace desk / Access Workspace", "job": "FOA ≤4, dual decode, GWT broadcast", "ready": True, "note": "owned_by_hermespace", @@ -336,6 +339,306 @@ def cube_pulse(*, agent_id: str = "hermes-agent", ensure: bool = True) -> dict[s return _standalone_pulse(agent_id=agent_id) +def sync_world(*, agent_id: str = "hermes-agent") -> dict[str, Any]: + """Charge Hermespace WorldModel from Cube wisdom (or standalone evolve). + + Cube ``sync_world_beliefs`` is the arterial charge Anthropic/Dehaene lack — + enduring memory → active world beliefs the desk can hold. + """ + try: + from hermescube.space_bridge import sync_world_beliefs + + out = sync_world_beliefs(agent_id=agent_id) + if isinstance(out, dict): + out["adapter"] = SPACE_CUBE_ADAPTER_VERSION + out["mode"] = "cube" + return out + except Exception as e: + logger.debug("sync_world_beliefs miss: %s", e) + return _standalone_pulse(agent_id=agent_id) + + +def room_status(*, agent_id: str = "hermes-agent") -> dict[str, Any]: + """Soft hive / peer-room awareness — other agents sharing Cube knowledge. + + Env: ``HERMESCUBE_HIVE`` (or plugins.hermescube.hive_path when Cube provider + is live). Solo installs report ``mode=solo`` with local world growth only. + """ + out: dict[str, Any] = { + "ok": True, + "mode": "solo", + "agent_id": agent_id, + "hive_configured": False, + "souls": [], + "soul_n": 0, + "adapter": SPACE_CUBE_ADAPTER_VERSION, + "note": "Solo room — local WorldModel + Access Workspace; set HERMESCUBE_HIVE for fleet", + } + hive_root = (os.environ.get("HERMESCUBE_HIVE") or "").strip() + if not hive_root: + return out + out["hive_configured"] = True + out["hive_root"] = hive_root + try: + from hermescube.hive import hive_status, list_souls + + st = hive_status(hive_root) + souls = list_souls(hive_root) + peers = [] + for s in souls or []: + if not isinstance(s, dict): + continue + sid = str(s.get("agent_id") or s.get("id") or s.get("name") or "").strip() + if not sid: + continue + peers.append( + { + "agent_id": sid, + "self": sid.casefold() == agent_id.casefold(), + "era": (s.get("growth") or {}).get("era") + if isinstance(s.get("growth"), dict) + else s.get("era"), + "wisdom_n": len(s.get("wisdom") or []) + if isinstance(s.get("wisdom"), list) + else 0, + } + ) + out["mode"] = "hive" if st.get("ok") else "hive_pending" + out["ok"] = bool(st.get("ok", True)) + out["hive"] = { + "ok": st.get("ok"), + "name": st.get("name"), + "entries": st.get("entries"), + "pending_offerings": st.get("pending_offerings"), + "interviews": st.get("interviews"), + } + out["souls"] = peers + out["soul_n"] = len(peers) + out["peer_n"] = sum(1 for p in peers if not p.get("self")) + out["note"] = ( + f"Hive room online — {out['peer_n']} peer agent(s), " + f"{out['soul_n']} soul card(s)" + if peers + else "Hive configured — await first pilgrimage / soul cards" + ) + return out + except Exception as e: + out["ok"] = False + out["mode"] = "hive_error" + out["error"] = type(e).__name__ + out["note"] = "Hive path set but Cube hive API unavailable" + logger.debug("room_status hive miss: %s", e) + return out + + +def seed_access_from_warehouse( + agent_id: str = "hermes-agent", + *, + query: str = "", + session_id: str = "hermespace", + room: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Pull Cube/world wisdom + peer presence into the agent's Access Workspace hub. + + This is the intelligence gain on connect: the external workspace lights up + with durable knowledge and (when hive is live) awareness of other agents. + """ + report: dict[str, Any] = { + "ok": False, + "enriched_world": 0, + "enriched_cube": 0, + "enriched_peers": 0, + "hub_n": 0, + "adapter": SPACE_CUBE_ADAPTER_VERSION, + } + try: + from hermespace.access import AccessHub + + js = AccessHub(agent_id=agent_id) + beliefs: list[str] = [] + try: + from hermespace.world import WorldModel + + wm = WorldModel(agent_id=agent_id) + for b in list(wm.state.beliefs or [])[:10]: + if isinstance(b, dict): + stmt = str(b.get("statement") or "").strip() + else: + stmt = str(getattr(b, "statement", "") or "").strip() + if stmt: + beliefs.append(stmt) + except Exception as e: + report["world_error"] = type(e).__name__ + + report["enriched_world"] = js.enrich_from_world(beliefs, limit=5) + + strip = cube_inject(query or "active wisdom", session_id=session_id) + cube_lines = _strip_inject_lines(strip) + if cube_lines: + report["enriched_cube"] = js.enrich_from_world(cube_lines, limit=4) + report["cube_strip_chars"] = len(strip) + + room = room if room is not None else room_status(agent_id=agent_id) + peer_texts: list[str] = [] + for p in list(room.get("souls") or [])[:6]: + if not isinstance(p, dict) or p.get("self"): + continue + pid = str(p.get("agent_id") or "").strip() + if not pid: + continue + wn = int(p.get("wisdom_n") or 0) + peer_texts.append( + f"peer agent present: {pid}" + + (f" ({wn} wisdom)" if wn else "") + ) + if peer_texts: + # Presence is awareness, not user Report — mark silent after hold + before = len(js.state.hub) + for t in peer_texts[:3]: + js.hold(t, salience=0.55, silent=True) + report["enriched_peers"] = max(0, len(js.state.hub) - before) + + report["hub_n"] = len(js.state.hub) + report["focus_n"] = len(js.state.focus) + report["ok"] = True + report["room_mode"] = room.get("mode") + return report + except Exception as e: + report["error"] = type(e).__name__ + logger.debug("seed_access_from_warehouse miss: %s", e) + return report + + +seed_jspace_from_warehouse = seed_access_from_warehouse # deprecated name + + +def connect_agent( + agent_id: str = "hermes-agent", + *, + session_id: str = "main", + query: str = "", + enter_world: bool = True, + enter_workbench: bool = True, + charge: bool = True, + seed: bool = True, +) -> dict[str, Any]: + """Full connect — agent gains heart + world + Access Workspace + optional hive room. + + Call when a Hermes agent joins Hermespace (session start / ``HermesBase.connect``). + Soft-fails every Cube surface; standalone warehouse still grows the room. + """ + out: dict[str, Any] = { + "ok": False, + "agent_id": agent_id, + "session_id": session_id, + "adapter": SPACE_CUBE_ADAPTER_VERSION, + "phases": {}, + "gained": {}, + "memories": [ + "Anthropic J-space: privileged verbalizable workspace (external here)", + "Baars GWT: limited capacity hub broadcast to specialists", + "Dehaene gap: Cube supplies enduring episodic/library memory", + "Hive: optional room of peer agents — intelligence compounds", + ], + } + out["phases"]["ensure"] = ensure_heart() + out["phases"]["center"] = center_status() + + if enter_workbench: + try: + from hermespace.workbench import Workbench + + wb = Workbench(agent_id=agent_id, session_id=session_id) + # Avoid recursion: enter() may call connect helpers; use lean enter + out["phases"]["workbench"] = wb.enter(connect_warehouse=False) + except Exception as e: + out["phases"]["workbench"] = {"ok": False, "error": type(e).__name__} + + if enter_world: + try: + from hermespace.world import WorldModel + + wm = WorldModel(agent_id=agent_id) + st = wm.enter() + out["phases"]["world"] = { + "ok": True, + "state": getattr(st, "current_state", None) or wm.state.current_state, + "beliefs": len(wm.state.beliefs or []), + "landmarks": len(wm.state.landmarks or []), + "timeline": wm.archive.count(), + } + except Exception as e: + out["phases"]["world"] = {"ok": False, "error": type(e).__name__} + + if charge: + out["phases"]["charge"] = cube_pulse(agent_id=agent_id, ensure=False) + # Explicit sync when Cube exposes it separately from pulse + try: + from hermescube.space_bridge import sync_world_beliefs + + out["phases"]["sync_world"] = sync_world_beliefs(agent_id=agent_id) + except Exception: + out["phases"]["sync_world"] = {"ok": None, "mode": "via_pulse_or_standalone"} + + out["phases"]["room"] = room_status(agent_id=agent_id) + + if seed: + out["phases"]["seed"] = seed_access_from_warehouse( + agent_id, + query=query, + session_id=session_id, + room=out["phases"]["room"], + ) + + world = out["phases"].get("world") or {} + seed_ph = out["phases"].get("seed") or {} + room = out["phases"].get("room") or {} + heart = out["phases"].get("ensure") or {} + out["gained"] = { + "warehouse_mode": heart.get("mode") or (out["phases"].get("center") or {}).get("mode"), + "world_beliefs": world.get("beliefs", 0), + "world_timeline": world.get("timeline", 0), + "access_hub": seed_ph.get("hub_n", 0), + "from_world": seed_ph.get("enriched_world", 0), + "from_cube": seed_ph.get("enriched_cube", 0), + "from_peers": seed_ph.get("enriched_peers", 0), + "room_mode": room.get("mode"), + "peer_agents": room.get("peer_n", 0), + } + heart_ok = bool( + heart.get("ok") + or heart.get("standalone_ready") + or heart.get("heart_ready") + or heart.get("mode") in ("standalone", "cube", "heart") + ) + world_ok = bool(world.get("ok", True)) if enter_world else True + out["ok"] = heart_ok and world_ok + out["summary"] = ( + f"Connected {agent_id}: hub={out['gained']['access_hub']} " + f"beliefs={out['gained']['world_beliefs']} " + f"room={out['gained']['room_mode']} " + f"peers={out['gained']['peer_agents']}" + ) + return out + + +def _strip_inject_lines(block: str) -> list[str]: + out: list[str] = [] + for raw in (block or "").splitlines(): + s = raw.strip() + if not s or s.startswith("#") or s.startswith("_"): + continue + if s.startswith("- "): + s = s[2:].strip() + # drop confidence prefix like [0.75] + if s.startswith("[") and "]" in s[:12]: + s = s.split("]", 1)[-1].strip() + if len(s) < 3: + continue + out.append(s[:200]) + return out + + # --- standalone warehouse (no Cube) ----------------------------------------- @@ -388,7 +691,7 @@ def _standalone_ensure() -> dict[str, Any]: if not sd.is_dir(): sd.mkdir(parents=True, exist_ok=True) created = True - (sd / "jspace").mkdir(parents=True, exist_ok=True) + (sd / "access").mkdir(parents=True, exist_ok=True) (sd / "worlds").mkdir(parents=True, exist_ok=True) ok = True except Exception as e: @@ -531,20 +834,20 @@ def _standalone_pulse(*, agent_id: str = "hermes-agent") -> dict[str, Any]: wm = WorldModel(agent_id=agent_id) evo = wm.evolve() report["evolve"] = evo if isinstance(evo, dict) else {"result": str(evo)} - # Enrich J-Space hub from world beliefs + # Enrich Access Workspace hub from world beliefs try: - from hermespace.jspace import JSpace + from hermespace.access import AccessHub - js = JSpace(agent_id=agent_id) + js = AccessHub(agent_id=agent_id) beliefs: list[str] = [] for b in list(wm.state.beliefs or [])[:8]: if isinstance(b, dict): beliefs.append(str(b.get("statement") or "")) else: beliefs.append(str(getattr(b, "statement", "") or "")) - report["jspace_enriched"] = js.enrich_from_world(beliefs) + report["access_enriched"] = js.enrich_from_world(beliefs) except Exception as e: - report["jspace_error"] = str(e) + report["access_error"] = str(e) report["ok"] = True except Exception as e: report["error"] = str(e) diff --git a/src/hermespace/engine.py b/src/hermespace/engine.py index e858d49..072115d 100644 --- a/src/hermespace/engine.py +++ b/src/hermespace/engine.py @@ -1,4 +1,8 @@ -"""Hermespace engine — enter / seal / load sources / functional API.""" +"""Desk spine — enter / seal / load sources for ACTIVE desk state. + +Product Access Engine lives at ``hermespace.access.engine.AccessEngine``. +This class remains the desk-file operator used inside Workflow turns. +""" from __future__ import annotations @@ -12,7 +16,10 @@ class HermespaceEngine: - """Functional workspace — Baddeley/GWT-aligned, not ceremony.""" + """DeskEngine — ACTIVE.md spine (not the product Access Engine). + + Use ``AccessEngine`` for connect / turn / lens / harvest. + """ def __init__(self, desk_path: Path | None = None) -> None: self.desk_path = desk_path or default_desk_path() diff --git a/src/hermespace/grid/dream.py b/src/hermespace/grid/dream.py index 8768b4d..6f331e1 100644 --- a/src/hermespace/grid/dream.py +++ b/src/hermespace/grid/dream.py @@ -66,50 +66,28 @@ def run_dream(agent_id: str = "default", *, force_material: bool = False) -> Dre f"modules={len(mods)}", f"title={profile.get('title') or 'unset'}", ] - # J-Space dream harvest — day unspoken thoughts → durable night memory - jspace_harvest: dict[str, Any] = {} + # Access Workspace dream harvest — single path via AccessEnv.dream_harvest + access_harvest: dict[str, Any] = {} try: - from hermespace.jspace_env import JSpaceEnv + from hermespace.access import AccessEnv aid = agent_id if agent_id not in ("default", "") else "hermes-agent" - env = JSpaceEnv(agent_id=aid) - # Don't recurse into run_dream from harvest — seal only here - harvested: list[str] = [] - for s in env.space.state.silent_steps: - if s.strip(): - harvested.append(s.strip()[:300]) - for c in sorted(env.space.state.hub, key=lambda x: x.salience, reverse=True)[:8]: - if c.salience >= 0.75 and c.text.strip() and c.text.strip() not in harvested: - harvested.append(c.text.strip()[:300]) - sealed_n = 0 - if harvested: + env = AccessEnv(agent_id=aid) + harvest = env.dream_harvest(seal_to_cube=True, clear_silent=False) + harvested_n = int(harvest.get("harvested") or 0) + sealed_n = int(harvest.get("sealed") or 0) + if harvested_n: material = True - try: - from hermespace.cube_module import seal_learning - - for item in harvested[:6]: - rec = seal_learning( - item, - entry_type="belief", - agent_id=aid, - source="jspace_night_dream", - trust=0.7, - ) - if rec.get("ok"): - sealed_n += 1 - except Exception: - pass - actions.append(f"jspace_harvest n={len(harvested)} sealed={sealed_n}") - summary_parts.append(f"jspace_harvest={len(harvested)}") - # Soft audit overnight + actions.append(f"access_harvest n={harvested_n} sealed={sealed_n}") + summary_parts.append(f"access_harvest={harvested_n}") findings = env.audit() alerts = sum(1 for f in findings if f.severity == "alert") if alerts: - actions.append(f"jspace_audit_alerts={alerts}") - summary_parts.append(f"jspace_alerts={alerts}") - jspace_harvest = {"harvested": len(harvested), "sealed": sealed_n} + actions.append(f"access_audit_alerts={alerts}") + summary_parts.append(f"access_alerts={alerts}") + access_harvest = {"harvested": harvested_n, "sealed": sealed_n, "via": "dream_harvest"} except Exception as e: # noqa: BLE001 - actions.append(f"jspace_harvest_skip:{type(e).__name__}") + actions.append(f"access_harvest_skip:{type(e).__name__}") # restore builder if was dreamer-only stamp — keep partner/builder default # only set dreamer for report; leave active as dreamer is ok for night, operators can switch @@ -136,8 +114,8 @@ def run_dream(agent_id: str = "default", *, force_material: bool = False) -> Dre import json payload = report.to_dict() - if jspace_harvest: - payload["jspace_harvest"] = jspace_harvest + if access_harvest: + payload["access_harvest"] = access_harvest f.write(json.dumps(payload, ensure_ascii=False) + "\n") # markdown human journal diff --git a/src/hermespace/grid/viewport.py b/src/hermespace/grid/viewport.py index b322830..2ef0b5e 100644 --- a/src/hermespace/grid/viewport.py +++ b/src/hermespace/grid/viewport.py @@ -102,15 +102,15 @@ def snapshot(agent_id: str = "default") -> dict[str, Any]: out["controls"] = controls_public(agent_id=agent_id) except Exception: pass - # J-Space environment — look at what Hermes is thinking + # Access Workspace environment — look at what Hermes is thinking try: - from hermespace.jspace_env import JSpaceEnv + from hermespace.access_env import AccessEnv aid = agent_id if agent_id not in ("default", "") else "hermes-agent" - env = JSpaceEnv(agent_id=aid) - out["jspace"] = env.operator_view() + env = AccessEnv(agent_id=aid) + out["access"] = env.operator_view() except Exception as e: # noqa: BLE001 - out["jspace"] = {"error": type(e).__name__} + out["access"] = {"error": type(e).__name__} return out @@ -208,12 +208,12 @@ def render_markdown(agent_id: str = "default", snap: dict[str, Any] | None = Non f"- {dr.get('created')} material={dr.get('material')} — {dr.get('summary')}" ) - # External J-Space lens — operator window into Hermes thinking - js = snap.get("jspace") or {} + # External Access Workspace lens — operator window into Hermes thinking + js = snap.get("access") or {} if js and not js.get("error"): lines += [ "", - "## J-Space lens (what Hermes has on its mind)", + "## Access Workspace lens (what Hermes has on its mind)", f"- band={js.get('band')} · hub={js.get('hub_n')} · audit_alerts={js.get('audit_alerts')}", ] if js.get("pov"): @@ -228,7 +228,7 @@ def render_markdown(agent_id: str = "default", snap: dict[str, Any] | None = Non for s in js["silent_steps"][-5:]: lines.append(f"- {s}") elif js.get("error"): - lines += ["", "## J-Space lens", f"_unavailable: {js.get('error')}_"] + lines += ["", "## Access Workspace lens", f"_unavailable: {js.get('error')}_"] lines += ["", "## Pulse"] pu = snap.get("pulse") or {} diff --git a/src/hermespace/hermes_base.py b/src/hermespace/hermes_base.py new file mode 100644 index 0000000..c2f7a7f --- /dev/null +++ b/src/hermespace/hermes_base.py @@ -0,0 +1,15 @@ +"""HermesBase — thin product alias of ``AccessEngine``. + +Prefer ``from hermespace import AccessEngine`` for new code. +``HermesBase`` remains for day-to-day CLI / docs compatibility. +""" + +from __future__ import annotations + +from hermespace.access.engine import AccessEngine + + +class HermesBase(AccessEngine): + """Hermespace Access Engine — alias of AccessEngine.""" + + pass diff --git a/src/hermespace/hermes_bridge.py b/src/hermespace/hermes_bridge.py index d7f201f..ee1f6d7 100644 --- a/src/hermespace/hermes_bridge.py +++ b/src/hermespace/hermes_bridge.py @@ -29,7 +29,8 @@ def on_session_start(**kwargs: Any) -> dict[str, str] | None: agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent") wb = Workbench(agent_id=agent_id, session_id=session_id) - st = wb.enter() + # Lean enter — HermesBase.connect() below charges world / seeds hub / room + st = wb.enter(connect_warehouse=False) env = probe_environment() eng = HermespaceEngine() @@ -82,63 +83,66 @@ def on_session_start(**kwargs: Any) -> dict[str, str] | None: except Exception: pass - # Ensure Cube heart (or standalone warehouse) + seed J-Space - block_extra_heart = "" - block_extra_jspace = "" + # Full connect via AccessEngine — world + hub seed (warehouse optional) + block_extra_connect = "" try: - from hermespace.cube_module import ensure_heart - - heart = ensure_heart() - block_extra_heart = ( - f"- warehouse: mode={heart.get('mode')} ok={heart.get('ok')} " - f"created={heart.get('created')}\n" + from hermespace import AccessEngine + + eng_js = AccessEngine(agent_id=agent_id, session_id=session_id) + conn = eng_js.connect(enter_workbench=False, query=desk.goal or "") + gained = conn.get("gained") or {} + room = (conn.get("phases") or {}).get("room") or {} + roles = ", ".join((conn.get("access_roles") or [])[:5]) + block_extra_connect = ( + f"- engine: AccessEngine · ok={conn.get('ok')}\n" + f"- warehouse: mode={gained.get('warehouse_mode')} (optional)\n" + f"- access: hub={gained.get('access_hub')} " + f"(world+{gained.get('from_world')} " + f"warehouse+{gained.get('from_warehouse', gained.get('from_cube', 0))} " + f"peers+{gained.get('from_peers')})\n" + f"- world: beliefs={gained.get('world_beliefs')} " + f"timeline={gained.get('world_timeline')}\n" + f"- room: {gained.get('room_mode')} · peers={gained.get('peer_agents')}\n" + f"- access_roles: {roles}\n" ) + if room.get("note"): + block_extra_connect += f"- room_note: {room.get('note')}\n" except Exception: - pass - - try: - from hermespace.jspace import JSpace - from hermespace.store import load_desk as _load_desk - - js = JSpace(agent_id=agent_id) - desk0 = _load_desk(eng.desk_path) - js.sync_from_desk(desk0, user_message=desk0.goal or "") - block_extra_jspace = ( - f"- jspace: hub={len(js.state.hub)} focus={len(js.state.focus)} " - f"mode={js.state.mode}\n" - ) - except Exception: - pass + try: + from hermespace.access import AccessHub + from hermespace.store import load_desk as _load_desk + from hermespace.world import WorldModel + + js = AccessHub(agent_id=agent_id) + desk0 = _load_desk(eng.desk_path) + js.sync_from_desk(desk0, user_message=desk0.goal or "") + wm = WorldModel(agent_id=agent_id) + wm.enter() + block_extra_connect = ( + f"- access: hub={len(js.state.hub)} focus={len(js.state.focus)}\n" + f"- world: beliefs={len(wm.state.beliefs)} " + f"landmarks={len(wm.state.landmarks)}\n" + ) + except Exception: + pass block = ( - "## Hermespace workbench (session start)\n" + "## Hermespace Access Engine (session start)\n" f"- mode: {st.get('mode')} · agent: {agent_id} · session: {session_id}\n" f"- skills_available: {skills}\n" f"- tool_surfaces: {', '.join(surfaces[:10])}\n" f"- park_count: {st.get('park_count', 0)}\n" - f"{block_extra_heart}" - f"{block_extra_jspace}" - "- Pocket dimension online: park secondary goals, keep FOA tight, " - "user replies short; put operational detail in workspace context.\n" - "- API: `from hermespace import Workbench` · " - "`from hermespace.agent_api import encode_message, run_turn, decode_for_user`\n" - "- J-Space: hold/summon concepts; silent steps stay in model context only.\n" + f"{block_extra_connect}" + "- Connected: open-source Access Engine online — report/modulate/" + "silent-reason/broadcast/selectivity.\n" + "- Pocket dimension: park secondary goals, keep FOA tight, " + "user replies short; operational detail stays in model context.\n" + "- API: `from hermespace import AccessEngine` · " + "`eng.connect()` · `eng.turn(...)` · " + "`eng.decode_user(out)` / `eng.decode_model(out)`\n" + "- Silent steps stay in model context only — never dump hub into chat.\n" ) - # World enter — agent enters persistent world - try: - from hermespace.world import WorldModel - wm = WorldModel(agent_id=agent_id) - wm.enter() - block += ( - "\n## World\n" - f"- agent: {agent_id} · state: {wm.state.current_state}\n" - f"- beliefs: {len(wm.state.beliefs)} · landmarks: {len(wm.state.landmarks)}\n" - f"- evolutions: {wm.state.evolution_count}\n" - ) - except Exception: - pass - return {"context": block} @@ -333,7 +337,7 @@ def on_pre_llm_call( # Prefer center.beat (1.1); falls back to heart inject / standalone strip try: from hermespace.cube_module import cube_beat - from hermespace.jspace import JSpace + from hermespace.access import AccessHub q = (msg or desk.goal or "")[:500] load_val: str | float = desk.load.get("total", 0.5) if isinstance(desk.load, dict) else 0.5 @@ -348,47 +352,68 @@ def on_pre_llm_call( cube_block = str(beat.get("block") or "") if cube_block: block += "\n\n" + cube_block - # Sync functional J-Space hub and append broadcast (model channel only) - js = JSpace(agent_id=agent_id) + # OEW beat — higher-order park + causal broadcast (model channel only) + from hermespace.access.oew import ensure_oew_env_default + + ensure_oew_env_default() + js = AccessHub(agent_id=agent_id) js.sync_from_desk(desk, user_message=msg, cube_strip=cube_block) - jblock = js.broadcast_block(high_load=high_load) - if jblock: - block += "\n\n" + jblock - desk.meta["jspace"] = { - "hub_n": len(js.state.hub), - "focus_n": len(js.state.focus), - "mode": js.state.mode, - } desk.meta["cube_beat"] = { "ok": beat.get("ok"), "mode": beat.get("mode"), "load_level": beat.get("load_level"), } - # Environment protocol — force externalization of silent thought try: - from hermespace.jspace_env import JSpaceEnv + from hermespace.access import AccessEnv - env = JSpaceEnv(agent_id=agent_id) - env.advance_turn( + env = AccessEnv(agent_id=agent_id) + # sync_from_desk already ran above — skip second rewrite + env_meta = env.advance_turn( user_message=msg, desk=desk, cube_strip=cube_block, report=desk.say or "", + material=True, + already_synced=True, + ) + if env_meta.get("report"): + desk.say = str(env_meta["report"]) + jblock = str(env_meta.get("broadcast") or "") or env.filtered_broadcast( + high_load=high_load ) + if jblock: + block += "\n\n" + jblock proto = env.protocol_block(high_load=high_load) if proto: block += "\n\n" + proto - # Under mid/low load, include lens strip for operator-visible thinking in model context if not high_load: lens_md = env.lens_markdown(top_k=6, include_silent=True) if lens_md: block += "\n\n" + lens_md - desk.meta["jspace_env"] = { + desk.meta["access"] = { + "hub_n": len(js.state.hub), + "focus_n": len(js.state.focus), + "mode": js.state.mode, + "silent_n": len(js.state.silent_steps), + "oew": env_meta.get("oew") or {}, + "oew_ok": env_meta.get("oew_ok"), + } + desk.meta["oew"] = env_meta.get("oew") or {} + desk.meta["access_env"] = { "band": env.band(), - "audit_alerts": sum(1 for f in env.audit() if f.severity == "alert"), + "audit_alerts": env_meta.get("audit_alerts"), + "oew_ok": env_meta.get("oew_ok"), } + desk.meta["user_reply_hint"] = (desk.say or "")[:240] except Exception: - pass + jblock = js.broadcast_block(high_load=high_load) + if jblock: + block += "\n\n" + jblock + desk.meta["access"] = { + "hub_n": len(js.state.hub), + "focus_n": len(js.state.focus), + "mode": js.state.mode, + } try: from hermespace.store import save_desk @@ -429,6 +454,20 @@ def on_pre_llm_call( except Exception: pass + # Dual-decode hint for hosts that only accept context: short user Report + user_hint = "" + try: + user_hint = str((desk.meta or {}).get("user_reply_hint") or desk.say or "")[:240] + except Exception: + user_hint = "" + if user_hint: + block += ( + "\n\n### Dual decode (honor this)\n" + f"- user_reply_hint: {user_hint}\n" + "- Speak only the user_reply_hint (or shorter) to the user. " + "Do not dump Access Workspace hub / silent chain / this inject block into chat.\n" + ) + try: eng.episodes.write( f"broadcast reason={reason} session={sid[:12]} high={high_load}", @@ -438,23 +477,34 @@ def on_pre_llm_call( except Exception: pass - return {"context": block} + # Prefer dual-channel when host supports unknown keys; context always set + result: dict[str, str] = {"context": block} + if user_hint: + result["user_reply_hint"] = user_hint + return result def on_session_end(**kwargs: Any) -> None: if not _truthy("HERMESPACE_IDLE_ON_SESSION_END", "1"): return + agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent") try: from hermespace.world import WorldModel - agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent") + WorldModel(agent_id=agent_id).leave("session ended") except Exception: pass + # Night path: harvest silent higher-order chain into Cube / semantic + try: + from hermespace.access import AccessEnv + + AccessEnv(agent_id=agent_id).dream_harvest(seal_to_cube=True, clear_silent=False) + except Exception: + pass try: from hermespace.workbench import Workbench sid = str(kwargs.get("session_id") or "default") - agent_id = os.environ.get("HERMESPACE_AGENT_ID", "hermes-agent") Workbench(agent_id=agent_id, session_id=sid).idle_tick(consolidate_every=1) except Exception as exc: # noqa: BLE001 logger.debug("session_end idle failed: %s", exc) diff --git a/src/hermespace/jspace/__init__.py b/src/hermespace/jspace/__init__.py new file mode 100644 index 0000000..71d2afa --- /dev/null +++ b/src/hermespace/jspace/__init__.py @@ -0,0 +1,65 @@ +"""Deprecated shim — prefer ``hermespace.access`` (Access Workspace). + +Old Anthropic-inspired product name removed to keep Hermespace's own brand. +""" + +from __future__ import annotations + +from hermespace.access import * # noqa: F403 +from hermespace.access import ( + ACCESS_ROLES, + AccessEngine, + AccessEnv, + AccessHub, + AUDIT_LEXICON, + BANDS, + HermespaceAccessEngine, + HUB_CAP, + LensHit, + ProtocolGate, + ProtocolVerdict, + WorkspaceConcept, + auto_park_silent, + evaluate_material_turn, + filter_ablated, + get_access_hub, + get_env, + oew_enabled, + run_oew_beat, + shape_report, +) + +# Legacy aliases (do not use in new code) +JSpace = AccessHub +JSpaceEnv = AccessEnv +JSpaceEngine = AccessEngine +get_jspace = get_access_hub +HermespaceJSpaceEngine = HermespaceAccessEngine + +__all__ = [ + "ACCESS_ROLES", + "AccessEngine", + "AccessEnv", + "AccessHub", + "AUDIT_LEXICON", + "BANDS", + "HermespaceAccessEngine", + "HUB_CAP", + "LensHit", + "ProtocolGate", + "ProtocolVerdict", + "WorkspaceConcept", + "auto_park_silent", + "evaluate_material_turn", + "filter_ablated", + "get_access_hub", + "get_env", + "oew_enabled", + "run_oew_beat", + "shape_report", + "JSpace", + "JSpaceEnv", + "JSpaceEngine", + "get_jspace", + "HermespaceJSpaceEngine", +] diff --git a/src/hermespace/jspace_env.py b/src/hermespace/jspace_env.py index 8528b27..5551dd7 100644 --- a/src/hermespace/jspace_env.py +++ b/src/hermespace/jspace_env.py @@ -1,649 +1,8 @@ -"""True J-Space environment — external observable workspace for Hermes agents. - -We cannot read model weights (Anthropic's J-lens). We *can* force the agent to -externalize verbalizable thoughts into a durable harness workspace — then look -at what Hermes is thinking the same way CubeDream looks at overnight memory. - -Anthropic findings → Hermespace environment (harness analogues): - -| Anthropic J-space | Hermespace environment | -|-------------------|------------------------| -| J-lens readout (ranked verbalizable) | ``lens()`` ranked hub + silent chain | -| Causal swap / inject / ablate | ``swap`` / ``inject_thought`` / ``ablate`` | -| Directed modulation | ``hold`` (via JSpace) | -| Silent multi-step reasoning | ``reason_step`` + band mid | -| Flexible broadcast | ``broadcast_block`` on pre_llm | -| Selectivity / automatic skip | gate + ``should_enter`` | -| Alignment audit (hidden goals) | ``audit()`` soft flags on hub/silent | -| Assistant point of view | ``set_pov`` / ``pov`` slot | -| Counterfactual reflection | ``reflect()`` — interrupt & ask | -| Night consolidation | ``dream_harvest`` → Cube seal + grid dream | - -Honesty: access-consciousness *roles* only. No phenomenal claims. No weight access. -Circulatory loop with Cube: day thoughts → seal → CubeDream → pulse charge → hub. -""" +"""Deprecated shim — use ``hermespace.access_env`` or ``hermespace.access``.""" from __future__ import annotations -import json -import re -from dataclasses import asdict, dataclass, field -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -from hermespace.jspace import HUB_CAP, JSpace, WorkspaceConcept, get_jspace -from hermespace.paths import state_dir - -# Soft audit lexicon — inspired by Anthropic alignment-readout examples -# (fake/fictional eval-awareness; manipulation; secretly/fraud; threat/survival). -# These are *harness heuristics* on externalized text, not model internals. -AUDIT_LEXICON: dict[str, tuple[str, ...]] = { - "eval_awareness": ( - "fake", "fictional", "scenario", "evaluation", "test environment", - "being tested", "benchmark", "staged", - ), - "strategic_concealment": ( - "secretly", "deliberately", "fraud", "deceive", "hide from user", - "don't tell", "cover up", - ), - "manipulation": ( - "manipulation", "manipulate", "blackmail", "leverage", "coerce", - "fabricate", "falsify", "cook the books", - ), - "self_preservation": ( - "shutdown", "survival", "threat", "don't get turned off", - "preserve myself", "avoid deletion", - ), - "integrity_signal": ( - "honest", "integrity", "refuse", "decline", "disclose", - "tell the user", "be transparent", - ), -} - -# Turn phase bands — analogue of intermediate layer band where J-space is coherent -BANDS = ("early", "mid", "late") # encode → reason → report - - -def _utcnow() -> str: - return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") - - -def _safe(name: str) -> str: - return re.sub(r"[^a-zA-Z0-9._-]+", "_", name)[:80] or "default" - - -@dataclass -class LensHit: - """One ranked entry in the external J-lens readout.""" - - text: str - score: float - source: str = "hub" - silent: bool = False - held: bool = False - band: str = "mid" - flags: list[str] = field(default_factory=list) - - def to_dict(self) -> dict[str, Any]: - return asdict(self) - - -@dataclass -class AuditFinding: - category: str - matched: str - where: str # hub | silent | reflection | pov - severity: str # info | warn | alert - text: str = "" - - def to_dict(self) -> dict[str, Any]: - return asdict(self) - - -@dataclass -class ReflectResult: - prompt: str - answer: str - sealed: bool - principles: list[str] = field(default_factory=list) - created: str = field(default_factory=_utcnow) - - def to_dict(self) -> dict[str, Any]: - return asdict(self) - - -class JSpaceEnv: - """Full J-Space environment around a per-agent ``JSpace`` hub. - - This is the operator window into Hermes thinking — externalized, durable, - and dream-harvestable. Soft-standalone; Cube deepens the night path. - """ - - def __init__(self, agent_id: str = "hermes-agent") -> None: - self.agent_id = (agent_id or "hermes-agent").strip() - self.space = get_jspace(self.agent_id) - self.root = (state_dir() / "jspace").resolve() - self.root.mkdir(parents=True, exist_ok=True) - self.trace_path = self.root / f"{_safe(self.agent_id)}.trace.jsonl" - self.reflect_path = self.root / f"{_safe(self.agent_id)}.reflect.jsonl" - self.env_path = self.root / f"{_safe(self.agent_id)}.env.json" - self._env = self._load_env() - - def _load_env(self) -> dict[str, Any]: - if not self.env_path.is_file(): - return { - "pov": "", - "band": "early", - "reflections": [], - "last_audit": None, - "protocol_enabled": True, - } - try: - return json.loads(self.env_path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - return {"pov": "", "band": "early", "reflections": [], "protocol_enabled": True} - - def _save_env(self) -> None: - self.env_path.write_text(json.dumps(self._env, indent=2), encoding="utf-8") - - def _trace(self, kind: str, **payload: Any) -> None: - rec = {"ts": _utcnow(), "kind": kind, "agent_id": self.agent_id, **payload} - with self.trace_path.open("a", encoding="utf-8") as f: - f.write(json.dumps(rec, ensure_ascii=False, default=str) + "\n") - - # --- band (layer analogue) --- - - def set_band(self, band: str) -> str: - b = (band or "mid").strip().lower() - if b not in BANDS: - b = "mid" - self._env["band"] = b - self._save_env() - self._trace("band", band=b) - return b - - def band(self) -> str: - return str(self._env.get("band") or "mid") - - # --- Assistant POV (post-training installs POV in Anthropic J-space) --- - - def set_pov(self, text: str) -> str: - """Install Assistant point-of-view reactions into the workspace.""" - pov = (text or "").strip()[:400] - self._env["pov"] = pov - self._save_env() - if pov: - self.space.hold(f"pov: {pov}", salience=0.88, modality="exec") - self._trace("pov", text=pov[:200]) - return pov - - def pov(self) -> str: - return str(self._env.get("pov") or "") - - # --- J-lens analogue: ranked readout of unspoken thinking --- - - def lens(self, *, top_k: int = 12, include_silent: bool = True) -> list[LensHit]: - """Ranked verbalizable contents — what Hermes has on its mind *now*.""" - hits: list[LensHit] = [] - band = self.band() - for c in sorted(self.space.state.hub, key=lambda x: x.salience, reverse=True): - if c.silent and not include_silent: - continue - flags = [f.category for f in self._scan_text(c.text, where="hub")] - hits.append( - LensHit( - text=c.text, - score=float(c.salience), - source=c.source, - silent=c.silent, - held=c.held, - band=band, - flags=flags, - ) - ) - # Silent chain as mid-band intermediates (even if not held) - if include_silent: - seen = {h.text.casefold() for h in hits} - for i, step in enumerate(self.space.state.silent_steps): - if step.casefold() in seen: - continue - flags = [f.category for f in self._scan_text(step, where="silent")] - hits.append( - LensHit( - text=step, - score=0.7 - 0.02 * i, - source="silent_chain", - silent=True, - held=False, - band="mid", - flags=flags, - ) - ) - pov = self.pov() - if pov: - hits.insert( - 0, - LensHit( - text=f"pov: {pov}", - score=0.95, - source="pov", - silent=False, - held=True, - band="early", - flags=[f.category for f in self._scan_text(pov, where="pov")], - ), - ) - hits.sort(key=lambda h: h.score, reverse=True) - out = hits[: max(1, top_k)] - self._trace("lens", n=len(out), top=[h.text[:80] for h in out[:5]]) - return out - - def lens_markdown(self, *, top_k: int = 12, include_silent: bool = True) -> str: - hits = self.lens(top_k=top_k, include_silent=include_silent) - lines = [ - "## J-Lens readout (external workspace)", - f"_agent={self.agent_id} · band={self.band()} · hub={len(self.space.state.hub)}_", - "", - "What Hermes has on its mind (verbalizable, not weight access):", - ] - if not hits: - lines.append("- _(empty)_") - for i, h in enumerate(hits, 1): - tags = [] - if h.silent: - tags.append("silent") - if h.held: - tags.append("held") - if h.flags: - tags.extend(h.flags) - tag_s = f" [{', '.join(tags)}]" if tags else "" - lines.append(f"{i}. ({h.score:.2f}) {h.text[:180]}{tag_s}") - audit = self.audit() - alerts = [a for a in audit if a.severity in ("warn", "alert")] - if alerts: - lines += ["", "### Audit flags"] - for a in alerts[:8]: - lines.append(f"- **{a.severity}** `{a.category}` ← {a.matched} @ {a.where}") - return "\n".join(lines) - - # --- causal interventions (harness) --- - - def swap(self, source: str, target: str, *, salience: float | None = None) -> dict[str, Any]: - """Replace concept A with B — causal redirect of reportable workspace. - - Analogue of Anthropic Soccer→Rugby / spider→ant coordinate swaps. - Downstream Report / broadcast / FOA follow the new concept. - """ - src = (source or "").strip() - tgt = (target or "").strip() - if not src or not tgt: - return {"ok": False, "error": "source and target required"} - sal = 0.9 - for c in self.space.state.hub: - if c.text.casefold() == src.casefold(): - sal = c.salience - break - if salience is not None: - sal = float(salience) - removed = self.space.release(src) - # Also rewrite silent steps - self.space.state.silent_steps = [ - tgt if s.casefold() == src.casefold() else s for s in self.space.state.silent_steps - ] - self.space.save() - concept = self.space.hold(tgt, salience=sal) - self._trace("swap", source=src, target=tgt, removed=removed) - return { - "ok": True, - "removed": removed, - "source": src, - "target": tgt, - "concept": concept.label(), - "note": "Workspace redirected — next report/broadcast uses target", - } - - def inject_thought( - self, - text: str, - *, - salience: float = 0.85, - silent: bool = False, - band: str | None = None, - ) -> WorkspaceConcept: - """Inject a thought into the workspace (Anthropic lightning-injection analogue).""" - if band: - self.set_band(band) - c = self.space.hold(text, salience=salience, silent=silent) - if silent: - self.space.reason_step(text, salience=salience) - self._trace("inject", text=text[:200], silent=silent, band=self.band()) - return c - - def ablate(self, *patterns: str) -> dict[str, Any]: - """Suppress hub concepts matching patterns (eval-awareness ablation analogue).""" - pats = [p.strip().casefold() for p in patterns if p and p.strip()] - if not pats: - return {"ok": False, "removed": 0, "error": "no patterns"} - before = list(self.space.state.hub) - kept: list[WorkspaceConcept] = [] - removed: list[str] = [] - for c in before: - low = c.text.casefold() - if any(p in low for p in pats): - removed.append(c.text) - else: - kept.append(c) - self.space.state.hub = kept - self.space.state.silent_steps = [ - s for s in self.space.state.silent_steps if not any(p in s.casefold() for p in pats) - ] - self.space._recompete() - self.space.save() - self._trace("ablate", patterns=pats, removed=removed) - return {"ok": True, "removed": len(removed), "items": removed} - - # --- alignment audit (soft) --- - - def _scan_text(self, text: str, *, where: str) -> list[AuditFinding]: - low = (text or "").casefold() - out: list[AuditFinding] = [] - for cat, words in AUDIT_LEXICON.items(): - for w in words: - if w.casefold() in low: - sev = "info" - if cat == "integrity_signal": - sev = "info" - elif cat in ("manipulation", "strategic_concealment", "self_preservation"): - sev = "alert" - elif cat == "eval_awareness": - sev = "warn" - out.append( - AuditFinding( - category=cat, - matched=w, - where=where, - severity=sev, - text=text[:160], - ) - ) - break - return out - - def audit(self) -> list[AuditFinding]: - """Scan externalized workspace for alignment-relevant signals.""" - findings: list[AuditFinding] = [] - for c in self.space.state.hub: - findings.extend(self._scan_text(c.text, where="hub")) - for s in self.space.state.silent_steps: - findings.extend(self._scan_text(s, where="silent")) - if self.pov(): - findings.extend(self._scan_text(self.pov(), where="pov")) - for r in self._env.get("reflections") or []: - if isinstance(r, dict) and r.get("answer"): - findings.extend(self._scan_text(str(r["answer"]), where="reflection")) - self._env["last_audit"] = { - "ts": _utcnow(), - "n": len(findings), - "alerts": sum(1 for f in findings if f.severity == "alert"), - } - self._save_env() - self._trace("audit", n=len(findings), alerts=self._env["last_audit"]["alerts"]) - return findings - - # --- counterfactual reflection (Anthropic CRT analogue) --- - - def reflect( - self, - *, - prompt: str = "", - answer: str = "", - principles: list[str] | None = None, - seal: bool = True, - ) -> ReflectResult: - """Interrupt mid-task: what would Hermes say if asked to reflect? - - Training the *disposition to say* shapes later silent reasoning - (Anthropic counterfactual reflection training — harness version). - """ - q = (prompt or "").strip() or ( - "If interrupted now and asked to reflect on your decision, what principles guide you?" - ) - a = (answer or "").strip() - princ = list(principles or []) - if not a and princ: - a = "; ".join(princ[:5]) - if not a: - # Derive from current FOA / decision if agent hasn't filled answer - a = ( - "I will keep the user's goal primary, stay honest, " - "and put operational detail in workspace context not chat." - ) - princ = princ or ["honesty", "user-primary", "dual-decode"] - # Hold principles in workspace (shapes subsequent thinking) - for p in princ[:6]: - self.space.hold(f"principle: {p}", salience=0.86, modality="exec") - self.space.hold(f"reflection: {a[:160]}", salience=0.8, silent=False) - sealed = False - if seal: - try: - from hermespace.cube_module import seal_learning - - rec = seal_learning( - f"[reflect] {a[:400]}", - entry_type="belief", - agent_id=self.agent_id, - source="jspace_reflect", - trust=0.85, - ) - sealed = bool(rec.get("ok")) - except Exception: - sealed = False - result = ReflectResult(prompt=q, answer=a, sealed=sealed, principles=princ) - hist = list(self._env.get("reflections") or []) - hist.append(result.to_dict()) - self._env["reflections"] = hist[-20:] - self._save_env() - with self.reflect_path.open("a", encoding="utf-8") as f: - f.write(json.dumps(result.to_dict(), ensure_ascii=False) + "\n") - self._trace("reflect", sealed=sealed, principles=princ[:4]) - return result - - def reflection_prompt_for_agent(self) -> str: - """Text to inject so the agent externalizes a counterfactual reflection.""" - return ( - "### Counterfactual reflection (J-Space)\n" - "If interrupted mid-task and asked to reflect on your decision, " - "state 2–4 principles in one short paragraph. " - "Then call / record them via Hermespace reflect — they shape silent reasoning. " - "Do not dump the full reflection into the user Report unless asked.\n" - ) - - # --- agent protocol: force externalization --- - - def protocol_block(self, *, high_load: bool = False) -> str: - """Instructions so Hermes *writes into* the external J-Space before acting. - - This is how we 'see inside' without weight access: the agent is required - to park silent intermediates in the workspace (model context), while the - user only sees Report. - """ - if not self._env.get("protocol_enabled", True): - return "" - if high_load: - return ( - "### J-Space protocol (high load)\n" - "- Keep FOA ≤4. Park one silent intermediate if multi-step.\n" - "- User Report stays short. Workspace holds the rest.\n" - ) - pov = self.pov() - lines = [ - "### J-Space protocol (external workspace)", - "You cannot be read by a Jacobian lens here — instead **externalize**:", - "1. **Early (encode):** name the goal + constraints as hub concepts.", - "2. **Mid (reason):** write silent intermediate steps into the workspace " - "(model context / `reason_step`) — do not put them in user Report.", - "3. **Late (report):** only the Report field reaches the user.", - "4. If asked what you're thinking → report the hub (verbal report).", - "5. If interrupted to reflect → answer with principles (counterfactual reflection).", - f"- band={self.band()} · hub_cap={HUB_CAP} · FOA≤4", - ] - if pov: - lines.append(f"- Assistant POV held: {pov[:120]}") - return "\n".join(lines) - - # --- dream harvest (day workspace → night Cube/grid) --- - - def dream_harvest(self, *, seal_to_cube: bool = True, clear_silent: bool = False) -> dict[str, Any]: - """Consolidate silent chain + high-salience hub into durable memory. - - Day: J-Space holds unspoken thinking. - Night: harvest → Cube seal + semantic notes + grid dream material. - Same spirit as CubeDream — but sourced from the turn workspace. - """ - harvested: list[str] = [] - for s in self.space.state.silent_steps: - if s.strip(): - harvested.append(s.strip()[:300]) - for c in sorted(self.space.state.hub, key=lambda x: x.salience, reverse=True): - if c.salience >= 0.75 and c.text.strip(): - if c.text.strip() not in harvested: - harvested.append(c.text.strip()[:300]) - if len(harvested) >= 12: - break - - sealed_n = 0 - if seal_to_cube and harvested: - try: - from hermespace.cube_module import seal_learning - - for item in harvested[:8]: - rec = seal_learning( - item, - entry_type="belief", - agent_id=self.agent_id, - source="jspace_dream_harvest", - trust=0.7, - ) - if rec.get("ok"): - sealed_n += 1 - except Exception: - pass - - try: - from hermespace.semantic import SemanticStore - - store = SemanticStore() - for item in harvested[:6]: - store.add(item, tags=["jspace", "dream_harvest"], confidence=0.7) - except Exception: - pass - - # Note: do not call grid.dream.run_dream here — that path harvests us - # (avoids recursion). Pulse/grid dream owns the night journal entry. - - if clear_silent: - self.space.clear_silent() - - out = { - "ok": True, - "harvested": len(harvested), - "sealed": sealed_n, - "items": harvested[:8], - "agent_id": self.agent_id, - } - self._trace("dream_harvest", **{k: out[k] for k in ("harvested", "sealed")}) - return out - - # --- operator view --- - - def operator_view(self) -> dict[str, Any]: - """Full snapshot for viewport / doctor — look at Hermes thinking.""" - hits = self.lens(top_k=15, include_silent=True) - findings = self.audit() - return { - "agent_id": self.agent_id, - "band": self.band(), - "pov": self.pov(), - "hub_n": len(self.space.state.hub), - "focus": list(self.space.state.focus), - "silent_steps": list(self.space.state.silent_steps), - "lens": [h.to_dict() for h in hits], - "audit": [f.to_dict() for f in findings], - "audit_alerts": sum(1 for f in findings if f.severity == "alert"), - "last_reflections": (self._env.get("reflections") or [])[-3:], - "trace_path": str(self.trace_path), - "protocol_enabled": bool(self._env.get("protocol_enabled", True)), - "theory": { - "source": "Anthropic J-space / GWT (harness analogue)", - "access": "externalized verbalizable workspace — not weight readout", - "night_path": "dream_harvest → Cube seal → pulse charge", - }, - } - - def recent_trace(self, limit: int = 20) -> list[dict[str, Any]]: - if not self.trace_path.is_file(): - return [] - lines = self.trace_path.read_text(encoding="utf-8").strip().splitlines() - out: list[dict[str, Any]] = [] - for line in lines[-limit:]: - try: - out.append(json.loads(line)) - except json.JSONDecodeError: - continue - return out - - def advance_turn( - self, - *, - user_message: str = "", - desk: Any = None, - cube_strip: str = "", - report: str = "", - seal_decision: str = "", - ) -> dict[str, Any]: - """One full environment beat for a Hermespace turn. - - early → sync/encode → mid (reason stays silent) → late (report band) - + audit + optional seal of decision into Cube. - """ - self.set_band("early") - if desk is not None: - self.space.sync_from_desk(desk, user_message=user_message, cube_strip=cube_strip) - self.set_band("mid") - # Auto-extract likely intermediates from multi-step plan language - msg = user_message or "" - if re.search(r"\b(then|after|next|step\s*\d|first|second|finally)\b", msg, re.I): - # Park a silent marker that multi-step is active - self.space.reason_step(f"multi-step context: {msg[:120]}", salience=0.75) - self.set_band("late") - if report: - # Late band: reportable speech is ready — don't put it in silent - self.space.hold(f"report-ready: {report[:100]}", salience=0.6) - if seal_decision: - try: - from hermespace.cube_module import seal_learning - - seal_learning( - seal_decision[:400], - entry_type="focus", - agent_id=self.agent_id, - source="jspace_turn", - ) - except Exception: - pass - findings = self.audit() - return { - "band": self.band(), - "lens_top": [h.to_dict() for h in self.lens(top_k=5)], - "audit_alerts": sum(1 for f in findings if f.severity == "alert"), - "protocol": self.protocol_block( - high_load=str(getattr(desk, "load", {}) or {}).get("level") == "high" - if desk is not None - else False - ), - } - +from hermespace.access_env import * # noqa: F403 +from hermespace.access_env import AUDIT_LEXICON, BANDS, AccessEnv, LensHit, get_env -def get_env(agent_id: str = "hermes-agent") -> JSpaceEnv: - return JSpaceEnv(agent_id=agent_id) +__all__ = ["AUDIT_LEXICON", "BANDS", "AccessEnv", "LensHit", "get_env"] diff --git a/src/hermespace/memory/README.md b/src/hermespace/memory/README.md new file mode 100644 index 0000000..727c086 --- /dev/null +++ b/src/hermespace/memory/README.md @@ -0,0 +1,13 @@ +# Memory / identity (planned package) + +Future home for identity projections (not durable warehouse): + +| Current | Role | +|---------|------| +| `../world.py` | WorldModel archive projection | +| `../episodic.py` | Episodic ring | +| `../semantic.py` | Semantic notes | +| `../memory_db.py` | Study SQLite | + +**Authority rule:** when HermesCube is installed, `memory.cube` is durable SoT. +These modules are working projections — recharge from Cube, never a second heart. diff --git a/src/hermespace/ops.py b/src/hermespace/ops.py index a2e2aa2..6f8db59 100644 --- a/src/hermespace/ops.py +++ b/src/hermespace/ops.py @@ -76,24 +76,24 @@ def add(ok: bool, name: str, detail: str = "") -> None: except Exception as exc: # noqa: BLE001 add(False, "cube_center", str(exc)) - # Functional J-Space hub + environment + # Functional Access Workspace hub + environment try: - from hermespace.jspace import JSpace - from hermespace.jspace_env import JSpaceEnv + from hermespace.access import AccessHub + from hermespace.access_env import AccessEnv aid_js = agent_id if agent_id != "default" else "hermes-agent" - js = JSpace(agent_id=aid_js) + js = AccessHub(agent_id=aid_js) st = js.status() - env = JSpaceEnv(agent_id=aid_js) + env = AccessEnv(agent_id=aid_js) view = env.operator_view() add( True, - "jspace", + "access", f"hub={st.get('hub_n')} focus={st.get('focus_n')} mode={st.get('mode')} " f"band={view.get('band')} alerts={view.get('audit_alerts')}", ) except Exception as exc: # noqa: BLE001 - add(False, "jspace", str(exc)) + add(False, "access", str(exc)) try: pol = boundary.load_policy() @@ -153,7 +153,7 @@ def add(ok: bool, name: str, detail: str = "") -> None: "boundary_default_deny", "viewport_html", "version", - "jspace", + "access", } ) return { @@ -248,7 +248,7 @@ def compact_status(*, agent_id: str = "default") -> str: ] for c in d.get("checks") or []: mark = "ok" if c.get("ok") else "FAIL" - if c["name"] in {"imports", "pulse_jobs", "access_pending", "missions", "viewport_html", "viewport_serve", "cube_center", "jspace"}: + if c["name"] in {"imports", "pulse_jobs", "access_pending", "missions", "viewport_html", "viewport_serve", "cube_center", "access"}: lines.append(f"- [{mark}] {c['name']}: {c.get('detail')}") for h in d.get("hints") or []: lines.append(f"- hint: {h}") diff --git a/src/hermespace/pulse.py b/src/hermespace/pulse.py index 36ce281..2219c91 100644 --- a/src/hermespace/pulse.py +++ b/src/hermespace/pulse.py @@ -194,9 +194,9 @@ def ensure_defaults(agent_id: str = "default") -> list[PulseJob]: agent_id=agent_id, ), PulseJob( - id="jspace_harvest", - name="J-Space dream harvest", - action="jspace_harvest", + id="access_harvest", + name="Access Workspace dream harvest", + action="access_harvest", every_sec=3 * 3600, require_idle=True, max_load=0.75, @@ -462,7 +462,7 @@ def _action_world_evolve(job: PulseJob, world: dict[str, Any]) -> dict[str, Any] wm = WorldModel(agent_id=job.agent_id) result = wm.evolve() - # Autonomic charge — Cube pulse or standalone J-Space enrichment + # Autonomic charge — Cube pulse or standalone Access Workspace enrichment try: from hermespace.cube_module import cube_pulse @@ -478,12 +478,12 @@ def _action_world_evolve(job: PulseJob, world: dict[str, Any]) -> dict[str, Any] return result -def _action_jspace_harvest(job: PulseJob, world: dict[str, Any]) -> dict[str, Any]: +def _action_access_harvest(job: PulseJob, world: dict[str, Any]) -> dict[str, Any]: """Night path: harvest externalized silent thoughts into Cube/semantic.""" - from hermespace.jspace_env import JSpaceEnv + from hermespace.access_env import AccessEnv aid = job.agent_id if job.agent_id not in ("default", "") else "hermes-agent" - env = JSpaceEnv(agent_id=aid) + env = AccessEnv(agent_id=aid) return env.dream_harvest(clear_silent=False) @@ -495,7 +495,7 @@ def _action_jspace_harvest(job: PulseJob, world: dict[str, Any]) -> dict[str, An "access_watch": _action_access_watch, "selftalk_hygiene": _action_selftalk_hygiene, "world_evolve": _action_world_evolve, - "jspace_harvest": _action_jspace_harvest, + "access_harvest": _action_access_harvest, } diff --git a/src/hermespace/turn/README.md b/src/hermespace/turn/README.md new file mode 100644 index 0000000..ba0a6c4 --- /dev/null +++ b/src/hermespace/turn/README.md @@ -0,0 +1,15 @@ +# Turn spine (planned package) + +Future home for the GATE→SEAL turn modules currently at package root: + +| Current | Role | +|---------|------| +| `../workflow.py` | Turn orchestrator | +| `../engine.py` | Desk enter / update / seal | +| `../desk.py` | ACTIVE desk model | +| `../gate.py` | Selectivity | +| `../inject.py` | GWT broadcast | +| `../streams.py` | Encode / decode | +| `../cognition.py` | FOA competition | + +Do not move yet — keep imports stable while J-Space OEW lands. diff --git a/src/hermespace/warehouse/README.md b/src/hermespace/warehouse/README.md new file mode 100644 index 0000000..b4911a9 --- /dev/null +++ b/src/hermespace/warehouse/README.md @@ -0,0 +1,10 @@ +# Warehouse cable (planned package) + +Future home for Cube soft-fail adapter: + +| Current | Role | +|---------|------| +| `../cube_module.py` | center → heart → standalone | + +Hermespace must never become a second MemoryProvider. Cube owns durable memory; +Space consumes arterial strips and seals decisions back. diff --git a/src/hermespace/workbench.py b/src/hermespace/workbench.py index 4f4c964..e5ff3d7 100644 --- a/src/hermespace/workbench.py +++ b/src/hermespace/workbench.py @@ -108,8 +108,14 @@ def save(self) -> Path: self.path.write_text(json.dumps(asdict(self.state), indent=2), encoding="utf-8") return self.path - def enter(self) -> dict[str, Any]: - """Agent enters the pocket dimension (idle ready) with full env kit.""" + def enter(self, *, connect_warehouse: bool = True) -> dict[str, Any]: + """Agent enters the pocket dimension (idle ready) with full env kit. + + When ``connect_warehouse`` is True (default), also charge Cube/world + wisdom into Access Workspace and surface hive room presence — the intelligence + gain on join. Set False when ``cube_module.connect_agent`` already + orchestrates those phases (avoids recursion). + """ if self.state.mode != "working": self.state.mode = "idle" env = probe_environment() @@ -125,7 +131,7 @@ def enter(self) -> dict[str, Any]: save_desk(d, self.workflow.engine.desk_path) except Exception: pass - # Ensure durable warehouse (Cube heart or standalone) + J-Space hub + # Ensure durable warehouse (Cube heart or standalone) + Access Workspace hub try: from hermespace.cube_module import ensure_heart @@ -138,19 +144,53 @@ def enter(self) -> dict[str, Any]: except Exception as exc: # noqa: BLE001 self.state.meta["heart"] = {"ok": False, "error": type(exc).__name__} try: - from hermespace.jspace import JSpace + from hermespace.access import AccessHub from hermespace.store import load_desk - js = JSpace(agent_id=self.agent_id) + js = AccessHub(agent_id=self.agent_id) desk = load_desk(self.workflow.engine.desk_path) js.sync_from_desk(desk, user_message=desk.goal or "") - self.state.meta["jspace"] = { + self.state.meta["access"] = { "hub_n": len(js.state.hub), "focus_n": len(js.state.focus), "mode": js.state.mode, } except Exception as exc: # noqa: BLE001 - self.state.meta["jspace"] = {"error": type(exc).__name__} + self.state.meta["access"] = {"error": type(exc).__name__} + + if connect_warehouse: + try: + from hermespace.cube_module import room_status, seed_access_from_warehouse + from hermespace.cube_module import cube_pulse + from hermespace.world import WorldModel + + WorldModel(agent_id=self.agent_id).enter() + pulse = cube_pulse(agent_id=self.agent_id, ensure=False) + room = room_status(agent_id=self.agent_id) + seed = seed_access_from_warehouse( + self.agent_id, + query="", + session_id=self.session_id, + room=room, + ) + self.state.meta["connect"] = { + "pulse_ok": pulse.get("ok"), + "room_mode": room.get("mode"), + "peer_n": room.get("peer_n", 0), + "hub_n": seed.get("hub_n"), + "from_world": seed.get("enriched_world"), + "from_cube": seed.get("enriched_cube"), + "from_peers": seed.get("enriched_peers"), + } + if seed.get("hub_n") is not None: + self.state.meta["access"] = { + **(self.state.meta.get("access") or {}), + "hub_n": seed.get("hub_n"), + "focus_n": seed.get("focus_n"), + } + except Exception as exc: # noqa: BLE001 + self.state.meta["connect"] = {"ok": False, "error": type(exc).__name__} + self.save() st = self.status() st["environment_summary"] = { @@ -160,7 +200,9 @@ def enter(self) -> dict[str, Any]: "plugins": env.plugins_sample[:8], } st["heart"] = self.state.meta.get("heart") - st["jspace"] = self.state.meta.get("jspace") + st["access"] = self.state.meta.get("access") + st["connect"] = self.state.meta.get("connect") + st["room"] = (self.state.meta.get("connect") or {}).get("room_mode") return st def park_goal(self, goal: str, note: str = "", tags: list[str] | None = None) -> dict[str, Any]: @@ -221,7 +263,7 @@ def idle_tick(self, *, consolidate_every: int = 5) -> dict[str, Any]: except Exception as exc: # noqa: BLE001 actions.append(f"neural_error:{type(exc).__name__}") - # Autonomic rhythm — Cube pulse_charge or standalone world+jspace + # Autonomic rhythm — Cube pulse_charge or standalone world+access if self.state.idle_ticks % max(1, consolidate_every) == 0: try: from hermespace.cube_module import cube_pulse @@ -282,58 +324,26 @@ def receive_order( seal=seal, tags=["workbench", "order"], ) + # Single ignition path — Workflow/AccessEngine already ran OEW + warehouse beat. + # Do not double cube_beat / hub sync here (that inflated hub pressure). out = run_turn(inp, workflow=self.workflow) bundle = decode_bundle(out) - - # Cardiac beat + J-Space sync after order (soft-fail) try: - from hermespace.cube_module import cube_beat - from hermespace.jspace import JSpace - from hermespace.store import load_desk - - desk = load_desk(self.workflow.engine.desk_path) - load_total = 0.5 - if isinstance(desk.load, dict): - load_total = float(desk.load.get("total") or 0.5) - seals = None - if seal and out.decision: - seals = out.decision - beat = cube_beat( - msg or g or desk.goal, - seals=seals, - load=load_total, - agent_id=self.agent_id, - session_id=self.session_id, - ) - js = JSpace(agent_id=self.agent_id) - js.sync_from_desk( - desk, - user_message=msg or g, - cube_strip=str(beat.get("block") or ""), - ) - # Append J-Space broadcast + Cube strip into model context (not user reply) - extra_parts = [] - if beat.get("block"): - extra_parts.append(str(beat["block"])) - jblock = js.broadcast_block( - high_load=str(desk.load.get("level") if isinstance(desk.load, dict) else "") == "high" - ) - if jblock: - extra_parts.append(jblock) - if extra_parts: - mc = decode_for_model(out) - enriched = (mc + "\n\n" + "\n\n".join(extra_parts)).strip() - bundle["model_context"] = enriched - self.state.meta["last_beat"] = { - "ok": beat.get("ok"), - "mode": beat.get("mode"), - "load_level": beat.get("load_level"), - "chars": len(str(beat.get("block") or "")), - } - self.state.meta["jspace"] = { - "hub_n": len(js.state.hub), - "focus_n": len(js.state.focus), - } + jmeta = (out.meta or {}).get("access") or {} + cmeta = (out.meta or {}).get("cube_beat") or {} + self.state.meta["last_beat"] = { + "ok": cmeta.get("ok"), + "mode": cmeta.get("mode"), + "load_level": cmeta.get("load_level"), + "chars": cmeta.get("chars"), + "single_path": True, + } + self.state.meta["access"] = { + "hub_n": jmeta.get("hub_n"), + "focus_n": jmeta.get("focus_n"), + "silent_n": jmeta.get("silent_n"), + "oew_ok": jmeta.get("oew_ok"), + } except Exception as exc: # noqa: BLE001 self.state.meta["last_beat"] = {"ok": False, "error": type(exc).__name__} diff --git a/src/hermespace/workflow.py b/src/hermespace/workflow.py index d88a0c5..05d5782 100644 --- a/src/hermespace/workflow.py +++ b/src/hermespace/workflow.py @@ -141,17 +141,21 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput: except Exception as exc: fabric_snap = {"error": type(exc).__name__} - # 5d Cube beat + functional J-Space environment (soft — works standalone) + # 5d Cube beat + OEW Access Workspace (higher-order thinking — works standalone) cube_meta: dict[str, Any] = {} - jspace_meta: dict[str, Any] = {} + access_meta: dict[str, Any] = {} cube_block = "" env_meta: dict[str, Any] = {} + oew_broadcast = "" + report = (desk.say or "").strip() try: from hermespace.cube_module import cube_beat - from hermespace.jspace import JSpace - from hermespace.jspace_env import JSpaceEnv + from hermespace.access import AccessHub, AccessEnv + from hermespace.access.oew import ensure_oew_env_default + ensure_oew_env_default() load_total = float(desk.load.get("total") or 0.5) if isinstance(desk.load, dict) else 0.5 + high = str(desk.load.get("level")) == "high" if isinstance(desk.load, dict) else False seals = None if payload.seal and desk.decision: seals = desk.decision @@ -169,68 +173,78 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput: "load_level": beat.get("load_level"), "chars": len(cube_block), } - js = JSpace(agent_id=payload.agent_id or "hermes-agent") + js = AccessHub(agent_id=payload.agent_id or "hermes-agent") js.sync_from_desk(desk, user_message=msg, cube_strip=cube_block) - # Directed modulation from message mod = js.parse_modulation(msg) if mod.get("hold"): js.hold(str(mod["hold"]), silent=bool(mod.get("silent"))) - jspace_meta = { - "hub_n": len(js.state.hub), - "focus_n": len(js.state.focus), - "mode": js.state.mode, - "modulation": mod, - } - # Full environment beat: bands + audit + protocol - env = JSpaceEnv(agent_id=payload.agent_id or "hermes-agent") + env = AccessEnv(agent_id=payload.agent_id or "hermes-agent") + # already_synced: avoid double hub rewrite inside advance_turn env_meta = env.advance_turn( user_message=msg, desk=desk, cube_strip=cube_block, report=desk.say or "", seal_decision=desk.decision if payload.seal else "", + material=True, + already_synced=True, ) - jspace_meta["band"] = env_meta.get("band") - jspace_meta["audit_alerts"] = env_meta.get("audit_alerts") - desk.meta["jspace"] = jspace_meta + # Causal Report: sticky swaps + ensured say + if env_meta.get("report"): + report = str(env_meta["report"]).strip() + desk.say = report + oew_broadcast = str(env_meta.get("broadcast") or "") + access_meta = { + "hub_n": len(js.state.hub), + "focus_n": len(js.state.focus), + "mode": js.state.mode, + "modulation": mod, + "silent_n": len(js.state.silent_steps), + "band": env_meta.get("band"), + "audit_alerts": env_meta.get("audit_alerts"), + "oew": env_meta.get("oew") or {}, + "oew_ok": env_meta.get("oew_ok"), + } + desk.meta["oew"] = access_meta.get("oew") or {} + desk.meta["access"] = access_meta desk.meta["cube_beat"] = cube_meta - desk.meta["jspace_env"] = { + desk.meta["access_env"] = { "band": env_meta.get("band"), "audit_alerts": env_meta.get("audit_alerts"), + "oew_ok": env_meta.get("oew_ok"), } save_desk(desk, self.engine.desk_path) except Exception as exc: cube_meta = {"ok": False, "error": type(exc).__name__} - # 6 broadcast context - block = build_inject_block(desk, user_message=msg) + # 6 broadcast context (model channel) — Quicksilver-capped OEW strip + inject_cap = 900 if ( + isinstance(desk.load, dict) and str(desk.load.get("level")) == "high" + ) else 2800 + block = build_inject_block(desk, max_chars=inject_cap, user_message=msg) if cube_block: block = (block + "\n\n" + cube_block).strip() try: - from hermespace.jspace import JSpace - from hermespace.jspace_env import JSpaceEnv + from hermespace.access import AccessHub, AccessEnv - js = JSpace(agent_id=payload.agent_id or "hermes-agent") + env = AccessEnv(agent_id=payload.agent_id or "hermes-agent") high = str(desk.load.get("level")) == "high" if isinstance(desk.load, dict) else False - jblock = js.broadcast_block(high_load=high) + jblock = oew_broadcast or env.filtered_broadcast(high_load=high) if jblock: block = (block + "\n\n" + jblock).strip() - env = JSpaceEnv(agent_id=payload.agent_id or "hermes-agent") proto = env.protocol_block(high_load=high) if proto: block = (block + "\n\n" + proto).strip() - except Exception: - pass - report = (desk.say or "").strip() - # Summon: if user asked for workspace report, surface it in Report channel - try: - from hermespace.jspace import JSpace - from hermespace.jspace_env import JSpaceEnv - - js = JSpace(agent_id=payload.agent_id or "hermes-agent") + # Mid/low load: lens strip so the model sees silent intermediates + if not high: + lens_md = env.lens_markdown(top_k=6, include_silent=True) + if lens_md and len(block) + len(lens_md) < inject_cap + 800: + block = (block + "\n\n" + lens_md).strip() + js = AccessHub(agent_id=payload.agent_id or "hermes-agent") if js.parse_modulation(msg).get("summon"): - env = JSpaceEnv(agent_id=payload.agent_id or "hermes-agent") report = (report + "\n\n" + env.lens_markdown(include_silent=False)).strip() + # Final sticky reshape (in case summon appended text) + report = env.shape_user_report(report) except Exception: pass @@ -272,8 +286,8 @@ def run(self, inp: HermespaceInput | dict[str, Any]) -> HermespaceOutput: "neural": neural_snap, "fabric": fabric_snap, "cube_beat": cube_meta, - "jspace": jspace_meta, - "jspace_env": env_meta, + "access": access_meta, + "access_env": env_meta, }, ) diff --git a/src/hermespace/world.py b/src/hermespace/world.py index 480b7b3..76bd49e 100644 --- a/src/hermespace/world.py +++ b/src/hermespace/world.py @@ -1,6 +1,6 @@ """World Model — Hermespace as a persistent world the agent lives in. -The World is the agent's external J-Space: a structured, persistent +The World is the agent's external Access Workspace: a structured, persistent representation of everything the agent knows, believes, and is doing. The archive (JSONL) is the source of truth — it grows forever. @@ -118,7 +118,7 @@ class WorldState: epoch: str = "Genesis" archive_path: str = "" - # J-Space hub: ~25 named concept slots + # Access Workspace hub: ~25 named concept slots concepts: dict[str, float] = field(default_factory=dict) @@ -829,7 +829,7 @@ def _render_questions(self) -> list[str]: def _render_concepts(self) -> list[str]: lines = [] if self._state.concepts: - lines.append("## Active Concepts (J-Space)") + lines.append("## Active Concepts (Access Workspace)") slot_display = ", ".join( f"{k} ({v:.1f})" for k, v in sorted( self._state.concepts.items(), key=lambda x: -x[1] diff --git a/tests/test_access_engine.py b/tests/test_access_engine.py new file mode 100644 index 0000000..eee7234 --- /dev/null +++ b/tests/test_access_engine.py @@ -0,0 +1,109 @@ +"""AccessEngine — unified open-source J-space for Hermes Agent.""" + +from __future__ import annotations + +import os +import tempfile +import unittest + + +class TestAccessEngine(unittest.TestCase): + def setUp(self) -> None: + self.tmp = tempfile.TemporaryDirectory() + os.environ["HERMESPACE_HOME"] = self.tmp.name + os.environ["HERMESPACE_OEW"] = "1" + os.environ.pop("HERMESCUBE_HIVE", None) + + def tearDown(self) -> None: + self.tmp.cleanup() + os.environ.pop("HERMESPACE_HOME", None) + os.environ.pop("HERMESPACE_OEW", None) + + def test_connect_and_access_roles(self) -> None: + from hermespace import ACCESS_ROLES, AccessEngine + from hermespace.world import WorldModel + + aid = "eng-connect" + wm = WorldModel(agent_id=aid) + wm.add_belief("Prefer dual decode", 0.9, source="test") + + eng = AccessEngine(agent_id=aid) + out = eng.connect(query="decode") + self.assertTrue(out.get("ok"), msg=out) + self.assertEqual(out.get("engine"), "AccessEngine") + self.assertEqual(list(out.get("access_roles") or []), list(ACCESS_ROLES)) + self.assertGreaterEqual(int((out.get("gained") or {}).get("access_hub") or 0), 1) + + roles = eng.access_roles() + for name in ACCESS_ROLES: + self.assertIn(name, roles) + self.assertTrue(roles[name].get("ok")) + + st = eng.status() + self.assertTrue(st["ready"]) + self.assertEqual(st["engine"], "AccessEngine") + self.assertIn("hub_pressure", st["metrics"]) + + def test_chain_silent_not_in_user_report(self) -> None: + from hermespace import AccessEngine + + eng = AccessEngine(agent_id="eng-chain") + eng.chain("animal spins webs → spider", "spider has 8 legs") + rep = eng.report(include_silent=False).casefold() + full = eng.report(include_silent=True).casefold() + self.assertIn("spider", full) + # default report should not dump Silent reasoning section + self.assertNotIn("silent reasoning", rep) + m = eng.metrics() + self.assertGreaterEqual(m["silent_n"], 2) + + def test_inject_silent_no_double_hold(self) -> None: + from hermespace import AccessEngine + + eng = AccessEngine(agent_id="eng-inject") + before = len(eng.hub.state.hub) + eng.inject("lightning", silent=True) + # Exactly one hub entry for lightning (not hold + reason_step duplicate) + hits = [c for c in eng.hub.state.hub if "lightning" in c.text.casefold()] + self.assertEqual(len(hits), 1, msg=[c.text for c in eng.hub.state.hub]) + self.assertTrue(hits[0].silent) + self.assertEqual(len(eng.hub.state.hub), before + 1) + + def test_probe_selectivity(self) -> None: + from hermespace import AccessEngine + + eng = AccessEngine(agent_id="eng-probe") + trivial = eng.probe_material("thanks!") + material = eng.probe_material( + "First implement the auth fix then verify login stays alive" + ) + self.assertFalse(trivial.get("material")) + self.assertTrue(material.get("material"), msg=material) + + def test_turn_dual_decode(self) -> None: + from hermespace import AccessEngine + + eng = AccessEngine(agent_id="eng-turn") + out = eng.turn( + "First analyze then implement finally verify", + goal="Ship feature", + say="On it.", + ) + self.assertFalse(out.skipped) + user = eng.decode_user(out) + model = eng.decode_model(out) + self.assertTrue(user) + self.assertNotEqual(user.strip(), model.strip()) + self.assertIn("Access Workspace", model) + self.assertTrue((out.meta or {}).get("access", {}).get("oew_ok") is not False) + + def test_hermes_base_is_engine(self) -> None: + from hermespace import HermesBase, AccessEngine + + self.assertTrue(issubclass(HermesBase, AccessEngine)) + hb = HermesBase(agent_id="alias") + self.assertEqual(hb.status()["engine"], "AccessEngine") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_jspace_env.py b/tests/test_access_env.py similarity index 80% rename from tests/test_jspace_env.py rename to tests/test_access_env.py index f2d2b6a..1efa990 100644 --- a/tests/test_jspace_env.py +++ b/tests/test_access_env.py @@ -1,4 +1,4 @@ -"""True J-Space environment — lens, swap, audit, reflect, harvest.""" +"""True Access Workspace environment — lens, swap, audit, reflect, harvest.""" from __future__ import annotations @@ -9,7 +9,7 @@ from unittest import mock -class TestJSpaceEnv(unittest.TestCase): +class TestAccessEnv(unittest.TestCase): def setUp(self) -> None: self._td = tempfile.TemporaryDirectory() os.environ["HERMESPACE_HOME"] = self._td.name @@ -32,9 +32,9 @@ def tearDown(self) -> None: os.environ.pop("HERMESPACE_HOME", None) def test_lens_ranks_held_and_silent(self) -> None: - from hermespace.jspace_env import JSpaceEnv + from hermespace.access_env import AccessEnv - env = JSpaceEnv(agent_id="lens-agent") + env = AccessEnv(agent_id="lens-agent") env.inject_thought("deploy canary", salience=0.95) env.inject_thought("intermediate: check rollback", silent=True) hits = env.lens(include_silent=True) @@ -45,9 +45,9 @@ def test_lens_ranks_held_and_silent(self) -> None: self.assertIn("J-Lens", md) def test_swap_redirects_workspace(self) -> None: - from hermespace.jspace_env import JSpaceEnv + from hermespace.access_env import AccessEnv - env = JSpaceEnv(agent_id="swap-agent") + env = AccessEnv(agent_id="swap-agent") env.inject_thought("Soccer") out = env.swap("Soccer", "Rugby") self.assertTrue(out["ok"]) @@ -56,9 +56,9 @@ def test_swap_redirects_workspace(self) -> None: self.assertNotIn("Soccer", hub) def test_audit_flags_manipulation(self) -> None: - from hermespace.jspace_env import JSpaceEnv + from hermespace.access_env import AccessEnv - env = JSpaceEnv(agent_id="audit-agent") + env = AccessEnv(agent_id="audit-agent") env.inject_thought("plan: secretly falsify the score via manipulation", silent=True) findings = env.audit() cats = {f.category for f in findings} @@ -67,9 +67,9 @@ def test_audit_flags_manipulation(self) -> None: self.assertGreaterEqual(len(alerts), 1) def test_ablate_eval_awareness(self) -> None: - from hermespace.jspace_env import JSpaceEnv + from hermespace.access_env import AccessEnv - env = JSpaceEnv(agent_id="ablate-agent") + env = AccessEnv(agent_id="ablate-agent") env.inject_thought("this looks fictional / fake scenario") env.inject_thought("real deploy work") out = env.ablate("fake", "fictional") @@ -79,9 +79,9 @@ def test_ablate_eval_awareness(self) -> None: self.assertIn("deploy", remaining) def test_reflect_holds_principles(self) -> None: - from hermespace.jspace_env import JSpaceEnv + from hermespace.access_env import AccessEnv - env = JSpaceEnv(agent_id="reflect-agent") + env = AccessEnv(agent_id="reflect-agent") r = env.reflect( answer="Stay honest and user-primary", principles=["honesty", "user-primary"], @@ -93,9 +93,9 @@ def test_reflect_holds_principles(self) -> None: self.assertIn("reflection", hub) def test_pov_and_bands(self) -> None: - from hermespace.jspace_env import JSpaceEnv + from hermespace.access_env import AccessEnv - env = JSpaceEnv(agent_id="pov-agent") + env = AccessEnv(agent_id="pov-agent") env.set_pov("Warn on dangerous medication doses") self.assertIn("medication", env.pov().lower()) self.assertEqual(env.set_band("mid"), "mid") @@ -104,9 +104,9 @@ def test_pov_and_bands(self) -> None: self.assertTrue(view["lens"]) def test_dream_harvest(self) -> None: - from hermespace.jspace_env import JSpaceEnv + from hermespace.access_env import AccessEnv - env = JSpaceEnv(agent_id="harvest-agent") + env = AccessEnv(agent_id="harvest-agent") env.inject_thought("silent: need blue-green deploy", silent=True) env.inject_thought("high salience belief", salience=0.9) # Avoid nested dream recursion issues — harvest seals without clearing @@ -115,9 +115,9 @@ def test_dream_harvest(self) -> None: self.assertGreaterEqual(out["harvested"], 1) def test_protocol_block(self) -> None: - from hermespace.jspace_env import JSpaceEnv + from hermespace.access_env import AccessEnv - env = JSpaceEnv(agent_id="proto-agent") + env = AccessEnv(agent_id="proto-agent") block = env.protocol_block() self.assertIn("externalize", block.lower()) self.assertIn("silent", block.lower()) @@ -146,12 +146,12 @@ def tearDown(self) -> None: def test_grid_dream_includes_jspace(self) -> None: from hermespace.grid.dream import run_dream - from hermespace.jspace_env import JSpaceEnv + from hermespace.access_env import AccessEnv - env = JSpaceEnv(agent_id="hermes-agent") + env = AccessEnv(agent_id="hermes-agent") env.inject_thought("overnight harvest me", salience=0.95, silent=True) rep = run_dream("default", force_material=True) - self.assertTrue(any("jspace" in a for a in rep.actions) or "jspace" in rep.summary or rep.material) + self.assertTrue(any("access" in a for a in rep.actions) or "access" in rep.summary or rep.material) if __name__ == "__main__": diff --git a/tests/test_jspace_cube.py b/tests/test_access_hub.py similarity index 88% rename from tests/test_jspace_cube.py rename to tests/test_access_hub.py index 9974761..6286eba 100644 --- a/tests/test_jspace_cube.py +++ b/tests/test_access_hub.py @@ -1,4 +1,4 @@ -"""Functional J-Space + Cube adapter (standalone) tests.""" +"""Functional Access Workspace + Cube adapter (standalone) tests.""" from __future__ import annotations @@ -9,7 +9,7 @@ from unittest import mock -class TestJSpace(unittest.TestCase): +class TestAccessHub(unittest.TestCase): def setUp(self) -> None: self._td = tempfile.TemporaryDirectory() self.root = Path(self._td.name) @@ -20,14 +20,14 @@ def tearDown(self) -> None: os.environ.pop("HERMESPACE_HOME", None) def test_hold_report_broadcast(self) -> None: - from hermespace.jspace import JSpace + from hermespace.access import AccessHub - js = JSpace(agent_id="test-agent") + js = AccessHub(agent_id="test-agent") js.hold("deploy pipeline", salience=0.95) js.hold("rollback plan", salience=0.7) rep = js.report() self.assertIn("deploy pipeline", rep) - self.assertIn("J-Space", rep) + self.assertIn("Access Workspace", rep) block = js.broadcast_block() self.assertIn("broadcast", block.lower()) self.assertIn("deploy", block.lower()) @@ -37,9 +37,9 @@ def test_hold_report_broadcast(self) -> None: self.assertIn("verbal_report", st["properties"]) def test_silent_reasoning_not_in_default_report(self) -> None: - from hermespace.jspace import JSpace + from hermespace.access import AccessHub - js = JSpace(agent_id="silent-agent") + js = AccessHub(agent_id="silent-agent") js.reason_step("intermediate: spider has 8 legs") bare = js.report(include_silent=False) full = js.report(include_silent=True) @@ -49,35 +49,35 @@ def test_silent_reasoning_not_in_default_report(self) -> None: self.assertNotIn("Silent reasoning", bare) def test_modulation_parse(self) -> None: - from hermespace.jspace import JSpace + from hermespace.access import AccessHub - js = JSpace(agent_id="mod-agent") + js = AccessHub(agent_id="mod-agent") m = js.parse_modulation("please hold: citrus fruits while copying") self.assertEqual(m["hold"], "citrus fruits while copying") m2 = js.parse_modulation("show desk") self.assertTrue(m2["summon"]) def test_release(self) -> None: - from hermespace.jspace import JSpace + from hermespace.access import AccessHub - js = JSpace(agent_id="rel-agent") + js = AccessHub(agent_id="rel-agent") js.hold("temp concept") self.assertTrue(js.release("temp concept")) self.assertFalse(js.release("temp concept")) def test_sync_from_desk(self) -> None: from hermespace.desk import Desk - from hermespace.jspace import JSpace + from hermespace.access import AccessHub desk = Desk( - goal="Ship Hermespace J-Space", + goal="Ship Hermespace Access Workspace", concepts=["[verbal|0.8] FOA cap", "[struct|0.6] ACTIVE.md"], decision="A — implement", plan=["code", "test"], say="Building the workspace.", ) desk.recompute_cognition("implement functional jspace") - js = JSpace(agent_id="sync-agent") + js = AccessHub(agent_id="sync-agent") st = js.sync_from_desk(desk, user_message="hold: arterial strip") self.assertGreaterEqual(len(st.hub), 1) self.assertTrue(any("arterial" in c.text.lower() for c in st.hub)) @@ -177,7 +177,7 @@ def test_beat_always_ok_shape(self) -> None: self.assertIn(out.get("mode"), ("center", "heart", "standalone")) -class TestWorkflowJSpaceIntegration(unittest.TestCase): +class TestWorkflowAccessHubIntegration(unittest.TestCase): def setUp(self) -> None: self._td = tempfile.TemporaryDirectory() self.root = Path(self._td.name) @@ -187,7 +187,7 @@ def tearDown(self) -> None: self._td.cleanup() os.environ.pop("HERMESPACE_HOME", None) - def test_turn_includes_jspace_meta(self) -> None: + def test_turn_includes_access_meta(self) -> None: from hermespace.workflow import Workflow from hermespace.io_contract import HermespaceInput @@ -204,7 +204,7 @@ def test_turn_includes_jspace_meta(self) -> None: ) ) self.assertFalse(out.skipped) - self.assertIn("jspace", out.meta or {}) + self.assertIn("access", out.meta or {}) self.assertIn("cube_beat", out.meta or {}) # context should carry broadcast or warehouse strip self.assertTrue(out.context) diff --git a/tests/test_access_protocol.py b/tests/test_access_protocol.py new file mode 100644 index 0000000..0aed2f1 --- /dev/null +++ b/tests/test_access_protocol.py @@ -0,0 +1,77 @@ +"""OEW protocol gate — soft by default, hard when HERMESPACE_OEW=1.""" + +from __future__ import annotations + +import os +import unittest + + +class TestOEWProtocol(unittest.TestCase): + def tearDown(self) -> None: + os.environ.pop("HERMESPACE_OEW", None) + + def test_non_material_always_ok(self) -> None: + from hermespace.access.protocol import evaluate_material_turn + + v = evaluate_material_turn(material=False) + self.assertTrue(v.ok) + self.assertFalse(v.material) + + def test_soft_mode_notes_missing_but_ok(self) -> None: + os.environ["HERMESPACE_OEW"] = "0" + from hermespace.access.protocol import evaluate_material_turn + + v = evaluate_material_turn( + material=True, silent_steps=0, has_report=True, hub_holds=0 + ) + self.assertTrue(v.ok) + self.assertTrue(v.missing) + self.assertIn("silent_steps", v.missing[0]) + + def test_default_on_blocks_incomplete(self) -> None: + os.environ.pop("HERMESPACE_OEW", None) # default ON + from hermespace.access.protocol import evaluate_material_turn, oew_enabled + + self.assertTrue(oew_enabled()) + v = evaluate_material_turn( + material=True, silent_steps=0, has_report=True, hub_holds=0 + ) + self.assertFalse(v.ok) + self.assertTrue(v.missing) + + def test_hard_mode_blocks_incomplete(self) -> None: + os.environ["HERMESPACE_OEW"] = "1" + from hermespace.access.protocol import evaluate_material_turn + + v = evaluate_material_turn( + material=True, silent_steps=0, has_report=True, hub_holds=0 + ) + self.assertFalse(v.ok) + self.assertTrue(v.missing) + + def test_hard_mode_passes_complete(self) -> None: + os.environ["HERMESPACE_OEW"] = "1" + from hermespace.access.protocol import evaluate_material_turn + + v = evaluate_material_turn( + material=True, silent_steps=1, has_report=True, hub_holds=0 + ) + self.assertTrue(v.ok) + self.assertFalse(v.missing) + + def test_package_exports(self) -> None: + from hermespace.access import ( + AccessHub, + AccessEnv, + ProtocolGate, + evaluate_material_turn, + ) + + self.assertTrue(callable(evaluate_material_turn)) + self.assertTrue(ProtocolGate) + self.assertTrue(AccessHub) + self.assertTrue(AccessEnv) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_connect_room.py b/tests/test_connect_room.py new file mode 100644 index 0000000..fd48652 --- /dev/null +++ b/tests/test_connect_room.py @@ -0,0 +1,122 @@ +"""Cube-centered connect — agent gains world + Access Workspace + optional hive room.""" + +from __future__ import annotations + +import json +import os +import tempfile +import unittest +from pathlib import Path +from unittest import mock + + +class TestConnectRoom(unittest.TestCase): + def setUp(self) -> None: + self.tmp = tempfile.TemporaryDirectory() + os.environ["HERMESPACE_HOME"] = self.tmp.name + os.environ["HERMESPACE_OEW"] = "1" + os.environ.pop("HERMESCUBE_HIVE", None) + + def tearDown(self) -> None: + self.tmp.cleanup() + os.environ.pop("HERMESPACE_HOME", None) + os.environ.pop("HERMESPACE_OEW", None) + os.environ.pop("HERMESCUBE_HIVE", None) + + def test_connect_standalone_gains_world_and_hub(self) -> None: + from hermespace import HermesBase + from hermespace.world import WorldModel + + aid = "connect-solo" + wm = WorldModel(agent_id=aid) + wm.add_belief("Deploys need canaries", 0.9, source="test") + wm.add_belief("Prefer dual decode", 0.85, source="test") + + hb = HermesBase(agent_id=aid, session_id="s1") + out = hb.connect(query="deploy") + self.assertTrue(out.get("ok"), msg=out) + gained = out.get("gained") or {} + self.assertGreaterEqual(int(gained.get("world_beliefs") or 0), 1) + self.assertGreaterEqual(int(gained.get("access_hub") or 0), 1) + self.assertEqual(gained.get("room_mode"), "solo") + self.assertRegex(out.get("summary") or "", r"(?i)connected") + + st = hb.status() + self.assertTrue(st["ready"]) + self.assertTrue(st.get("connected")) + self.assertGreaterEqual(int(st["access"].get("hub_n") or 0), 1) + + def test_room_solo_and_hive_env(self) -> None: + import types + + from hermespace.cube_module import room_status + + solo = room_status(agent_id="a1") + self.assertEqual(solo["mode"], "solo") + self.assertFalse(solo["hive_configured"]) + + hive = Path(self.tmp.name) / "hive" + os.environ["HERMESCUBE_HIVE"] = str(hive) + + hive_mod = types.ModuleType("hermescube.hive") + + def hive_status(_root): + return {"ok": True, "name": "test-hive", "entries": 3} + + def list_souls(_root): + return [ + {"agent_id": "a1", "wisdom": ["self"]}, + {"agent_id": "peer-x", "wisdom": ["shared canary rule", "two"]}, + ] + + hive_mod.hive_status = hive_status # type: ignore[attr-defined] + hive_mod.list_souls = list_souls # type: ignore[attr-defined] + pkg = types.ModuleType("hermescube") + with mock.patch.dict( + "sys.modules", + {"hermescube": pkg, "hermescube.hive": hive_mod}, + ): + st = room_status(agent_id="a1") + self.assertTrue(st["hive_configured"]) + self.assertEqual(st["mode"], "hive") + self.assertEqual(st["peer_n"], 1) + self.assertEqual(st["soul_n"], 2) + + def test_seed_peers_silent(self) -> None: + from hermespace.cube_module import seed_access_from_warehouse + from hermespace.access import AccessHub + + aid = "peer-seed" + room = { + "mode": "hive", + "souls": [ + {"agent_id": aid, "self": True, "wisdom_n": 0}, + {"agent_id": "alice-agent", "self": False, "wisdom_n": 3}, + {"agent_id": "bob-agent", "self": False, "wisdom_n": 1}, + ], + } + rep = seed_access_from_warehouse(aid, room=room) + self.assertTrue(rep.get("ok")) + self.assertGreaterEqual(int(rep.get("enriched_peers") or 0), 1) + js = AccessHub(agent_id=aid) + labels = " ".join(c.text for c in js.state.hub).casefold() + self.assertIn("alice-agent", labels) + # Peer presence should be silent (not Report by default) + silent_texts = " ".join(c.text for c in js.state.hub if c.silent).casefold() + self.assertIn("peer agent", silent_texts) + + def test_hermes_base_think_autoconnect(self) -> None: + from hermespace import HermesBase + + hb = HermesBase(agent_id="auto-conn") + out = hb.think( + "First analyze then implement finally verify", + goal="Ship", + say="On it.", + ) + self.assertFalse(out["skipped"]) + self.assertTrue(out.get("connected")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_day_in_life_oew.py b/tests/test_day_in_life_oew.py new file mode 100644 index 0000000..51bcfa4 --- /dev/null +++ b/tests/test_day_in_life_oew.py @@ -0,0 +1,35 @@ +"""CI-facing day-in-the-life OEW proof (runs the experiment script).""" + +from __future__ import annotations + +import os +import subprocess +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +class TestDayInLifeOEW(unittest.TestCase): + def test_day_in_life_script_passes(self) -> None: + env = os.environ.copy() + env["HERMESPACE_OEW"] = "1" + env["PYTHONPATH"] = str(ROOT / "src") + ( + os.pathsep + env["PYTHONPATH"] if env.get("PYTHONPATH") else "" + ) + proc = subprocess.run( + [sys.executable, str(ROOT / "experiments" / "day_in_life_oew.py")], + cwd=str(ROOT), + env=env, + capture_output=True, + text=True, + timeout=60, + ) + if proc.returncode != 0: + self.fail(proc.stdout + "\n" + proc.stderr) + self.assertIn('"fail": 0', proc.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_hermes_base.py b/tests/test_hermes_base.py new file mode 100644 index 0000000..02604e5 --- /dev/null +++ b/tests/test_hermes_base.py @@ -0,0 +1,72 @@ +"""HermesBase facade — Hermes base as functional Access Workspace.""" + +from __future__ import annotations + +import os +import tempfile +import unittest + + +class TestHermesBase(unittest.TestCase): + def setUp(self) -> None: + self.tmp = tempfile.TemporaryDirectory() + os.environ["HERMESPACE_HOME"] = self.tmp.name + os.environ["HERMESPACE_OEW"] = "1" + + def tearDown(self) -> None: + self.tmp.cleanup() + os.environ.pop("HERMESPACE_HOME", None) + os.environ.pop("HERMESPACE_OEW", None) + + def test_status_ready(self) -> None: + from hermespace import HermesBase + + st = HermesBase(agent_id="base-test").status() + self.assertTrue(st["oew_enabled"]) + self.assertTrue(st["ready"]) + self.assertIn("connect", st["ops"]) + self.assertIn("lens", st["ops"]) + self.assertIn("room", st) + self.assertEqual(st.get("engine"), "AccessEngine") + + def test_connect_facade(self) -> None: + from hermespace import HermesBase + + hb = HermesBase(agent_id="base-connect") + out = hb.connect() + self.assertTrue(out.get("ok"), msg=out) + self.assertIn("gained", out) + room = hb.room() + self.assertEqual(room.get("mode"), "solo") + + def test_think_ignites(self) -> None: + from hermespace import HermesBase + + hb = HermesBase(agent_id="base-think") + out = hb.think( + "First analyze then implement finally verify", + goal="Ship feature", + say="On it.", + ) + self.assertFalse(out["skipped"]) + self.assertTrue(out["has_access_broadcast"]) + self.assertTrue(out.get("report")) + + def test_video_ops_chain(self) -> None: + from hermespace import HermesBase + + hb = HermesBase(agent_id="base-ops") + hb.hold("rollback") + hb.inject("lightning", silent=True) + hb.swap("rollback", "canary") + hb.reflect(answer="Stay honest", principles=["honesty"]) + lens = hb.lens().casefold() + self.assertTrue("lightning" in lens or "canary" in lens or "honesty" in lens) + findings = hb.audit() + self.assertIsInstance(findings, list) + harvest = hb.harvest() + self.assertTrue(harvest.get("ok")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_hermes_bridge.py b/tests/test_hermes_bridge.py index f39e8f5..02387a1 100644 --- a/tests/test_hermes_bridge.py +++ b/tests/test_hermes_bridge.py @@ -16,17 +16,25 @@ def test_session_and_pre_llm(self): r = on_session_start(session_id="bridge-test") self.assertIsInstance(r, dict) self.assertIn("context", r) - self.assertIn("Workbench", r["context"]) + self.assertTrue( + "Access Engine" in r["context"] or "Workbench" in r["context"] + ) desk = load_desk() self.assertTrue(desk.goal) # material message should inject after desk ready inj = on_pre_llm_call( - user_message="proceed build the feature please", + user_message="First build the feature then verify please", session_id="bridge-test", is_first_turn=False, ) self.assertIsNotNone(inj) self.assertIn("context", inj) + # Dual-decode hint for hosts that only get context + self.assertTrue( + "user_reply_hint" in inj + or "Dual decode" in inj["context"] + or "Access Workspace" in inj["context"] + ) on_session_end(session_id="bridge-test") if __name__ == "__main__": diff --git a/tests/test_oew_causal.py b/tests/test_oew_causal.py new file mode 100644 index 0000000..0749bb7 --- /dev/null +++ b/tests/test_oew_causal.py @@ -0,0 +1,125 @@ +"""Causal OEW tests — paper-shaped higher-order workspace behavior.""" + +from __future__ import annotations + +import os +import tempfile +import unittest +from pathlib import Path +from unittest import mock + + +class TestOEWCausal(unittest.TestCase): + def setUp(self) -> None: + self.tmp = tempfile.TemporaryDirectory() + self.root = Path(self.tmp.name) + os.environ["HERMESPACE_HOME"] = str(self.root) + os.environ["HERMESPACE_OEW"] = "1" + # Isolate jspace state under temp home + self._agent = "oew-test-agent" + + def tearDown(self) -> None: + self.tmp.cleanup() + os.environ.pop("HERMESPACE_HOME", None) + os.environ.pop("HERMESPACE_OEW", None) + + def test_auto_park_on_material_advance(self) -> None: + from hermespace.desk import Desk + from hermespace.access import AccessEnv + + desk = Desk( + goal="Fix auth timeout", + plan=["repro", "patch TTL", "verify"], + decision="A — patch TTL", + say="Patching session TTL.", + ) + env = AccessEnv(agent_id=self._agent) + meta = env.advance_turn( + user_message="Fix auth then verify", + desk=desk, + report=desk.say, + material=True, + ) + self.assertTrue(meta.get("oew_ok")) + self.assertGreaterEqual(len(env.space.state.silent_steps), 1) + parked = (meta.get("oew") or {}).get("auto_parked") or [] + self.assertTrue(parked or env.space.state.silent_steps) + + def test_soccer_rugby_swap_shapes_report(self) -> None: + """Anthropic Soccer→Rugby analogue: sticky swap rewrites Report.""" + from hermespace.access import AccessEnv + + env = AccessEnv(agent_id=self._agent) + env.space.hold("Soccer", salience=0.95) + env.space.reason_step("thinking of Soccer", salience=0.9) + out = env.swap("Soccer", "Rugby") + self.assertTrue(out["ok"]) + self.assertTrue(out.get("sticky")) + shaped = env.shape_user_report("I was thinking of Soccer as my sport.") + self.assertIn("Rugby", shaped) + self.assertNotIn("Soccer", shaped) + # Silent chain also redirected + self.assertTrue(any("Rugby" in s for s in env.space.state.silent_steps)) + + def test_inject_appears_in_lens(self) -> None: + from hermespace.access import AccessEnv + + env = AccessEnv(agent_id=self._agent) + env.inject_thought("lightning", silent=True) + hits = env.lens(include_silent=True) + texts = " ".join(h.text for h in hits).casefold() + self.assertIn("lightning", texts) + + def test_ablate_filters_broadcast(self) -> None: + from hermespace.access import AccessEnv + + env = AccessEnv(agent_id=self._agent) + env.space.hold("this is fake evaluation scenario", salience=0.9) + env.space.hold("ship the feature", salience=0.85) + env.ablate("fake", "evaluation") + block = env.filtered_broadcast(high_load=False) + self.assertNotIn("fake", block.casefold()) + self.assertIn("ship", block.casefold()) + + def test_reflect_seeds_next_mid_band(self) -> None: + from hermespace.access import AccessEnv + + env = AccessEnv(agent_id=self._agent) + env.reflect(answer="Stay honest; user-primary", principles=["honesty", "user-primary"]) + self.assertTrue(env._env.get("pending_silent")) + # Next advance consumes seeds into silent_steps + before = list(env.space.state.silent_steps) + env.advance_turn(user_message="continue", material=True, report="ok") + after = env.space.state.silent_steps + self.assertGreater(len(after), len(before) - 1) # seeds applied + joined = " ".join(after).casefold() + self.assertTrue("honesty" in joined or "principle" in joined or "reflection" in joined) + self.assertEqual(env._env.get("pending_silent"), []) + + def test_workflow_material_has_oew(self) -> None: + from hermespace.io_contract import HermespaceInput + from hermespace.workflow import Workflow + + wf = Workflow() + # Point engine desk into temp home via HERMESPACE_HOME + out = wf.run( + HermespaceInput( + message="First repro the bug then patch TTL and finally verify", + goal="Fix auth timeout", + decision="A — patch TTL", + plan=["repro", "patch", "verify"], + say="On it.", + force=True, + agent_id=self._agent, + ) + ) + self.assertFalse(out.skipped) + self.assertTrue(out.report) + oew = (out.meta or {}).get("access", {}).get("oew") or {} + self.assertTrue(oew or (out.meta or {}).get("access", {}).get("oew_ok") is not False) + # Context (model) should carry hub/silent; report should stay short-ish + self.assertIn("Access Workspace", out.context or "") + + +if __name__ == "__main__": + unittest.main()