5d06a274bf
Pull Worldtree main's role/model-cutover doc correction (worldtree commit b4a278c) into the pinned conversation-api-spec: the Echo ephemeral-template section now documents allowed_roles/default_role, config.role (omitted -> default_role "echo"), the repurposed model_not_allowed (any non-empty config.model hard-rejects), and the new role_required error. Frozen OpenAPI untouched. affect-egress-consumer-reference re-synced in the same pass; re-pin hashes in .corviduo-canonicals.toml. Resolves the role<->model drift ratatoskr-dev raised on althing (thread 01KXT976NN91DRBZBPXNZ2BVZR); worldtree-dev cleared the re-sync. No version bump (vendored-canonical docs sync).
173 lines
11 KiB
Markdown
173 lines
11 KiB
Markdown
# 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-v2.json` = mood/PAD — the renderer
|
||
loads v2; `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 | |
|
||
|
||
> **✓ R32-1B (landed, v1.0.0b29):** The PAD range `[-1.0, 1.0]` relaxes to an **unbounded latent `z`** with a finite wire sanity bound (`~±10`) as of R32 Slice-1B. The JSON shape/fields/types are UNCHANGED — only the declared range/semantics change (the value becomes a latent that renders to a bounded display value). Consumers that merely store-and-return PAD need no change; consumers that validate/clamp PAD to `[-1,1]` must relax that bound. Source of truth: `docs/contracts/persona_envelope.contract.md` rev 1.7 (INV-ENV-16).
|
||
|
||
**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.
|