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).
11 KiB
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 latentzwith 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.mdrev 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 richeremotions_activelist 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.emitrelations(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_emotiongives the emotion TYPE, butaffect.emitdoes 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.