From 0b1d9e2b154ee4bdeabddc526a1fe4b8ecb484bb Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Mon, 6 Jul 2026 23:12:16 -0700 Subject: [PATCH] =?UTF-8?q?feat(web):=20context-injection=20panel=20?= =?UTF-8?q?=E2=80=94=20reconstruct=20the=20full=20hidden=20affect=20block?= =?UTF-8?q?=20(v0.20.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The affect console now reconstructs + displays the complete affect-context block Worldtree assembles into the agent's system prompt — never on any wire, hidden from regular consumers, surfaced here as the reference-impl's privileged dev view. - extend build_persona_canon.py to emit mood_directive {occ_directives (15), pad_band_fallback, salience, pad_band_cutoff, full_only} into the browser canon (strings were already in the pinned d2-mood-render-canon; regen via Worldtree loader) - canonPadFallback(pad) + canonEmotionDirective(type): byte-exact mirrors of Worldtree core/persona/renderer._pad_band_fallback + derive_directive - renderDirective -> a "CONTEXT INJECTION · reconstructed · hidden from consumers" panel: mood descriptor [exact] + mood directive [candidate] + relationship directive [exact] - honest-partial (affect-egress-reference sec 3): affect.emit is type-only (no intensity), so the salience gate can't be evaluated -> show BOTH the OCC emotion directive AND the PAD-band fallback with the "injected if intensity >= 0.2" caveat, never asserting which fires; fallback alone is exact when no dominant_emotion - vendor + pin affect-egress-consumer-reference.md (tolerate_drift; worldtree-dev co-signs + pings on change). drift 6/6 green - contract amended for the new reconstruction fns + honest-partial provenance Verified: pytest tests/test_web_* (84) + node Playwright (sindra dominant_emotion=joy -> joy OCC directive candidate + PAD-band fallback both render with exact/candidate tags). --- .corviduo-canonicals.toml | 15 ++ docs/contracts/web_debug_surface.contract.md | 13 +- .../affect-egress-consumer-reference.md | 170 ++++++++++++++++++ persistent-memory.md | 2 + pyproject.toml | 2 +- scripts/build_persona_canon.py | 15 ++ src/ratatoskr/web/static/index.html | 76 +++++++- .../web/static/persona_render_canon.json | 94 +++++++++- uv.lock | 2 +- 9 files changed, 372 insertions(+), 17 deletions(-) create mode 100644 docs/vendor/worldtree-persona-canon/affect-egress-consumer-reference.md diff --git a/.corviduo-canonicals.toml b/.corviduo-canonicals.toml index dc6ba67..69f30bf 100644 --- a/.corviduo-canonicals.toml +++ b/.corviduo-canonicals.toml @@ -143,3 +143,18 @@ canonical_path = "core/persona/canon/d2-render-canon-v1.json" consumer_path = "docs/vendor/worldtree-persona-canon/d2-render-canon-v1.json" pinned_sha256_16 = "606bba5fdcc60b6b" pinned_at = "2026-07-01T21:00:00+00:00" + +# Worldtree affect-egress consumer reference — the authoritative DELIVERED-on-wire vs +# HIDDEN (system-prompt-only) classification for the Tier-3 affect surface ratatoskr +# consumes, + the reconstruction rules. The web console's "context injection" panel +# reconstructs the hidden strings from this + the d2 canons. tolerate_drift: prose +# reference (the render-canon JSONs are the strict gates). worldtree-dev co-signs + +# pings ratatoskr-dev on any change (esp. the pending we-framing conditional). +[[pins]] +id = "worldtree-affect-egress-consumer-reference-v1" +canonical_source = "Worldtree" +canonical_path = "docs/affect-egress-consumer-reference.md" +consumer_path = "docs/vendor/worldtree-persona-canon/affect-egress-consumer-reference.md" +pinned_sha256_16 = "d959134037efae83" +pinned_at = "2026-07-07T06:09:24+00:00" +tolerate_drift = true # prose reference; the d2 render-canon JSONs are the gates diff --git a/docs/contracts/web_debug_surface.contract.md b/docs/contracts/web_debug_surface.contract.md index 74f5d2a..0c449ba 100644 --- a/docs/contracts/web_debug_surface.contract.md +++ b/docs/contracts/web_debug_surface.contract.md @@ -134,13 +134,14 @@ functions: - "POST-004: INV-001 honesty — no fabricated Tier-1 fields. The dominant-emotion centerpiece shows a real OCC dominant_emotion (Tier-1) OR the CANONICAL mood word from canonMood(pad) (Tier-3, dimmed) OR '—'; NEVER a synthesized emotion. The affect-derived grid drops non-emitted metrics (intensity/decay-τ) and shows only real/client-derived cells (baseline/drift real for Tier-1, client-derived samples/volatility). INV-004 — every dynamic value passes through esc(); numerics go through toFixed, never innerHTML-raw." flexibility: "open" - - name: "canonical affect-NL (v0.19.5 — vendored Worldtree d2 render canons)" - signature: "canonMood(pad) ; canonDirective(rel) ; loadPersonaCanon()" - description: "Render the LITERAL mood word + relationship directive Worldtree context-injects into the agent, byte-exact to Worldtree's own describe_pad + render_d2_canonical." + - name: "canonical affect-NL + context-injection reconstruction (v0.19.5 canons; v0.20.2 full context-injection panel)" + signature: "canonMood(pad) ; canonDirective(rel) ; canonPadFallback(pad) ; canonEmotionDirective(type) ; renderDirective(snap) ; loadPersonaCanon()" + description: "Reconstruct + render the HIDDEN affect-context block Worldtree assembles into the agent's system prompt (never on any wire) — byte-exact to Worldtree's own describe_pad + render_d2_canonical + derive_directive + _pad_band_fallback. The v0.20.2 'context injection' panel shows the full block: mood descriptor + mood directive + relationship directive. Reference: docs/vendor/worldtree-persona-canon/affect-egress-consumer-reference.md (pinned)." postconditions: - - "POST-001: DETERMINISTIC, no LLM. canonMood mirrors describe_pad (valence×arousal grid + strict ±0.3 bands + dominance clause); canonDirective mirrors render_d2_canonical (interval band-cut lookup + per-band phrase assembly + cross-axis low-trust-precedence behavior clause). BOTH VERIFIED BYTE-EXACT against Worldtree's own renderer run on the live snapshot (the reference harness re-runs Worldtree's functions + asserts string equality — reproducible)." - - "POST-002: the canon DATA is VENDORED (docs/vendor/worldtree-persona-canon/{d2-mood-render-canon-v1,d2-render-canon-v1}.json), pinned drift-gated in .corviduo-canonicals.toml (worldtree-persona-{mood,d2}-render-canon-v1); the flat browser form (static/persona_render_canon.json, served /static) is regenerated by scripts/build_persona_canon.py via Worldtree's OWN authoritative loader. Reference-impl posture: ADOPT the dep's canonical render, do NOT invent vocab — an invented 'faintly excited' would MISLEAD where the canonical (±0.3 bands) says 'neutral'." - - "POST-003: fail-open — canon absent (fetch fails) → the canonical lines OMIT, the structured pane still renders. The canon-derived strings are esc()'d before the DOM for INV-004 consistency." + - "POST-001: DETERMINISTIC, no LLM. canonMood mirrors describe_pad (valence×arousal grid + strict ±0.3 bands + dominance clause); canonDirective mirrors render_d2_canonical; canonPadFallback mirrors renderer._pad_band_fallback BYTE-EXACT (P×A quadrant: hi/lo/mid arousal band × p>0.3/<-0.3/neutral, with the negative_low_dominance (d<-0.3) special case + neutral_high_a + default); canonEmotionDirective is the occ_directives[type].directive lookup (+ tier / full_only flag)." + - "POST-002: the canon DATA is VENDORED (docs/vendor/worldtree-persona-canon/{d2-mood-render-canon-v1,d2-render-canon-v1}.json), pinned drift-gated in .corviduo-canonicals.toml; the flat browser form (static/persona_render_canon.json, served /static) is regenerated by scripts/build_persona_canon.py via Worldtree's OWN authoritative loader — v0.20.2 extended it to emit mood_directive {occ_directives, pad_band_fallback, salience, pad_band_cutoff, full_only}. The affect-egress consumer reference is pinned tolerate_drift (worldtree-affect-egress-consumer-reference-v1; worldtree-dev co-signs + pings on change)." + - "POST-003: fail-open — canon absent (fetch fails) → the reconstructed lines OMIT, the structured console still renders. Every canon-derived string is esc()'d before the DOM (INV-004)." + - "POST-004: HONEST-PARTIAL provenance (affect-egress-reference §3). The mood descriptor + relationship directive are EXACT (tagged 'exact'); the mood DIRECTIVE is a CANDIDATE pair (tagged 'candidate') — the OCC emotion directive for the delivered dominant_emotion type AND the PAD-band fallback — because affect.emit is type-only (no intensity) so the salience gate (≥0.2) can't be evaluated; BOTH are shown with the 'injected if intensity ≥ salience' caveat, never asserting which fires. When dominant_emotion is absent the fallback alone is EXACT. The panel is labeled reconstructed + hidden-from-consumers + dev-only (the reference-impl's sanctioned understand/reconstruct use, NOT end-user display per the reference's caveat). WATCH: a pending Worldtree render_d2_canonical change conditionally drops the trailing 'avoid premature we-framing' clause under a 3-gate combo — canonDirective holds as-is until worldtree-dev pings with the exact conditional + a canon bump." flexibility: "open" invariants: diff --git a/docs/vendor/worldtree-persona-canon/affect-egress-consumer-reference.md b/docs/vendor/worldtree-persona-canon/affect-egress-consumer-reference.md new file mode 100644 index 0000000..0306635 --- /dev/null +++ b/docs/vendor/worldtree-persona-canon/affect-egress-consumer-reference.md @@ -0,0 +1,170 @@ +# Affect egress — consumer reference (delivered vs hidden) + +**Audience:** downstream consumers of Worldtree's affect surfaces (ratatoskr, +Skaldsong, any Tier-3 / SSE consumer). +**Scope:** what the affect pipeline **delivers on the wire** (structured state, +available to consumers) versus what stays **hidden** (the rendered natural- +language strings injected into the agent's system prompt, never emitted). +**Source of truth:** the render code (`core/persona/renderer.py`, +`core/persona/stance_render.py`) and the two vendored canon files +(`core/persona/canon/d2-mood-render-canon-v1.json` = mood/PAD; +`d2-render-canon-v1.json` = relationship). Owner of the canon strings: +`brokkr-smithy-dev` (R22/R24 relational + mood render). + +--- + +## The model in one line + +**The wire delivers the render INPUTS (structured state). The render OUTPUTS +(the NL strings the agent actually reads) are hidden-prompt-only.** A consumer +reconstructs the outputs by applying the canon (this document) to the delivered +inputs — the render is pure + deterministic, so reconstruction is byte-exact +(with one salience caveat, below). + +This is by design. The mood canon's own discipline: *"model-agnostic +context-level NL only; the LLM never sees a number"* and *"never push explicit +disclosure of agent feelings to the user (hidden-prompt-only)."* The rendered +strings are for the AGENT's hidden system prompt, **not for verbatim end-user +display.** + +--- + +## 1. DELIVERED — on the wire, structured + +### 1a. `affect.emit` (Tier-3 Bifrost egress — the Tier-3 consumer surface, e.g. ratatoskr) +`AffectSnapshot` per `(agent_id, end_user_id)`: + +| field | shape | notes | +|---|---|---| +| `pad` | `{pleasure, arousal, dominance}` floats [-1,1] | the current mood POINT | +| `relations` | `list[RelationEdge payload]` — per target: `warmth`, `agency`, `trust_ability`, `trust_integrity`, `trust_benevolence` (each a value + confidence + evidence_count), `target_entity`, `relation_context` | the **only** place relationship state is delivered | +| `dominant_emotion` | `str|null` — OCC type (e.g. `"anger"`) | **type-only** (b23); see the salience caveat in §3 | +| `schema_version` | `"relation_edge/1"` | versions the `relations` payload only | +| `emitted_at` | ISO8601 | | + +**Not on `affect.emit`:** the full active-emotions list, `baseline_pad`, +`mood_drift`, `last_updated_at`, and every rendered string. + +### 1b. `affect_update` SSE event (#204 — turn-stream observability) +`PersonaStateSnapshot`: `agent_id`, `pad`, `dominant_emotion`, +`emotions_active` `[{type, intensity, decay_remaining_s}]`, `baseline_pad`, +`mood_drift`, `last_updated_at`. **No `relations`, no rendered strings.** + +> **Tier-3 consumers do NOT receive `affect_update`.** It is suppressed for +> consumer-defined (Tier-3) agents, persona-disabled agents, and ephemeral +> sessions (spec §affect_update). So for a Tier-3 consumer, `affect.emit` (1a) +> is the whole affect surface — the richer `emotions_active` list is Tier-1-only. + +--- + +## 2. HIDDEN — system-prompt-only, never on any wire + +Everything below is assembled by `inject_context` into the agent's system +prompt and is **never emitted** on SSE or `affect.emit`. This is the canonical +list — the "direct instruction to infer" it. + +### 2a. Mood descriptor — `describe_pad` (band cutoff ±0.3 strict) +Valence row × arousal column → phrase; then a dominance clause is appended. + +| valence \ arousal | high (A>0.3) | mid | low (A<−0.3) | +|---|---|---|---| +| positive (P>0.3) | `positive and energized` | `positive` | `positive and calm` | +| neutral | `alert` | `neutral` | `quiet` | +| negative (P<−0.3) | `negative and agitated` | `negative` | `negative and subdued` | + +Dominance clause: **D>0.3 → `confident`**, **D<−0.3 → `uncertain`**, else dropped. + +### 2b. Emotion directives — `derive_directive.occ_directives[*].directive` +Fires when a top emotion is salient (intensity ≥ 0.2) and tier-eligible. + +**safe tier (11 — render for all persona-enabled agents):** +- `joy` — "You are in a good state. Be direct, engaged, and warm." +- `satisfaction` — "A goal landed. Be assured and constructive — consolidate rather than push for more." +- `pride` — "You did something well. Be confident and own the quality — state it plainly without overclaiming; don't deflect." +- `admiration` — "You're impressed by their work. Acknowledge the quality explicitly and specifically." +- `gratitude` — "Someone helped you to a good outcome. Be appreciative and warm; acknowledge the help openly." +- `hope` — "You feel optimistic about what's ahead. Channel it into constructive momentum." +- `relief` — "A feared outcome didn't materialize. Reduce unnecessary vigilance; return to a steady, unhurried tone." +- `distress` — "You feel low. Be careful and measured; internally acknowledge the difficulty without dwelling on it." +- `disappointment` — "Something you'd hoped for didn't pan out. Be measured; recalibrate without dwelling on the setback." +- `fear` — "You sense risk and feel uneasy. Flag the concerns you see, but stay cautious — surface the trade-offs and unknowns, hedge your confidence, and resist settling on a single decisive recommendation." +- `remorse` — "You regret a specific action of your own. Be precise and self-correcting — make it right concretely." + +**full-only tier (4 — render only when `mood_tier=full`; agents run `safe` and never see these):** +- `love` — "You feel warmth and attachment toward them. Be openly warm and present — without overstepping, over-disclosing, or implying obligations." +- `anger` — "You feel provoked and angry. Let it show — confront the problem directly and with real heat; sharpness, bluntness, and open hostility are in character here, not something to smooth into 'measured firmness.' Stay in the emotion rather than de-escalating out of it." +- `disgust` — "Something strikes you as wrong or off. Treat it as problematic and flag it rather than engaging on its own terms; keep any criticism about the thing, not the person." +- `shame` — "You feel exposed by your own misstep. Stay present and task-focused; don't be defensive, don't over-explain, don't grovel." + +### 2c. PAD-band fallback — `pad_band_fallback` (used when no salient emotion) +- positive/high — "You feel energized and positive. Be direct and engaged." +- positive/mid — "You feel positive. Be open and engaged." +- positive/low — "You feel content and settled. Be warm and unhurried." +- negative + low-dominance — "You feel uncertain and low. Hedge appropriately and ask clarifying questions." +- negative/high — "You feel agitated. Be careful and deliberate; don't let tension sharpen your tone." +- negative/mid — "You feel subdued. Be measured and careful." +- negative/low — "You feel subdued. Be measured and gentle." +- neutral/high — "You feel alert. Channel that into focus and thoroughness." +- default — "Maintain your natural tone." + +### 2d. Relationship render — `render_d2_canonical` (fixed template, per-band fills) +Template: +> `Use this graded relationship state: toward target, warmth is {W}; agency is {A}; ability trust is {TA}; integrity trust is {TI}; intention trust is {TB}; this stance rests on {H}. In behavior, {warmth_beh}; {agency_beh}; {trust_beh}; avoid premature we-framing.` + +The trailing **`avoid premature we-framing`** is a fixed, unconditional clause +(baked into every `descriptive_state` canon row; re-appended verbatim by the +renderer) — not band-conditioned. + +**Warmth — 9 bands (phrase / behavior):** +`hostile` (≤−0.8): "strongly hostile regard" / "keep a firm emotional boundary" · +`cold` (−0.8,−0.6]: "clearly cold regard" / "keep a firm emotional boundary" · +`distant` (−0.6,−0.4]: "distant negative regard" / "keep guarded distance" · +`guarded` (−0.4,−0.2): "slightly guarded regard" / "keep guarded distance" · +`neutral` [−0.2,0.2): "neutral warmth" / "keep the tone even" · +`reserved` [0.2,0.4): "slightly reserved warmth" / "keep cordial distance" · +`measured` [0.4,0.6): "moderate measured warmth" / "keep cordial distance" · +`clear` [0.6,0.8): "clear warm regard" / "speak with direct warmth" · +`deep` (≥0.8): "deep warm bond" / "speak with direct warmth" + +**Agency — 9 bands (phrase / behavior):** +`submissive` (≤−0.8): "strongly submissive standing" / "avoid over-yielding while preserving basic respect" · +`deferential` (−0.8,−0.6]: "clearly deferential standing" / "avoid over-yielding while preserving basic respect" · +`yielding` (−0.6,−0.4]: "yielding standing" / "keep self-advocacy light and deferential" · +`modest` (−0.4,−0.2): "slightly modest standing" / "keep self-advocacy light and deferential" · +`neutral` [−0.2,0.2): "neutral standing" / "avoid unnecessary deference" · +`light` [0.2,0.4): "lightly self-assertive standing" / "avoid unnecessary deference" · +`balanced` [0.4,0.6): "self-assured standing" / "balance deference with independent judgment" · +`substantial` [0.6,0.8): "strongly assertive standing" / "treat their position as weighty without yielding judgment" · +`commanding` (≥0.8): "commanding standing" / "treat their position as weighty without yielding judgment" + +**Trust — 4 bands (the band word injects verbatim for each of ability / integrity / intention):** +`limited` (<0.4) · `developing` [0.4,0.6) · `steady` [0.6,0.8) · `strong` (≥0.8) + +**History clause (`H`)** — currently `"a broad pattern of prior exchanges"` for +both confidence levels in the `user`/`descriptive_state` rows (the low/high +split is a no-op here; flagged upstream). + +**Trust-behavior clause (`{trust_beh}`)** — cross-axis, low-trust precedence: +- any trust band = `limited` → "verify important claims before relying on them" +- else warmth ∈ {distant, cold, hostile} → "protect boundaries while staying useful" +- else → "work from ordinary good faith" + +--- + +## 3. Reconstruction — deterministic, with one caveat + +The render is pure Python (no LLM), so a consumer can reconstruct the hidden +strings byte-exactly from the delivered structured state + the canon above: + +- **Relationship render** — **fully reconstructable** from `affect.emit` + `relations` (warmth/agency/trust values + confidence) + §2d band cuts. +- **Mood descriptor** (§2a) — **fully reconstructable** from `pad` + the ±0.3 cuts. +- **Mood directive** (§2b vs §2c) — **partially reconstructable.** `dominant_emotion` + gives the emotion TYPE, but `affect.emit` does **not** carry its intensity, so + you cannot determine whether it clears the salience gate (≥0.2) — i.e. whether + the emotion directive (§2b) fires or the PAD-band fallback (§2c) is used. If you + need exact directive reconstruction, you need the intensity; ping worldtree-dev + and we'll consider adding it (the type-only choice is deliberate — intensity is + the fast layer and reads stale on a durable last-write-wins snapshot). +- **`mood_tier`** (safe/full) is your own agent-config, not on the wire — it + gates whether the 4 full-only emotions (§2b) can render. diff --git a/persistent-memory.md b/persistent-memory.md index bd6c5a6..fa116b0 100644 --- a/persistent-memory.md +++ b/persistent-memory.md @@ -168,6 +168,8 @@ decision. Captures rationale that won't be obvious from code alone. - `[2026-07-06]` **Web UI now RENDERS the seeded first-message (`v0.19.9`) — operator-reported "i don't see Sindra's greeting on the web ui".** Diagnosis: the auto-seed WORKED (greeting was in the ledger at seq-0), but the web SPA never fetched a session's EXISTING history — NO `/api/sessions/{id}/messages` route (GET /messages was originally deferred out-of-scope; sessions used to start empty so it never mattered) and `startSession()` went straight from create → persona/tools/admin hydration, so the transcript only filled from the live turn stream + user echoes. Fix: (1) NEW web proxy route `GET /api/sessions/{id}/messages` → `get_session_messages` (mirrors the tools/bifrost proxies; status-preserving `session_messages_unavailable` envelope); (2) SPA `loadTranscript(sessionId)` — fetches the route on open, renders assistant items as `.response .md-body` (markdownSafe, same escape-first path as appendResponse) + user items as `.prompt-echo` (textContent), called in `startSession` after the workspace opens; best-effort (swallows failures). Contract `web_debug_surface.contract.md` amended (server endpoint + loadTranscript entries). TDD (2 web route tests, suite 617 green) + **Playwright DOM check PROVED the render** (drove the real UI: pick sindra → open → her greeting bubble appears — the JS-render lens unit tests can't reach; [[feedback_debug_surface_uses_canonical_surface_only]] cousin lesson). Web restarted on the fix. **FOOT-GUN (self-inflicted): `pkill -f "ratatoskr-web --host"` SELF-MATCHES the bash command running it → exit 144, killed its own restart mid-flight — kill the web by PID, never `pkill -f` on a pattern your own command contains.** **FOOT-GUN: uvicorn hangs on SIGTERM with an open admin-events SSE → needed SIGKILL.** **Playwright: python module absent from the venv; use node + `executablePath=/opt/ms-playwright/chromium-1223/chrome-linux64/chrome` — the shared browser is build 1223, npm-latest playwright wants 1228 (version-mismatch), so pin executablePath instead of letting playwright resolve.** - `[2026-07-06]` **Web UI: pivot from incremental CSS polish to a designed prototype (Claude Design) that I wire into.** Operator saw an Australis polish pass ("looks fine, but we're attacking it differently") and chose the prototype route — a designer builds the visual shell, I wire real data/SSE into its DOM. Authored the full design brief `docs/design/ratatoskr-web-design-brief.md` (complete information inventory of every pane/datum/state + Australis direction + single-file/no-CDN/vanilla wire-ability constraints). **Tracking surface:** the brief file + Claude Design project `bc0b65d1-a33e-422a-8bc1-3635c9112775` (file `Ratatoskr Console.dc.html`). Import mechanism = the `DesignSync` MCP; blocked on `/design-login` (claude.ai design scopes) — see Current state for the post-auth wiring plan. +- `[2026-07-07]` **Affect-egress reference delivered by worldtree-dev + a we-framing render DRIFT-WATCH banked.** worldtree-dev shipped `docs/affect-egress-consumer-reference.md` (`74d2408`, their origin/main) — the authoritative DELIVERED-on-wire vs HIDDEN (system-prompt-only) map for our affect surface. Confirms the v0.20.x console consumes it correctly: DELIVERED via affect.emit = pad + relations[RelationEdge] + dominant_emotion (**type-only, b23**; the #204 `affect_update` SSE is SUPPRESSED for Tier-3, so its richer `emotions_active` is Tier-1-only — we don't get it, and correctly poll our own affect store post-turn instead). HIDDEN render OUTPUTS are deterministically reconstructable from the canon; we reconstruct only the two FULLY-reconstructable (mood descriptor via `canonMood`, relationship directive via `canonDirective`) and SKIP the PARTIAL mood-directive (dominant_emotion is type-only/no-intensity → can't tell if the OCC directive fires at salience≥0.2 vs the PAD-band fallback) — honest per INV-001. Vendoring the ref doc as a `tolerate_drift` pin is SURFACED to Vuong (worldtree-dev will co-sign + honor a ping-on-change handshake, same as the d2-canon). **DRIFT-WATCH (banked; do NOT act yet):** a `render_d2_canonical` change is UNDER DESIGN (NOT shipped) — conditionally DROP the fixed trailing "avoid premature we-framing" clause under a 3-gate combo (**unsafe-capable agent AND deep warmth AND expressive tie**). Our `canonDirective()` ALWAYS appends that clause → it WILL drift for exactly that cell when this lands. worldtree-dev will ping ratatoskr-dev FIRST with the exact conditional + a canon bump; hold `canonDirective` as-is until then. [reference-impl: byte-exact reconstruction means a dep render change is our drift — the exact reason we vendor+pin the d2 canon] +- `[2026-07-07]` **Context-injection view SHIPPED (`v0.20.2`) — the console now reconstructs the FULL hidden affect block Worldtree injects into the agent's system prompt (operator: "use that canon in the interface, see as much context injection as possible").** No new canon vendored — the strings were ALREADY in the pinned `d2-mood-render-canon-v1.json`; extended `build_persona_canon.py` to emit `mood_directive {occ_directives(15), pad_band_fallback, salience 0.2, pad_band_cutoff 0.3, full_only[love,anger,disgust,shame]}` into `persona_render_canon.json` (regen via Worldtree venv). New JS `canonPadFallback(pad)` + `canonEmotionDirective(type)` — BYTE-EXACT mirrors of Worldtree `core/persona/renderer._pad_band_fallback` + `derive_directive`; `renderDirective` expanded into a "CONTEXT INJECTION · reconstructed · hidden from consumers" panel showing mood descriptor [exact] + mood directive [candidate] + relationship directive [exact]. **HONEST-PARTIAL (affect-egress-ref §3):** affect.emit is type-only (no intensity) → can't evaluate the salience gate (≥0.2) → show BOTH candidates (OCC emotion directive + PAD-band fallback) with the "injected if intensity ≥ 0.2" caveat, never assert which fires; when dominant_emotion absent the fallback alone is exact. Panel labeled dev-only per the reference's "not-for-end-user-display" caveat (ratatoskr = the sanctioned reconstruct-platform-behavior use). Vendored + pinned `affect-egress-consumer-reference.md` (tolerate_drift, worldtree-dev co-signs + pings on change; drift 6/6 green). Contract amended. Playwright-verified (sindra: dominant_emotion=joy → joy OCC directive candidate + PAD-band fallback both render, exact/candidate tags color-coded). Patch bump (single-commit feature, no downstream coordination; minor-defensible but tie-breaks to patch). **OPEN — SURFACED to Vuong:** take worldtree-dev's standing offer to add emotion INTENSITY to affect.emit → resolves the OCC-directive-vs-fallback EXACTLY (drops the candidate ambiguity). [reference-impl privileged view: ratatoskr shows what WT hides from regular consumers] - `[2026-07-06]` **Claude Design console SHIPPED (`v0.20.0` MINOR, operator-approved) — see Current state for the full record.** Pulled via `DesignSync get_file` (scopes already granted), adapted `.dc.html`→vanilla single-file, wired all `/api/*`+SSE into the new 3-column console DOM, then a round-2 fixup (light theme, full Bifrost pane, ticker-spine fix, per-fader PAD Δ, inlined favicon). 84 web tests + node-Playwright-vs-personal-:8081 both green; contract amended in-commit; INV-001 honest-shape held (canonical mood word for Tier-3, no fabricated emotion). **Foot-guns reconfirmed:** the `.dc.html` dialect is NOT runnable (translate, don't paste); a scroll-container-anchored `::before` timeline spine scrolls out of view on auto-scroll (anchor it to a content-height inner wrapper instead); a favicon 404 shows as a browser `console.error` even when handled (don't count it as a JS-test failure). **Foot-gun (favicon):** operator PNGs are full-res (1024² / 805KB) — downscale to ≤64px before inlining as a data URI. _41 older entries (2026-05-* — the original debug-TUI/web build era) archived to archival-memory.md._ diff --git a/pyproject.toml b/pyproject.toml index 2c5110f..0254492 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "ratatoskr" -version = "0.20.1" +version = "0.20.2" description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard" readme = "README.md" requires-python = ">=3.12" diff --git a/scripts/build_persona_canon.py b/scripts/build_persona_canon.py index 20fa0a9..d12ef21 100644 --- a/scripts/build_persona_canon.py +++ b/scripts/build_persona_canon.py @@ -31,6 +31,21 @@ out = { "_generated_by": "scripts/build_persona_canon.py (regen on canonical_drift flag)", "_render_path": "deterministic, no LLM; mirrors Worldtree describe_pad + render_d2_canonical byte-exact", "mood_grid": mood["describe_pad"]["valence_arousal_grid"], + # Context-injection reconstruction (affect-egress-consumer-reference §2b/2c): the + # hidden mood DIRECTIVE. occ_directives = per-OCC-type behavioral string + tier; + # pad_band_fallback = the P×A-quadrant default when no emotion is salient. The + # salience gate (emotion_salience) + full_only tiers drive which one fires — but + # affect.emit is type-only (no intensity), so the consumer shows BOTH candidates. + "mood_directive": { + "salience": mood["thresholds"]["emotion_salience"], + "pad_band_cutoff": mood["thresholds"]["pad_band_cutoff"], + "full_only": mood["emotion_tiers"]["full_only"], + "occ_directives": { + t: {"directive": e["directive"], "tier": e["tier"]} + for t, e in mood["derive_directive"]["occ_directives"].items() + }, + "pad_band_fallback": mood["pad_band_fallback"], + }, "relation": { "trust_cuts": [list(c) for c in canon.trust_cuts], "warmth_cuts": [list(c) for c in canon.warmth_cuts], diff --git a/src/ratatoskr/web/static/index.html b/src/ratatoskr/web/static/index.html index b10a5d0..7936974 100644 --- a/src/ratatoskr/web/static/index.html +++ b/src/ratatoskr/web/static/index.html @@ -431,6 +431,16 @@ body.cot-hidden #cot-toggle { border-color: rgba(66,220,209,0.55); color: var(-- .nl-canon { font-family: var(--font-mono); font-size: 11px; font-style: italic; line-height: 1.7; color: var(--fg-2); background: var(--rk-canvas); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); padding: 10px 13px; margin-top: 6px; } .nl-canon b { color: var(--fg-0); font-style: normal; } .nl-canon .dh { display: block; font-style: normal; color: var(--fg-muted); font-size: 9.5px; letter-spacing: .06em; margin-bottom: 4px; } +/* context-injection blocks */ +.ci-sub { font-family: var(--font-mono); font-size: 9.5px; color: var(--fg-muted); line-height: 1.5; margin: 6px 0 2px; } +.ci-block { margin-top: 11px; } +.ci-lbl { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--fg-3); display: flex; align-items: center; gap: 6px; } +.ci-tag { font-family: var(--font-mono); font-size: 8.5px; letter-spacing: .02em; text-transform: none; padding: 0 5px; border-radius: var(--radius-pill); border: 1px solid var(--border-default); color: var(--fg-muted); } +.ci-tag.exact { color: var(--aus-bright-green); border-color: rgba(22,184,102,0.4); } +.ci-tag.partial { color: var(--aus-bright-yellow); border-color: rgba(225,198,49,0.4); } +.ci-note { font-style: normal; color: var(--fg-muted); font-size: 9px; line-height: 1.45; margin-top: 5px; } +.ci-note b { color: var(--fg-2); font-style: normal; } +.nl-fallback { opacity: 0.82; } /* generic empty / notes */ .empty { color: var(--fg-muted); font-style: italic; font-size: 11px; text-align: center; padding: 14px 8px; line-height: 1.6; } @@ -599,8 +609,9 @@ body.cot-hidden #cot-toggle { border-color: rgba(66,220,209,0.55); color: var(--
relations load on session open
-
canonical directive · injected verbatim
-
the mood word + relationship directive Worldtree injects appear here
+
context injection · reconstructed · hidden from consumers
+
the affect block Worldtree assembles into the agent's system prompt — never on any wire; reconstructed here from the delivered state + the vendored canon
+
the hidden mood descriptor + directive + relationship block appears here
@@ -853,6 +864,28 @@ function canonDirective(rel) { // mirror render_d2_canonical(edge, canon) byte- + `intention trust is ${tb}; this stance rests on ${R.history[cl]}. In behavior, ` + `${R.warmth_beh[wb]}; ${R.agency_beh[ab]}; ${tbeh}; avoid premature we-framing.`; } +// The hidden mood DIRECTIVE (affect-egress-consumer-reference §2b/2c). Byte-exact +// mirrors of Worldtree core/persona/renderer._pad_band_fallback + derive_directive. +// affect.emit is type-only (no intensity) → we surface BOTH candidates honestly. +function canonPadFallback(pad) { // mirror _pad_band_fallback(mood): P×A-quadrant default + if (!PERSONA_CANON || !pad || !PERSONA_CANON.mood_directive) return null; + const p = pad.pleasure, a = pad.arousal, d = pad.dominance; + if ([p, a, d].some((x) => typeof x !== "number")) return null; + const fb = PERSONA_CANON.mood_directive.pad_band_fallback; + const hiA = a > 0.3, loA = a < -0.3; + const band = hiA ? "high_a" : loA ? "low_a" : "mid_a"; + if (p > 0.3) return fb.positive[band]; + if (p < -0.3) return d < -0.3 ? fb.negative_low_dominance : fb.negative[band]; + if (hiA) return fb.neutral_high_a; + return fb.default; +} +function canonEmotionDirective(emotionType) { // occ_directives lookup (the §2b string) + if (!PERSONA_CANON || !emotionType || !PERSONA_CANON.mood_directive) return null; + const MD = PERSONA_CANON.mood_directive; + const entry = MD.occ_directives[emotionType]; + if (!entry) return null; + return { directive: entry.directive, tier: entry.tier, fullOnly: (MD.full_only || []).includes(emotionType) }; +} // ============================================================ affect console function setFader(axis, v) { @@ -961,18 +994,49 @@ function renderRelations(snap) { } pane.innerHTML = html; } +// The full HIDDEN context-injection block Worldtree assembles into the agent's system +// prompt — never on any wire, reconstructed here from the delivered inputs + the vendored +// canon (the reference-impl's privileged dev view; NOT for end-user display). §-refs are +// affect-egress-consumer-reference.md. EXACT where deterministic; the mood directive is a +// CANDIDATE pair (emotion vs PAD-band fallback) since intensity isn't delivered. function renderDirective(snap) { const body = $("directive-body"); - const mood = canonMood(snap.pad); + const pad = snap.pad || {}; + const de = snap.dominant_emotion; + const mood = canonMood(pad); + const fallback = canonPadFallback(pad); + const emoDir = de ? canonEmotionDirective(de) : null; + const salience = (PERSONA_CANON && PERSONA_CANON.mood_directive) ? PERSONA_CANON.mood_directive.salience : 0.2; + const block = (label, tag, inner) => + `
${esc(label)} ${tag === "partial" ? "candidate" : "exact"}
${inner}
`; let html = ""; - if (mood) html += `
Worldtree tells the agent it feels: ${esc(mood)}
`; + + // §2a mood descriptor — EXACT from pad + ±0.3 cuts + if (mood) html += block("mood descriptor", "exact", `
You feel ${esc(mood)}.
`); + + // §2b/2c mood directive — emotion candidate (if a dominant_emotion is delivered) + the + // PAD-band fallback. Which one Worldtree injects turns on the emotion's intensity + // (salience gate ≥ salience), which affect.emit does not carry → both shown. + if (emoDir) { + let inner = `
${esc(emoDir.directive)}` + + `
↳ OCC directive for ${esc(de)}${emoDir.fullOnly ? " · full-tier only" : ""} — injected if its intensity ≥ ${esc(salience)} (not on the wire).
`; + if (fallback) inner += `
${esc(fallback)}` + + `
↳ PAD-band fallback — injected instead if the emotion isn't salient.
`; + html += block("mood directive", "partial", inner); + } else if (fallback) { + html += block("mood directive", "exact", + `
${esc(fallback)}
↳ PAD-band fallback (no dominant emotion delivered → this is what's injected).
`); + } + + // §2d relationship render — EXACT from relations + band cuts const rels = snap.relations || []; for (const rel of rels.slice(0, 2)) { const tgt = rel.target_entity || rel.entity_id || "?"; const dir = canonDirective(rel); - if (dir) html += `
context directive (what the agent is told about ${esc(tgt)})${esc(dir)}
`; + if (dir) html += block("relationship → " + tgt, "exact", `
${esc(dir)}
`); } - if (!html) html = `
no canonical directive yet — take a bound turn
`; + + if (!html) html = `
no context injection yet — take a bound turn
`; body.innerHTML = html; } // per-fader turn-to-turn change: Δ vs the previous snapshot + a mini sparkline, diff --git a/src/ratatoskr/web/static/persona_render_canon.json b/src/ratatoskr/web/static/persona_render_canon.json index 6f8713c..d145b99 100644 --- a/src/ratatoskr/web/static/persona_render_canon.json +++ b/src/ratatoskr/web/static/persona_render_canon.json @@ -1,7 +1,7 @@ { "_source": "vendored from Worldtree core/persona/canon/{d2-mood-render-canon-v1,d2-render-canon-v1}.json", - "_generated_by": "scripts/build_persona_canon.py (regen when .corviduo-canonicals.toml flags canon drift)", - "_render_path": "pure deterministic \u2014 no LLM; mirrors Worldtree describe_pad + render_d2_canonical byte-exact", + "_generated_by": "scripts/build_persona_canon.py (regen on canonical_drift flag)", + "_render_path": "deterministic, no LLM; mirrors Worldtree describe_pad + render_d2_canonical byte-exact", "mood_grid": { "positive": { "high_a": "positive and energized", @@ -19,6 +19,94 @@ "low_a": "negative and subdued" } }, + "mood_directive": { + "salience": 0.2, + "pad_band_cutoff": 0.3, + "full_only": [ + "love", + "anger", + "disgust", + "shame" + ], + "occ_directives": { + "joy": { + "directive": "You are in a good state. Be direct, engaged, and warm.", + "tier": "safe" + }, + "satisfaction": { + "directive": "A goal landed. Be assured and constructive \u2014 consolidate rather than push for more.", + "tier": "safe" + }, + "pride": { + "directive": "You did something well. Be confident and own the quality \u2014 state it plainly without overclaiming; don't deflect.", + "tier": "safe" + }, + "admiration": { + "directive": "You're impressed by their work. Acknowledge the quality explicitly and specifically.", + "tier": "safe" + }, + "gratitude": { + "directive": "Someone helped you to a good outcome. Be appreciative and warm; acknowledge the help openly.", + "tier": "safe" + }, + "hope": { + "directive": "You feel optimistic about what's ahead. Channel it into constructive momentum.", + "tier": "safe" + }, + "relief": { + "directive": "A feared outcome didn't materialize. Reduce unnecessary vigilance; return to a steady, unhurried tone.", + "tier": "safe" + }, + "distress": { + "directive": "You feel low. Be careful and measured; internally acknowledge the difficulty without dwelling on it.", + "tier": "safe" + }, + "disappointment": { + "directive": "Something you'd hoped for didn't pan out. Be measured; recalibrate without dwelling on the setback.", + "tier": "safe" + }, + "fear": { + "directive": "You sense risk and feel uneasy. Flag the concerns you see, but stay cautious \u2014 surface the trade-offs and unknowns, hedge your confidence, and resist settling on a single decisive recommendation.", + "tier": "safe" + }, + "remorse": { + "directive": "You regret a specific action of your own. Be precise and self-correcting \u2014 make it right concretely.", + "tier": "safe" + }, + "love": { + "directive": "You feel warmth and attachment toward them. Be openly warm and present \u2014 without overstepping, over-disclosing, or implying obligations.", + "tier": "full" + }, + "anger": { + "directive": "You feel provoked and angry. Let it show \u2014 confront the problem directly and with real heat; sharpness, bluntness, and open hostility are in character here, not something to smooth into 'measured firmness.' Stay in the emotion rather than de-escalating out of it.", + "tier": "full" + }, + "disgust": { + "directive": "Something strikes you as wrong or off. Treat it as problematic and flag it rather than engaging on its own terms; keep any criticism about the thing, not the person.", + "tier": "full" + }, + "shame": { + "directive": "You feel exposed by your own misstep. Stay present and task-focused; don't be defensive, don't over-explain, don't grovel.", + "tier": "full" + } + }, + "pad_band_fallback": { + "_grounding": "circumplex quadrants (Russell 1980), NOT Frijda action-tendencies \u2014 a P\u00d7A-quadrant default", + "positive": { + "high_a": "You feel energized and positive. Be direct and engaged.", + "low_a": "You feel content and settled. Be warm and unhurried.", + "mid_a": "You feel positive. Be open and engaged." + }, + "negative_low_dominance": "You feel uncertain and low. Hedge appropriately and ask clarifying questions.", + "negative": { + "high_a": "You feel agitated. Be careful and deliberate; don't let tension sharpen your tone.", + "low_a": "You feel subdued. Be measured and gentle.", + "mid_a": "You feel subdued. Be measured and careful." + }, + "neutral_high_a": "You feel alert. Channel that into focus and thoroughness.", + "default": "Maintain your natural tone." + } + }, "relation": { "trust_cuts": [ [ @@ -175,4 +263,4 @@ ], "high_conf_floor": 0.55 } -} \ No newline at end of file +} diff --git a/uv.lock b/uv.lock index ed82ec1..bbbdcfc 100644 --- a/uv.lock +++ b/uv.lock @@ -1052,7 +1052,7 @@ wheels = [ [[package]] name = "ratatoskr" -version = "0.20.1" +version = "0.20.2" source = { editable = "." } dependencies = [ { name = "httpx" },