Files
ratatoskr/docs/vendor/worldtree-persona-canon/affect-egress-consumer-reference.md
T
vh 5d06a274bf chore(canonicals): re-sync conversation-api-spec to v1.1 + affect-egress
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).
2026-07-18 12:01:48 -07:00

11 KiB
Raw Blame History

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 strnull — 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 renderfully 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.