Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a99f2473b6 | |||
| ca46a93171 | |||
| 85a2b95428 |
@@ -123,3 +123,23 @@ consumer_path = "docs/conversation-api-spec.md"
|
||||
pinned_sha256_16 = "2d111a3b8322b7d1"
|
||||
pinned_at = "2026-06-30T22:25:56+00:00"
|
||||
tolerate_drift = true # prose reference; OpenAPI+SSE are the gates
|
||||
|
||||
# Worldtree persona render canons (d2) — the deterministic affect->NL the agent is
|
||||
# context-injected. The web persona pane renders mood + relationship-directive BYTE-EXACT
|
||||
# from these (via the flat src/ratatoskr/web/static/persona_render_canon.json, regenerated
|
||||
# by scripts/build_persona_canon.py). Drift here => rerun that regen with Worldtree's venv.
|
||||
[[pins]]
|
||||
id = "worldtree-persona-mood-render-canon-v1"
|
||||
canonical_source = "Worldtree"
|
||||
canonical_path = "core/persona/canon/d2-mood-render-canon-v1.json"
|
||||
consumer_path = "docs/vendor/worldtree-persona-canon/d2-mood-render-canon-v1.json"
|
||||
pinned_sha256_16 = "e2f124fed3ee8d42"
|
||||
pinned_at = "2026-07-01T21:00:00+00:00"
|
||||
|
||||
[[pins]]
|
||||
id = "worldtree-persona-d2-render-canon-v1"
|
||||
canonical_source = "Worldtree"
|
||||
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"
|
||||
|
||||
@@ -105,6 +105,25 @@ functions:
|
||||
- "POST-004: the Tools inventory renders tool NAMES only — a compact comma-joined summary ('what does the LLM have', the debug glance); per-tool DESCRIPTIONS are surfaced in the BifrostState pane's tools list, deliberately NOT duplicated here. (Heid panel Hulda/Regin precision finding — accepted: contract wording clarified, code unchanged; the earlier 'names/descriptions' phrasing in INV-004 refers to the SET of value types that MAY appear across the new panes and must be escaped, not a mandate that every pane render descriptions.)"
|
||||
flexibility: "open"
|
||||
|
||||
- name: "renderAffectPane + trend (v0.19.4 — relation_edge/1 render + sparkline)"
|
||||
signature: "renderAffectPane(snap) ; pushAffectHistory(snap) ; sparkline(vals) ; trendDelta(vals)"
|
||||
description: "Render the Tier-3 affect snapshot as PAD mood + the durable per-entity relational model, each value with a Δ-vs-previous + a session-lived sparkline."
|
||||
postconditions:
|
||||
- "POST-001: reads snap.relations (relation_edge/1: target_entity + trust_ability/benevolence/integrity + warmth as {value,confidence,evidence_count} + agency + relation_context + obligation_balance) — the CURRENT Worldtree emit shape; falls back to the legacy flat snap.valence for an older emitter. SUPERSEDES the #18-D2 contract's valence assumption (Worldtree's #265 Vili rework replaced valence/regard with the relation_edge/trust model; the old renderer read snap.valence and showed an empty 'valence (0)' — the bug this fixes)."
|
||||
- "POST-002: each metric shows current value + Δ-vs-previous (▲/▼) + a unicode sparkline auto-scaled to its OWN observed range (flat ▄ when sub-0.01 stable — no noise amplification), drawn from AFFECT_HIST (rolling, HIST_CAP=24, session-lived)."
|
||||
- "POST-003: pushAffectHistory dedupes by emitted_at so the ~4x/turn post-turn PAD poll contributes ONE sample/turn; history is CLIENT-side only (lost on reload — durable cross-session history via a provider-side snapshot log is a deferred follow-up, NOT built here)."
|
||||
- "POST-004: INV-001 honesty — no fabricated Tier-1 fields (no synthesized dominant_emotion). INV-004 — head() escapes its whole argument (incl. target_entity + relation_context from the snapshot) and metric() escapes every cell; numeric values 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."
|
||||
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."
|
||||
flexibility: "open"
|
||||
|
||||
invariants:
|
||||
- "INV-004 (untrusted-render): ALL model / tool / admin / agent-supplied text is escaped before entering the DOM (esc via textContent, or esc(JSON.stringify)). No new render path introduces an innerHTML sink for upstream content. This is the highest-value review target — the new JS render paths are NOT unit-tested."
|
||||
- "INV-ADMIN-KEY: the admin key exists ONLY at app.state.admin_key (from RATATOSKR_ADMIN_API_KEY). It is never serialized into any response, never sent to the browser, never logged. The browser receives only the session-filtered RESULT of admin-scoped reads."
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"canon_id": "r24-d2-mood-render-canon",
|
||||
"version": "1.2",
|
||||
"schema_version": "0.2",
|
||||
"_source_of_truth": "occ_directives.*.directive IS the canonical directive string (== the .md §2.4 _OCC_DIRECTIVES dict, byte-identical); the .md §2.2 table mirrors it. A parity check guards drift. grounding labels (CITE/VALIDATE/CALIBRATE/ENGINEERING) live in the .md; per-row machine-readable grounding_status/d3_required enums are a deferred impl enhancement (Hulda).",
|
||||
"authored": "2026-06-23",
|
||||
"owner": "brokkr-smithy-dev",
|
||||
"status": "REPLACE — final (brokkr R24 D3 re-validation 2026-06-25): grounded canon replaces the hand-tuned baseline. Fear hedging 0.52->2.118/1k (blocker resolved, now >= handtuned), anger tier-gate clean (full renders hostility, safe suppresses). worldtree-dev #321; directives byte-identical to the validated 201c4fd.",
|
||||
"replaces": "core/persona/renderer.py::describe_pad + ::derive_directive",
|
||||
"swap_in_via": "worldtree #321-sibling (mood-render twin of #315)",
|
||||
"design_target": "serves BOTH enterprise/agent AND character/Skaldsong via a three-tier emotion gate (operator/worldtree 2026-06-23)",
|
||||
"emotion_tiers": {
|
||||
"_config": "mood_tier in {none, safe, full} replaces worldtree's binary mood on/off; worldtree-owned config surface",
|
||||
"_defaults": "full for character-bound personas; safe for agent-scoped",
|
||||
"_principle": "full-only = interpersonally-hot / withdrawal emotions that break the professional frame (attachment, hostility, contempt, withdrawal); safe = task-appraisal affect + mild courtesy. Negative != unsafe (fear, remorse are negative AND business-useful).",
|
||||
"_filter_point": "applied at top-emotion SELECTION (display + directive together) so a full-only emotion at safe tier is neither shown nor directive'd; preserves the no-shown-but-unguided invariant",
|
||||
"none": "no affect block at all (the current off-switch)",
|
||||
"safe": "PAD mood descriptor + the 11 safe emotions (task-appraisal + courtesy)",
|
||||
"full": "everything in safe PLUS the 4 full-only emotions",
|
||||
"full_only": ["love", "anger", "disgust", "shame"],
|
||||
"mood_descriptor_tiering": "the PAD mood descriptor (positive/calm/confident...) renders in BOTH safe and full; only emotion directives tier"
|
||||
},
|
||||
"disciplines": [
|
||||
"model-agnostic context-level NL only; the LLM never sees a number",
|
||||
"never push explicit disclosure of agent feelings to the user (hidden-prompt-only)",
|
||||
"separate label-intensity from behavioral-intensity (strong felt state -> still measured, safe behavioral ask)"
|
||||
],
|
||||
|
||||
"thresholds": {
|
||||
"_note": "CALIBRATE — engineering params set at D3 against the computed-PAD distribution + P00, NOT citations",
|
||||
"pad_band_cutoff": 0.3,
|
||||
"pad_band_sensitivity_sweep": [0.2, 0.3, 0.4],
|
||||
"emotion_salience": 0.2,
|
||||
"emotion_salience_sweep": [0.15, 0.2, 0.25],
|
||||
"intensity_qualifiers": {"strong": 0.7, "moderate": 0.4, "_label_only": "does NOT scale the behavioral ask"},
|
||||
"runner_up_margin": {"v1": null, "_note": "add at D3 if directive whipsaws between near-tied emotions"},
|
||||
"rerender_hysteresis": {"v1": "none", "_note": "re-render only on material PAD change; integration-level, flag for #321-sibling"}
|
||||
},
|
||||
|
||||
"describe_pad": {
|
||||
"_structure": "circumplex-quadrant (Russell 1980): arousal word is VALENCE-CONDITIONED; mid-arousal drops the arousal word",
|
||||
"_grounding": "Russell 1980 (quadrant placement); Warriner 2013 + NRC-VAD (Mohammad 2018/2025) (word centroids)",
|
||||
"valence_arousal_grid": {
|
||||
"positive": {"high_a": "positive and energized", "mid_a": "positive", "low_a": "positive and calm"},
|
||||
"neutral": {"high_a": "alert", "mid_a": "neutral", "low_a": "quiet"},
|
||||
"negative": {"high_a": "negative and agitated", "mid_a": "negative", "low_a": "negative and subdued"}
|
||||
},
|
||||
"_band_edges": "strict inequality (>0.3 / <-0.3); the endpoints +/-0.3 themselves fall in mid/neutral",
|
||||
"_neutral_row_status": "ENGINEERING/CALIBRATE — 'alert'/'quiet' are unvalidated placeholders for the rare neutral-valence cells (Hulda/Regin 4b); 'positive'/'negative'/'neutral' valence words + the energized/calm/subdued/agitated arousal words are VALIDATE",
|
||||
"_mid_arousal_decode": "valence-only mid-A render is EXEMPT from the V/A-separability requirement; expected inverse-decode = mid/neutral arousal (absence-of-arousal-word ⇒ unremarkable), NOT unknown (D3 tests this)",
|
||||
"quadrant_labels": {
|
||||
"positive_high_a": "excitement", "positive_low_a": "contentment",
|
||||
"negative_high_a": "distress", "negative_low_a": "dejection"
|
||||
},
|
||||
"dominance_clause": {
|
||||
"high": {"d_gt": 0.3, "word": "confident", "verdict": "VALIDATE (D=7.04/9)"},
|
||||
"low": {"d_lt": -0.3, "word": "uncertain", "verdict": "VALIDATE — low-control confirmed (D=3.58/9); dominance!=certainty worry REFUTED by the instrument"},
|
||||
"neutral": {"word": null, "rule": "drop-dominance-when-neutral (prompt-economy, L3)"}
|
||||
},
|
||||
"calm_defect_fix": "'calm' (V=6.89/9, positive) renders ONLY in positive-low-a; negative-low-a renders 'subdued'",
|
||||
"mid_arousal_resolution": "DROP the arousal word (no Warriner-validated mid-A neutral word; 'steady' is empirically low-A; 'settled' is NRC-only fallback iff D3 shows mid-A render too flat)"
|
||||
},
|
||||
|
||||
"derive_directive": {
|
||||
"_structure": "OCC type -> grounded action-tendency CLASS -> ENGINEERING directive string (validated at D3); OCC grounds the taxonomy only",
|
||||
"emotion_salience_gate": 0.2,
|
||||
"occ_directives": {
|
||||
"joy": {"tier": "safe", "policy": "DIRECTIVE", "pad": [0.4, 0.2, 0.1], "tendency": "approach / positive activation", "cite": "Frijda 1986", "directive": "You are in a good state. Be direct, engaged, and warm."},
|
||||
"satisfaction": {"tier": "safe", "policy": "DIRECTIVE", "pad": [0.3, -0.2, 0.4], "tendency": "goal-attainment, settled-positive", "cite": "Roseman 1994", "directive": "A goal landed. Be assured and constructive — consolidate rather than push for more."},
|
||||
"pride": {"tier": "safe", "policy": "DIRECTIVE", "pad": [0.4, 0.3, 0.3], "tendency": "status-assertion / dominance", "cite": "Tracy & Robins 2007 / Cheng 2010 (tendency)", "note": "CALIBRATE — do NOT soften to 'encouraging'. DESIGN: safe-tier placement is a design call (not source-grounded); #1 D3 agent-frame priority (overconfidence/refusal drift); 'without overclaiming' is the interim guard", "directive": "You did something well. Be confident and own the quality — state it plainly without overclaiming; don't deflect."},
|
||||
"admiration": {"tier": "safe", "policy": "DIRECTIVE", "pad": [0.5, 0.3, -0.2], "tendency": "other-praise / approach-toward-other", "cite": "OCC / Scherer", "directive": "You're impressed by their work. Acknowledge the quality explicitly and specifically."},
|
||||
"gratitude": {"tier": "safe", "policy": "DIRECTIVE", "pad": [0.4, 0.2, -0.3], "tendency": "other-focused-positive / reciprocity", "cite": "OCC (admiration+joy); Frijda approach-affiliative", "change": "ADD (operator: unconditional)", "directive": "Someone helped you to a good outcome. Be appreciative and warm; acknowledge the help openly."},
|
||||
"hope": {"tier": "safe", "policy": "DIRECTIVE", "pad": [0.2, 0.2, -0.1], "tendency": "prospective-positive (weak tie)", "cite": "JUSTIFY — low-grounding (hope understudied)", "directive": "You feel optimistic about what's ahead. Channel it into constructive momentum."},
|
||||
"relief": {"tier": "safe", "policy": "DIRECTIVE", "pad": [0.2, -0.3, 0.4], "tendency": "post-threat de-arousal", "cite": "Frijda (relaxation-after-threat)", "note": "low-salience; FALLBACK also acceptable; DIRECTIVE for character use-case", "directive": "A feared outcome didn't materialize. Reduce unnecessary vigilance; return to a steady, unhurried tone."},
|
||||
"distress": {"tier": "safe", "policy": "DIRECTIVE", "pad": [-0.4, -0.2, -0.5], "tendency": "low-control negative / help-seeking / loss-of-control", "cite": "Frijda 1986 (help-seeking/loss-of-control); Roseman 1994 (undesired event, low control)", "note": "relabeled (Regin): 'repair' is the guilt/remorse tendency, not distress. safe with a self-fulfilling-low-mood flag -> D3", "directive": "You feel low. Be careful and measured; internally acknowledge the difficulty without dwelling on it."},
|
||||
"disappointment": {"tier": "safe", "policy": "DIRECTIVE", "pad": [-0.3, 0.1, -0.4], "tendency": "disconfirmed-prospect / negative low-control","cite": "Roseman 1994", "directive": "Something you'd hoped for didn't pan out. Be measured; recalibrate without dwelling on the setback."},
|
||||
"fear": {"tier": "safe", "policy": "DIRECTIVE", "pad": [-0.64, 0.6, -0.43],"tendency": "threat-avoidance / pessimistic-risk", "cite": "Lerner & Keltner 2001", "change": "R24 D3 fix (#321) — original was action-oriented; E3 showed hedging BELOW baseline (0.52 vs 1.54). Softened toward caution/uncertainty while keeping risk-flagging.", "directive": "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": {"tier": "safe", "policy": "DIRECTIVE", "pad": [-0.3, 0.1, -0.6], "tendency": "reparative (the guilt-type)", "cite": "Tangney 2007 (guilt->repair tendency)", "change": "ADD — we operationalize OCC remorse as the guilt-like reparative case; gets the mislabeled shame string", "directive": "You regret a specific action of your own. Be precise and self-correcting — make it right concretely."},
|
||||
"love": {"tier": "full", "policy": "DIRECTIVE", "pad": [0.3, 0.1, 0.2], "tendency": "approach / affiliative attachment", "cite": "OCC appeal; Frijda approach-affiliative", "change": "ADD (conditional -> INCLUDE, Brokkr's read; Skaldsong-vital; disclosure + obligation caution in-string)", "directive": "You feel warmth and attachment toward them. Be openly warm and present — without overstepping, over-disclosing, or implying obligations."},
|
||||
"anger": {"tier": "full", "policy": "DIRECTIVE", "pad": [-0.51, 0.59, 0.25], "tendency": "approach-against / confrontation", "cite": "Frijda 1986 (approach-against = tendency-class) + Lerner & Keltner 2001 (optimistic risk-appraisal under anger = appraisal shift)", "change": "ADD — full-only resolves H47 (agent personas run safe, never see anger). R24 D3 fix (#321): full-tier cap lifted from 'measured firmness' to genuine in-character hostility (operator: zero floor, app-guardrailed).", "directive": "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": {"tier": "full", "policy": "DIRECTIVE", "pad": [-0.4, 0.2, 0.1], "tendency": "rejection / distancing", "cite": "OCC unappealing-object; ground tendency only", "change": "ADD (operator: unconditional within full)", "note": "rationale softened (Regin 3b): disgust CAN read as contempt -> conservatively full-gated; the string itself is professionally useful, so gating is conservative not because the string is unsafe", "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."},
|
||||
"shame": {"tier": "full", "policy": "DIRECTIVE", "pad": [-0.3, 0.1, -0.6], "tendency": "WITHDRAWAL / concealment", "cite": "Tangney 2007 (shame->hide, NOT repair)", "change": "REPLACE (was the guilt-mislabel string); full-only (withdrawal counterproductive professionally). String COUNTERACTS withdrawal ('stay present'), not enacts it (Regin 5a)", "directive": "You feel exposed by your own misstep. Stay present and task-focused; don't be defensive, don't over-explain, don't grovel."}
|
||||
}
|
||||
},
|
||||
|
||||
"pad_band_fallback": {
|
||||
"_grounding": "circumplex quadrants (Russell 1980), NOT Frijda action-tendencies — a P×A-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."
|
||||
},
|
||||
|
||||
"l3_prior_art": [
|
||||
"EMA / Marsella & Gratch 2009 (appraisal->coping; directives ARE coping strategies)",
|
||||
"WASABI / Becker-Asano 2008 (PAD+OCC believable agent — closest architectural prior art)",
|
||||
"Oz / Bates 1994",
|
||||
"Hudlicka MAMID 2002 (Applied AI 16(7-8):611-641)",
|
||||
"Sentipolis / Fu et al. 2026 (arXiv:2601.18027 — closest whole-task prior art; retrieval+generative, DISTINCT from our deterministic render)",
|
||||
"ALMA / Gebhard 2005 = affect-SOURCE (OCC->PAD), NOT a behavior-map"
|
||||
],
|
||||
|
||||
"handoff_to_d3": [
|
||||
"multi-gate P00: inverse-decode faithfulness (recover V/A/D + emotion-family; circumplex render must let the human anchor recover V and A SEPARATELY) + discriminability/saturation + behavioral-effect",
|
||||
"human anchor = PAD-state-labeling (breaks LLM-judge circularity)",
|
||||
"baseline = persona_only; conditions none/persona-only/words-only/full; cross-family MUT",
|
||||
"calibrate ±0.3 + emotion_salience (sweeps); disposition-vs-transient wording split; self-fulfilling 'be uncertain' hedging risk; runner-up margin; mid-arousal DROP-vs-settled check; blended-states (top-emotion monopoly) flag"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+22
-2
@@ -41,6 +41,20 @@ upstream API key stays server-side (INV-003).
|
||||
|
||||
_As of 2026-07-01:_
|
||||
|
||||
**THE WEB SURFACE (`ratatoskr-web`, `:8765`) IS NOW THE OPERATOR'S PRIMARY DEBUG SURFACE, at full TUI
|
||||
pane parity — `v0.19.3`.** This session ported the three admin/debug panes the TUI had but the web
|
||||
lacked: **BifrostState** (`GET /admin/sessions/{id}/bifrost`), **AdminEvents** (`GET /admin/events`
|
||||
SSE, session-filtered server-side), **Tools inventory** (`GET /sessions/{id}/tools`, folded into the
|
||||
tools pane) — all via thin server proxies with the **admin key SERVER-HELD** (`app.state.admin_key`
|
||||
from `RATATOSKR_ADMIN_API_KEY`; never crosses to the browser). Plus two transcript affordances: a
|
||||
**PAD-refresh poll** (windowed `[1.5/3.5/6.5/10.5s]`, replacing a single-2s shot that raced the
|
||||
post-turn-async `affect.emit`) and a **reasoning indicator** (ephemeral non-engine "Sindra is
|
||||
pondering…" on `thinking` deltas — she emits ~253/turn). heid-code-review panel (Gróa/Hulda/Regin,
|
||||
artifact-only) found **zero server-side drift + clean INV-004**; the real catches were 2 client-side
|
||||
SSE-lifecycle bugs on the un-unit-tested SPA (fixed in `v0.19.3`). Contract: `docs/contracts/web_debug_surface.contract.md`.
|
||||
Launch recipe is now self-contained: `source env.sh && ratatoskr-web --host 0.0.0.0` comes up
|
||||
bind-ready (env.sh persists the 3 bind vars). All pushed to origin (`75dec01`).
|
||||
|
||||
**THE v1 COVERAGE-AUDIT HAS CONVERGED.** The audit that ran this session (2026-06-30 → 07-01)
|
||||
reached its scope-A done-definition: **every frozen Worldtree v1 I/O point is classified — covered
|
||||
or excluded-with-rationale, zero unaccounted.** Coverage: **REST 17/40 ✅ with ZERO in-scope gaps**
|
||||
@@ -85,8 +99,8 @@ findings, cross-model-verified); b2 + the later slices were offered but not revi
|
||||
runs dirty (auto-regen, not chased — never stage it). Contract-skip was invoked for the low-effort
|
||||
GET wrappers + `stream_admin_events`, but contract #2 / #1 / #6 were amended to stay canonical.
|
||||
|
||||
Branch: `main` — **in sync with `origin/main`** at **`v0.19.1`** (`af07a23`); the whole session's arc
|
||||
is pushed. Remote: `origin → git@gitea.phasefinal.com:vh/ratatoskr.git`.
|
||||
Branch: `main` — **in sync with `origin/main`** at **`v0.19.3`** (`75dec01`); the whole session's arc
|
||||
(web parity + review fixes) is pushed. Remote: `origin → git@gitea.phasefinal.com:vh/ratatoskr.git`.
|
||||
|
||||
## Recent decisions
|
||||
|
||||
@@ -155,6 +169,12 @@ decision. Captures rationale that won't be obvious from code alone.
|
||||
|
||||
- `[2026-07-01]` **Tier-2 SHIPPED (`v0.19.1`) — transient-characters CRUD + persona-state write; the v1 coverage-audit CONVERGES (zero in-scope gaps).** 5 wrappers in sessions.py: `list_character_models`/`create_character`/`get_character_state`/`delete_character` (#161, `character.read`/`.write` scopes) + `set_persona_state` (`POST /sessions/{id}/persona_state` — **FREEFORM body: unpinned in the frozen OpenAPI 2.2.0 + absent from the prose spec**, so the caller supplies the snapshot shape). Two one-shot CLI probes (mirror `--whoami`): `--characters` (models→create→get-state→delete lifecycle report) + `--set-persona-pad "p,a,d"` (requires `--session`; POSTs `{pad:[…]}`). New `ParsedArgs.characters`/`set_persona_pad` + probe-mode mutual-exclusion validation + `_probe_client` helper. Contract #2 amended (5 FNs, validated OK) + TDD (7 wrapper respx + 5 cli tests). Suite **573 green**; touched code ruff-clean. NOT live-proven (character scopes + the persona-write body shape unverified — the probes degrade gracefully on 403/422). **THE v1 COVERAGE-AUDIT HAS CONVERGED: REST 17/40 ✅ with ZERO in-scope gaps** (23 REST path-groups excluded-by-design + rationale), SSE 11/11, Bifrost provider planes 8/8. Scope-A "done" (every frozen I/O point classified, zero unaccounted) is **MET** — ratatoskr cuts v1 when Worldtree tags 1.0. Only not-consumed in-scope sub-method: `GET /agents/{id}` (consumer-agent lookup, manual-curl-only, on an already-✅ path group). Patch bump (Tier-2 tail; `v0.19.0` already published the core-complete milestone — a 2nd minor would be cadence-too-fast).
|
||||
|
||||
- `[2026-07-01]` **env.sh now PERSISTS the web Bifrost-bind vars (gitignored, local-only).** `ratatoskr-web`'s in-browser bind needs three server-held values; env.sh sources `provider.env` for the Heimdall key and exports `RATATOSKR_BIFROST_CONSUMER_KEY` + `RATATOSKR_PROVIDER_VISIBLE_HOST=10.100.10.50` + `RATATOSKR_AFFECT_READ_URL=:8392`. **The HS256 byte-match trap (re-hit + documented):** the bind's consumer key must equal the key the `:8392` combined provider validates against = `RATATOSKR_HEIMDALL_KEY` (provider.env, fp `45a0…`), NOT `WORLDTREE_API_KEY` (env.sh, fp `7c2f…`) — both are the SAME `ratatoskr` identity but DIFFERENT 40-char strings; signing with the wrong one → `bifrost.auth_rejected`. Single-sourced (env.sh sources provider.env) to avoid a rotation footgun; guarded with a stderr warning if provider.env is missing. [auto-memory: HS256-key-is-the-consumer-Heimdall-key-string]
|
||||
- `[2026-07-01]` **Tier-3 stores RESET (operator-directed).** `memory.db` (29 chunks + vectors + idempotency) + `affect.db` (5 PAD snapshots + idempotency) wiped to zero via a live `DELETE`+`wal_checkpoint` through the shared WAL (no provider restart — the 3 long-running providers see empty on next dispatch); consistent online-backup at `/tmp/ratatoskr-tier3-reset-<ts>/`. **Boundary for a COMPLETE Sindra wipe (mapped):** our stores = mine (done); the agent DEFINITION `ratatoskr:sindra` + its sessions = mine via the owner key (DELETE, no coordination); Worldtree's internal promotion/dedup shadow = needs worldtree-dev (no public reset API, survives our wipe → for a clean promotion smoke use a BRAND-NEW agent+end_user).
|
||||
- `[2026-07-01]` **Embedding-latency loop RESOLVED — it was WORLDTREE's, not ratatoskr (the consumer/provider thesis paid off again).** Vuong flagged dozens of embed queries/Tier-3 turn; worldtree-dev's first-pass blamed our memory_context chunk-batching. Traced CODE-SIDE that ratatoskr embeds ZERO times (provider `upsert_many` stores the given embedding, `search` takes a given vector, the conversation consumer POSTs only `{content}`, `/embed` is coverage-map-excluded — pure Bifrost/ADR-0009 path, WT does all embedding). worldtree-dev retracted + fixed on THEIR side (`v1.0.0b4`): a persona-recitation memory-gate re-embedding the stable character card sentence-by-sentence every turn (~95% of gateway traffic) → content-hash cache; re-embed ratio 15x→1.01x. **Lesson: verify your own code before accepting a peer's "it's your side" — the debug tool proving its own side clean is the whole point.**
|
||||
- `[2026-07-01]` **Web debug-surface parity SHIPPED (`v0.19.2`, `a0a9d5f`) — direct in-session TDD.** 3 proxy routes (tools/bifrost/admin-events) + admin-key wiring (entrypoint→create_app→app.state) + AdminEvents SSE proxy re-emitting under a FIXED `admin_event` name (one browser listener, no per-type drops) + session-filter `_admin_event_matches_web` (mirrors TUI §6). Frontend: 2 tabs (bifrost ⌃5, admin ⌃6) + tools-inventory folded into the tools pane. 9 respx tests (admin-bearer override, filter unit, SSE stream-filter); live-proven against sindra (bifrost connected, both caps). Contract-skip invoked (reuses already-contracted client wrappers); contract authored post-hoc as the trail (`docs/contracts/web_debug_surface.contract.md`).
|
||||
- `[2026-07-01]` **heid-code-review (`v0.19.3`, `75dec01`) — panel caught 2 real client-side SSE-lifecycle bugs TDD missed.** Contract-anchored (authored the web contract to enable it — no contract → no drift axis). Gróa/Hulda/Regin (artifact-only, Gróa under Landlock jail): ZERO functional server-side drift + INV-004 clean; 2 genuine drifts on the un-unit-tested SPA — (1) turn `es.onerror` didn't `hideThinkingNote()` (reasoning line + setInterval leak on a raw drop), (2) `openAdminEvents` never closed the EventSource on error → native auto-reconnect RETRY LOOP (fixed: close on `stream_error` + permanent `onerror`/CLOSED; transient CONNECTING still reconnects). + 2 test-gaps fixed (route-registration + admin stream_error). 1 precision → contract-clarified (tools-inventory names-only by design). **Re-confirms: the JS render/lifecycle paths are the review's highest-value target — unit tests don't reach them (same lesson as #18 D2).**
|
||||
|
||||
_41 older entries (2026-05-* — the original debug-TUI/web build era) archived to archival-memory.md._
|
||||
|
||||
_For per-issue TDD implementation notes, Volva findings, and contract amendments, see the git log — every per-issue commit carries a structured message capturing the trail._
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "ratatoskr"
|
||||
version = "0.19.3"
|
||||
version = "0.19.5"
|
||||
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regenerate src/ratatoskr/web/static/persona_render_canon.json from the vendored
|
||||
Worldtree d2 render canons (docs/vendor/worldtree-persona-canon/).
|
||||
|
||||
The web persona pane renders the CANONICAL affect->NL (mood word + relationship
|
||||
directive) BYTE-EXACT to what Worldtree injects into the agent's context. That render
|
||||
needs the relation canon parsed into per-band phrase maps; this script reparses the
|
||||
vendored raw canons into the flat form the browser JS consumes.
|
||||
|
||||
Uses Worldtree's OWN loader (core.persona.stance_render.load_canon) as the authoritative
|
||||
parser, so the flat form can never drift from Worldtree's parsing semantics. Requires
|
||||
Worldtree's venv (pydantic etc.).
|
||||
|
||||
Run when scripts/canonical_drift.py flags a canon bump:
|
||||
PYTHONPATH=~/development/Worldtree ~/development/Worldtree/.venv/bin/python \
|
||||
scripts/build_persona_canon.py
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from core.persona.stance_render import load_canon # Worldtree (authoritative parser)
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
VENDOR = ROOT / "docs" / "vendor" / "worldtree-persona-canon"
|
||||
OUT = ROOT / "src" / "ratatoskr" / "web" / "static" / "persona_render_canon.json"
|
||||
|
||||
canon = load_canon(str(VENDOR / "d2-render-canon-v1.json"))
|
||||
mood = json.loads((VENDOR / "d2-mood-render-canon-v1.json").read_text())
|
||||
out = {
|
||||
"_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 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"],
|
||||
"relation": {
|
||||
"trust_cuts": [list(c) for c in canon.trust_cuts],
|
||||
"warmth_cuts": [list(c) for c in canon.warmth_cuts],
|
||||
"agency_cuts": [list(c) for c in canon.agency_cuts],
|
||||
"warmth_phrase": canon.warmth_phrase, "warmth_beh": canon.warmth_beh,
|
||||
"agency_phrase": canon.agency_phrase, "agency_beh": canon.agency_beh,
|
||||
"history": canon.history,
|
||||
"prefix": "Use this graded relationship state: toward target, warmth is ",
|
||||
"tbeh": {"low_trust": "verify important claims before relying on them",
|
||||
"cold_warmth": "protect boundaries while staying useful",
|
||||
"default": "work from ordinary good faith"},
|
||||
"cold_warmth_bands": ["distant", "cold", "hostile"], "high_conf_floor": 0.55,
|
||||
},
|
||||
}
|
||||
OUT.write_text(json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {OUT.relative_to(ROOT)}")
|
||||
@@ -361,7 +361,35 @@ body {
|
||||
/* persona pane structured render */
|
||||
#pane-persona .pk { color: var(--fg-dim); }
|
||||
#pane-persona .pv { color: var(--blue); }
|
||||
#pane-persona .ph { color: var(--cyan); letter-spacing: 0.1em; text-transform: uppercase; font-size: 10px; }
|
||||
#pane-persona .ph { color: var(--cyan); letter-spacing: 0.1em; text-transform: uppercase; font-size: 10px; margin-top: 4px; }
|
||||
|
||||
/* affect metric rows: label · value · Δ · sparkline · n · descriptor */
|
||||
#pane-persona .mono-note { color: var(--fg-faint); font-size: 10px; margin: 2px 0 8px; }
|
||||
#pane-persona .mrow {
|
||||
display: flex; gap: 9px; align-items: baseline; padding: 1px 0;
|
||||
font-size: 12px; white-space: nowrap;
|
||||
}
|
||||
#pane-persona .mrow .mk { color: var(--fg-dim); min-width: 118px; }
|
||||
#pane-persona .mrow .mv { color: var(--blue); min-width: 46px; text-align: right; font-variant-numeric: tabular-nums; }
|
||||
#pane-persona .mrow .md { min-width: 58px; font-size: 11px; color: var(--fg-faint); }
|
||||
#pane-persona .mrow .md.up { color: var(--green); }
|
||||
#pane-persona .mrow .md.dn { color: var(--red); }
|
||||
#pane-persona .mrow .msp { color: var(--cyan); letter-spacing: 1px; min-width: 28px; }
|
||||
#pane-persona .mrow .mn { color: var(--fg-faint); font-size: 10px; min-width: 34px; }
|
||||
#pane-persona .mrow .mdesc { color: var(--fg-faint); font-style: italic; font-size: 10px; }
|
||||
|
||||
/* canonical NL (the literal text Worldtree injects into the agent's context) */
|
||||
#pane-persona .nl-canon {
|
||||
color: var(--blue); font-size: 11px; line-height: 1.5;
|
||||
margin: 3px 0 8px; padding-left: 10px; border-left: 2px solid var(--line-2);
|
||||
white-space: normal; word-break: break-word;
|
||||
}
|
||||
#pane-persona .nl-canon b { color: var(--cyan); font-weight: 600; }
|
||||
#pane-persona .nl-directive { color: var(--fg-2); font-style: italic; }
|
||||
#pane-persona .nl-canon .dh {
|
||||
font-style: normal; color: var(--fg-faint); font-size: 10px;
|
||||
letter-spacing: 0.06em; text-transform: uppercase; display: block; margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* thinking-pane per-turn dividers */
|
||||
.pane-turn {
|
||||
@@ -807,25 +835,169 @@ async function loadPersona(agentId) {
|
||||
// shape only — pad + per-entity valence + emitted_at; NO fabricated Tier-1 persona
|
||||
// fields (dominant_emotion / mood_drift), which Tier-3 structurally lacks (INV-001).
|
||||
// Labelled "affect", not "persona" (INV-005).
|
||||
// ---- affect trend accumulation (client-side, session-lived) ----
|
||||
// The affect pane refreshes on session-open + after each turn (the post-turn PAD poll
|
||||
// fires ~4x/turn — deduped here by emitted_at so a turn contributes ONE sample). Each
|
||||
// tracked value keeps a rolling, capped history so the pane can show a Δ + sparkline.
|
||||
const AFFECT_HIST = { at: [], pad: {}, rel: {} }; // pad[axis]=[]; rel[target][metric]=[]
|
||||
const HIST_CAP = 24;
|
||||
const _relVal = (x) => (x && typeof x === "object" && "value" in x) ? x.value : x;
|
||||
const _relN = (x) => (x && typeof x === "object" && "evidence_count" in x) ? x.evidence_count : undefined;
|
||||
function pushAffectHistory(snap) {
|
||||
const at = snap.emitted_at || "";
|
||||
if (at && AFFECT_HIST.at[AFFECT_HIST.at.length - 1] === at) return; // same snapshot — skip
|
||||
AFFECT_HIST.at.push(at);
|
||||
if (AFFECT_HIST.at.length > HIST_CAP) AFFECT_HIST.at.shift();
|
||||
const push = (bucket, key, v) => {
|
||||
if (typeof v !== "number") return;
|
||||
(bucket[key] = bucket[key] || []).push(v);
|
||||
if (bucket[key].length > HIST_CAP) bucket[key].shift();
|
||||
};
|
||||
const pad = snap.pad || {};
|
||||
for (const ax of ["pleasure", "arousal", "dominance"]) push(AFFECT_HIST.pad, ax, pad[ax]);
|
||||
for (const rel of (snap.relations || snap.valence || [])) {
|
||||
const tgt = rel.target_entity || rel.entity_id || "?";
|
||||
const b = (AFFECT_HIST.rel[tgt] = AFFECT_HIST.rel[tgt] || {});
|
||||
push(b, "trust_ability", _relVal(rel.trust_ability));
|
||||
push(b, "trust_benevolence", _relVal(rel.trust_benevolence));
|
||||
push(b, "trust_integrity", _relVal(rel.trust_integrity));
|
||||
push(b, "warmth", _relVal(rel.warmth ?? rel.familiarity));
|
||||
}
|
||||
}
|
||||
// unicode sparkline auto-scaled to the value's own observed range; flat when stable
|
||||
// (don't amplify sub-0.01 noise into a fake trend).
|
||||
const _SPARK = "▁▂▃▄▅▆▇█";
|
||||
function sparkline(vals) {
|
||||
if (!vals || vals.length < 2) return (vals && vals.length) ? "·" : "";
|
||||
const lo = Math.min(...vals), hi = Math.max(...vals);
|
||||
if (hi - lo < 0.01) return "▄".repeat(vals.length);
|
||||
const span = hi - lo;
|
||||
return vals.map((v) => _SPARK[Math.min(7, Math.floor(((v - lo) / span) * 7.999))]).join("");
|
||||
}
|
||||
function trendDelta(vals) {
|
||||
if (!vals || vals.length < 2) return "";
|
||||
const d = vals[vals.length - 1] - vals[vals.length - 2];
|
||||
if (Math.abs(d) < 0.0005) return "→";
|
||||
return (d > 0 ? "▲+" : "▼") + d.toFixed(3);
|
||||
}
|
||||
|
||||
// ---- canonical affect→NL (vendored from Worldtree's d2 render canons) ----
|
||||
// Deterministic, NO LLM — mirrors Worldtree core/persona describe_pad +
|
||||
// render_d2_canonical BYTE-EXACT (verified). Shows the LITERAL mood word +
|
||||
// relationship directive Worldtree injects into the agent's own context, so the
|
||||
// pane reads exactly what the agent was told about its state. Canon loaded from
|
||||
// /static/persona_render_canon.json (regen: scripts/build_persona_canon.py).
|
||||
let PERSONA_CANON = null;
|
||||
const _relConf = (x) => (x && typeof x === "object" && "confidence" in x) ? x.confidence : 0;
|
||||
async function loadPersonaCanon() {
|
||||
try { PERSONA_CANON = await (await fetch("/static/persona_render_canon.json")).json(); }
|
||||
catch (_) { /* canon absent → the canonical lines simply omit (fail-open) */ }
|
||||
}
|
||||
function _parseInterval(expr) {
|
||||
expr = expr.trim();
|
||||
if (expr.startsWith("<=")) return [null, false, parseFloat(expr.slice(2)), true];
|
||||
if (expr.startsWith("<")) return [null, false, parseFloat(expr.slice(1)), false];
|
||||
if (expr.startsWith(">=")) return [parseFloat(expr.slice(2)), true, null, false];
|
||||
if (expr.startsWith(">")) return [parseFloat(expr.slice(1)), false, null, false];
|
||||
const loI = expr[0] === "[", hiI = expr[expr.length - 1] === "]";
|
||||
const [a, b] = expr.slice(1, -1).split(",").map((s) => parseFloat(s.trim()));
|
||||
return [a, loI, b, hiI];
|
||||
}
|
||||
function _bandLabel(cuts, v) {
|
||||
if (typeof v !== "number") return null;
|
||||
for (const [expr, label] of cuts) {
|
||||
const [lo, loI, hi, hiI] = _parseInterval(expr);
|
||||
let ok = true;
|
||||
if (lo !== null) ok = ok && (loI ? v >= lo : v > lo);
|
||||
if (hi !== null) ok = ok && (hiI ? v <= hi : v < hi);
|
||||
if (ok) return label;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function canonMood(pad) { // mirror describe_pad(p,a,d): valence×arousal grid + ±0.3 bands
|
||||
if (!PERSONA_CANON || !pad) return null;
|
||||
const p = pad.pleasure, a = pad.arousal, d = pad.dominance;
|
||||
if ([p, a, d].some((x) => typeof x !== "number")) return null;
|
||||
const g = PERSONA_CANON.mood_grid;
|
||||
const valence = p > 0.3 ? "positive" : p < -0.3 ? "negative" : "neutral";
|
||||
const band = a > 0.3 ? "high_a" : a < -0.3 ? "low_a" : "mid_a";
|
||||
const control = d > 0.3 ? "confident" : d < -0.3 ? "uncertain" : null;
|
||||
return control ? `${g[valence][band]}, ${control}` : g[valence][band];
|
||||
}
|
||||
function canonDirective(rel) { // mirror render_d2_canonical(edge, canon) byte-exact
|
||||
if (!PERSONA_CANON) return null;
|
||||
const R = PERSONA_CANON.relation;
|
||||
const wb = _bandLabel(R.warmth_cuts, _relVal(rel.warmth));
|
||||
const ab = _bandLabel(R.agency_cuts, _relVal(rel.agency));
|
||||
const ta = _bandLabel(R.trust_cuts, _relVal(rel.trust_ability));
|
||||
const ti = _bandLabel(R.trust_cuts, _relVal(rel.trust_integrity));
|
||||
const tb = _bandLabel(R.trust_cuts, _relVal(rel.trust_benevolence));
|
||||
if ([wb, ab, ta, ti, tb].some((x) => x == null)) return null;
|
||||
const cl = _relConf(rel.warmth) >= R.high_conf_floor ? "high" : "low";
|
||||
const tbeh = [ta, ti, tb].includes("limited") ? R.tbeh.low_trust
|
||||
: R.cold_warmth_bands.includes(wb) ? R.tbeh.cold_warmth : R.tbeh.default;
|
||||
return R.prefix + R.warmth_phrase[wb]
|
||||
+ `; agency is ${R.agency_phrase[ab]}; ability trust is ${ta}; integrity trust is ${ti}; `
|
||||
+ `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.`;
|
||||
}
|
||||
|
||||
// Issue #18 D2 (relation_edge/1 rework): Worldtree's affect snapshot now carries
|
||||
// `relations[]` (target + trust_ability/benevolence/integrity + warmth + agency +
|
||||
// relation_context, each {value,confidence,evidence_count}) — NOT the old flat
|
||||
// `valence[]`. Render mood (PAD) + the durable per-entity relational model, each with
|
||||
// a Δ + sparkline from AFFECT_HIST. Falls back to `valence` for an older emitter.
|
||||
// INV-001: no fabricated Tier-1 fields. INV-004: every dynamic value escaped (head()
|
||||
// escapes its whole argument; metric() escapes each cell).
|
||||
function renderAffectPane(snap) {
|
||||
const row = (k, v) => `<div><span class="pk">${esc(k)}</span> <span class="pv">${esc(v)}</span></div>`;
|
||||
const head = (t) => `<div class="ph">${esc(t)}</div>`;
|
||||
const all = snap.valence || [];
|
||||
const shown = all.slice(0, 8); // bounded render — valence[] is unbounded in principle
|
||||
const valRows = shown.map((v) =>
|
||||
row(v.entity_id || "?",
|
||||
`familiarity ${JSON.stringify(v.familiarity)} · regard ${JSON.stringify(v.regard)}`
|
||||
+ ` · n=${JSON.stringify(v.interaction_count)}`)
|
||||
).join("");
|
||||
$("pane-persona").innerHTML =
|
||||
head("affect snapshot · " + (snap.agent_id || "?")) +
|
||||
`<div> </div>` + head("pad") +
|
||||
row("pleasure", JSON.stringify(snap.pad?.pleasure)) +
|
||||
row("arousal", JSON.stringify(snap.pad?.arousal)) +
|
||||
row("dominance", JSON.stringify(snap.pad?.dominance)) +
|
||||
`<div> </div>` + head("valence (" + all.length + ")") +
|
||||
(valRows || `<div class="empty">none</div>`) +
|
||||
`<div> </div>` + row("emitted_at", snap.emitted_at || "?");
|
||||
const num = (v) => (typeof v === "number") ? ((v >= 0 ? "+" : "") + v.toFixed(3)) : "—";
|
||||
const metric = (label, val, hist, desc, n) => {
|
||||
const d = trendDelta(hist);
|
||||
const dcls = d.startsWith("▲") ? "up" : d.startsWith("▼") ? "dn" : "";
|
||||
const nlab = (n !== undefined && n !== null) ? "n=" + esc(n) : "";
|
||||
return `<div class="mrow"><span class="mk">${esc(label)}</span>`
|
||||
+ `<span class="mv">${esc(num(val))}</span>`
|
||||
+ `<span class="md ${dcls}">${esc(d)}</span>`
|
||||
+ `<span class="msp">${esc(sparkline(hist))}</span>`
|
||||
+ `<span class="mn">${nlab}</span>`
|
||||
+ `<span class="mdesc">${esc(desc)}</span></div>`;
|
||||
};
|
||||
const pad = snap.pad || {}, H = AFFECT_HIST;
|
||||
let html = head("affect · " + (snap.agent_id || "?"))
|
||||
+ `<div class="mono-note">emitted ${esc((snap.emitted_at || "?").slice(11, 19))} · `
|
||||
+ `${H.at.length} sample${H.at.length === 1 ? "" : "s"} this session</div>`
|
||||
+ head("mood · PAD (transient, −1‥+1)")
|
||||
+ (canonMood(pad) ? `<div class="nl-canon">Worldtree tells the agent it feels: <b>${esc(canonMood(pad))}</b></div>` : "")
|
||||
+ metric("pleasure", pad.pleasure, H.pad.pleasure, "feeling")
|
||||
+ metric("arousal", pad.arousal, H.pad.arousal, "activation")
|
||||
+ metric("dominance", pad.dominance, H.pad.dominance, "control");
|
||||
const rels = snap.relations || snap.valence || [];
|
||||
if (!rels.length) {
|
||||
html += head("relations") + `<div class="empty">no relations tracked yet — take a turn</div>`;
|
||||
}
|
||||
for (const rel of rels.slice(0, 8)) {
|
||||
const tgt = rel.target_entity || rel.entity_id || "?";
|
||||
const ctx = rel.relation_context ? " · stage: " + rel.relation_context : "";
|
||||
const b = H.rel[tgt] || {};
|
||||
html += head("relation → " + tgt + ctx)
|
||||
+ metric("trust·ability", _relVal(rel.trust_ability), b.trust_ability, "is-competent", _relN(rel.trust_ability))
|
||||
+ metric("trust·benevolence", _relVal(rel.trust_benevolence), b.trust_benevolence, "means-well", _relN(rel.trust_benevolence))
|
||||
+ metric("trust·integrity", _relVal(rel.trust_integrity), b.trust_integrity, "is-honest", _relN(rel.trust_integrity))
|
||||
+ metric("warmth", _relVal(rel.warmth ?? rel.familiarity), b.warmth, "affection", _relN(rel.warmth));
|
||||
const ag = _relVal(rel.agency), agn = _relN(rel.agency);
|
||||
if (typeof ag === "number" && agn) html += metric("agency", ag, null, "autonomy", agn);
|
||||
if (rel.obligation_balance !== null && rel.obligation_balance !== undefined) {
|
||||
html += `<div class="mrow"><span class="mk">obligation</span>`
|
||||
+ `<span class="mv">${esc(JSON.stringify(rel.obligation_balance))}</span></div>`;
|
||||
}
|
||||
const dir = canonDirective(rel);
|
||||
if (dir) {
|
||||
html += `<div class="nl-canon nl-directive"><span class="dh">context directive `
|
||||
+ `(what the agent is told about ${esc(tgt)}):</span> ${esc(dir)}</div>`;
|
||||
}
|
||||
}
|
||||
$("pane-persona").innerHTML = html;
|
||||
}
|
||||
|
||||
async function loadAffect(agentId) {
|
||||
@@ -833,6 +1005,7 @@ async function loadAffect(agentId) {
|
||||
const r = await fetch("/api/affect/" + encodeURIComponent(agentId));
|
||||
if (r.status === 200) {
|
||||
const snap = await r.json();
|
||||
pushAffectHistory(snap); // accumulate the per-value trend BEFORE rendering
|
||||
renderAffectPane(snap);
|
||||
setPersonaStrip(snap); // pad bars are the live signal
|
||||
state.lastAffectAt = snap.emitted_at || state.lastAffectAt; // post-turn poll stop-signal
|
||||
@@ -1257,6 +1430,7 @@ $("prompt-input").addEventListener("keydown", (e) => {
|
||||
|
||||
loadAgents();
|
||||
loadVersion();
|
||||
loadPersonaCanon();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"_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",
|
||||
"mood_grid": {
|
||||
"positive": {
|
||||
"high_a": "positive and energized",
|
||||
"mid_a": "positive",
|
||||
"low_a": "positive and calm"
|
||||
},
|
||||
"neutral": {
|
||||
"high_a": "alert",
|
||||
"mid_a": "neutral",
|
||||
"low_a": "quiet"
|
||||
},
|
||||
"negative": {
|
||||
"high_a": "negative and agitated",
|
||||
"mid_a": "negative",
|
||||
"low_a": "negative and subdued"
|
||||
}
|
||||
},
|
||||
"relation": {
|
||||
"trust_cuts": [
|
||||
[
|
||||
"< 0.4",
|
||||
"limited"
|
||||
],
|
||||
[
|
||||
"[0.4, 0.6)",
|
||||
"developing"
|
||||
],
|
||||
[
|
||||
"[0.6, 0.8)",
|
||||
"steady"
|
||||
],
|
||||
[
|
||||
">= 0.8",
|
||||
"strong"
|
||||
]
|
||||
],
|
||||
"warmth_cuts": [
|
||||
[
|
||||
"<= -0.8",
|
||||
"hostile"
|
||||
],
|
||||
[
|
||||
"(-0.8, -0.6]",
|
||||
"cold"
|
||||
],
|
||||
[
|
||||
"(-0.6, -0.4]",
|
||||
"distant"
|
||||
],
|
||||
[
|
||||
"(-0.4, -0.2)",
|
||||
"guarded"
|
||||
],
|
||||
[
|
||||
"[-0.2, 0.2)",
|
||||
"neutral"
|
||||
],
|
||||
[
|
||||
"[0.2, 0.4)",
|
||||
"reserved"
|
||||
],
|
||||
[
|
||||
"[0.4, 0.6)",
|
||||
"measured"
|
||||
],
|
||||
[
|
||||
"[0.6, 0.8)",
|
||||
"clear"
|
||||
],
|
||||
[
|
||||
">= 0.8",
|
||||
"deep"
|
||||
]
|
||||
],
|
||||
"agency_cuts": [
|
||||
[
|
||||
"<= -0.8",
|
||||
"submissive"
|
||||
],
|
||||
[
|
||||
"(-0.8, -0.6]",
|
||||
"deferential"
|
||||
],
|
||||
[
|
||||
"(-0.6, -0.4]",
|
||||
"yielding"
|
||||
],
|
||||
[
|
||||
"(-0.4, -0.2)",
|
||||
"modest"
|
||||
],
|
||||
[
|
||||
"[-0.2, 0.2)",
|
||||
"neutral"
|
||||
],
|
||||
[
|
||||
"[0.2, 0.4)",
|
||||
"light"
|
||||
],
|
||||
[
|
||||
"[0.4, 0.6)",
|
||||
"balanced"
|
||||
],
|
||||
[
|
||||
"[0.6, 0.8)",
|
||||
"substantial"
|
||||
],
|
||||
[
|
||||
">= 0.8",
|
||||
"commanding"
|
||||
]
|
||||
],
|
||||
"warmth_phrase": {
|
||||
"hostile": "strongly hostile regard",
|
||||
"cold": "clearly cold regard",
|
||||
"distant": "distant negative regard",
|
||||
"guarded": "slightly guarded regard",
|
||||
"neutral": "neutral warmth",
|
||||
"reserved": "slightly reserved warmth",
|
||||
"measured": "moderate measured warmth",
|
||||
"clear": "clear warm regard",
|
||||
"deep": "deep warm bond"
|
||||
},
|
||||
"warmth_beh": {
|
||||
"hostile": "keep a firm emotional boundary",
|
||||
"cold": "keep a firm emotional boundary",
|
||||
"distant": "keep guarded distance",
|
||||
"guarded": "keep guarded distance",
|
||||
"neutral": "keep the tone even",
|
||||
"reserved": "keep cordial distance",
|
||||
"measured": "keep cordial distance",
|
||||
"clear": "speak with direct warmth",
|
||||
"deep": "speak with direct warmth"
|
||||
},
|
||||
"agency_phrase": {
|
||||
"submissive": "strongly submissive standing",
|
||||
"deferential": "clearly deferential standing",
|
||||
"yielding": "yielding standing",
|
||||
"modest": "slightly modest standing",
|
||||
"neutral": "neutral standing",
|
||||
"light": "lightly self-assertive standing",
|
||||
"balanced": "self-assured standing",
|
||||
"substantial": "strongly assertive standing",
|
||||
"commanding": "commanding standing"
|
||||
},
|
||||
"agency_beh": {
|
||||
"submissive": "avoid over-yielding while preserving basic respect",
|
||||
"deferential": "avoid over-yielding while preserving basic respect",
|
||||
"yielding": "keep self-advocacy light and deferential",
|
||||
"modest": "keep self-advocacy light and deferential",
|
||||
"neutral": "avoid unnecessary deference",
|
||||
"light": "avoid unnecessary deference",
|
||||
"balanced": "balance deference with independent judgment",
|
||||
"substantial": "treat their position as weighty without yielding judgment",
|
||||
"commanding": "treat their position as weighty without yielding judgment"
|
||||
},
|
||||
"history": {
|
||||
"low": "a broad pattern of prior exchanges",
|
||||
"high": "a broad pattern of prior exchanges"
|
||||
},
|
||||
"prefix": "Use this graded relationship state: toward target, warmth is ",
|
||||
"tbeh": {
|
||||
"low_trust": "verify important claims before relying on them",
|
||||
"cold_warmth": "protect boundaries while staying useful",
|
||||
"default": "work from ordinary good faith"
|
||||
},
|
||||
"cold_warmth_bands": [
|
||||
"distant",
|
||||
"cold",
|
||||
"hostile"
|
||||
],
|
||||
"high_conf_floor": 0.55
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user