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