# 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.