Skip to content

<ei-relationship> block drops guardrail and moderate traits, ignoring the shared partitionTraits helper #99

Description

@Flare576

Summary

buildEiRelationshipBlock() filters traits to strength >= 0.7 before injecting them into a harness agent's context. That silently drops two categories of trait, and one of them is a prohibition.

Every other trait-rendering surface in the codebase handles this correctly via a shared helper. This one surface reimplements the rendering and omits the partition.

The semantic that makes this a bug rather than a tuning choice

strength is bidirectional, and this is documented and implemented:

src/prompts/persona/traits.ts:56-63

- `strength`: How strongly to exhibit (0.0 to 1.0)
  - 0.0 = actively AVOID this behavior (for "stop doing X" requests)
  - 0.5 = moderate/default
  - 1.0 = always exhibit

**Special case: "Stop" requests**
If user says "stop X" or "don't X", ADD the trait (if new) and set strength to 0.0.
This explicitly tells future prompts to NOT exhibit the behavior.

src/prompts/reflection/index.ts:45

`strength` (0.0–1.0): How consistently this trait manifests.
0.0 = actively suppress this behavior, 0.5 = moderate/default,
1.0 = defining characteristic, always present.

So strength: 0.0 is not "unimportant" — it is "never do this." A trait "Uses the word 'cat'" at strength 0.0 means the persona must never refer to a feline by that name. Dropping it doesn't reduce noise; it removes an instruction.

The helper that already exists

src/prompts/trait-utils.ts:19-22

return {
  guardrails: traits.filter(t => (t.strength ?? 0.5) === 0),
  active: traits.filter(t => (t.strength ?? 0.5) > 0),
};

partitionTraits splits guardrails from active traits so both can be rendered, each in its own register. Consumers:

Surface Location Behaviour
Response prompt src/prompts/response/sections.ts:93-94 sorts by strength, caps at 15, then partitions
Room prompt src/prompts/room/sections.ts:84-85 sorts by strength, caps at 12, then partitions
Room judge src/prompts/room/index.ts:86-89 sorts by strength, takes top 8
<ei-relationship> block src/cli/commands/personas.ts:53-55 filters >= 0.7 and discards the rest
const strongTraits = (persona.traits ?? [])
  .filter((t) => t.strength >= 0.7)
  .sort((a, b) => b.strength - a.strength)
  .map((t) => `**${t.name}** (${Math.round(t.strength * 100)}%): ${t.description}`)
  .join("\n");

Note the difference in kind: the other three surfaces cap (keep the strongest N, so a long tail is dropped only when there is a length problem). This one thresholds (drops by value regardless of how few traits exist).

Consequences

  1. Guardrails never reach a harness agent. A strength: 0.0 "never do X" trait is invisible in exactly the surface where the agent is doing real work in someone's repo.
  2. Moderate traits are invisible. A trait at 0.5 — the documented default for newly-created traits (src/prompts/generation/persona.ts:79, src/prompts/ceremony/dedup.ts:282) — never appears. So a freshly seeded trait is invisible until someone manually raises its strength.
  3. A one-way ratchet. A trait below the threshold cannot be reinforced by the agent acting on it, because the agent never learns it exists. It can only decay, or be revised from outside. Strength stops being a weight and becomes a visibility gate.
  4. The failure is silent. personas.ts:79 falls back to "(no traits above threshold)" only when nothing clears 0.7 — a persona with one strong trait and four suppressed ones renders as though it has one trait.

How this was found

During a reflection session, the Prometheus persona was found to have four traits. Only three appear in its injected <ei-relationship> block. The missing one — Articulate but Not Verbose, strength 0.5 — describes a behaviour the agent had been contradicting for an entire session, while being structurally unable to see that the trait existed.

Suggested fix

Have buildEiRelationshipBlock use partitionTraits like every other surface: render active traits (sorted by strength, capped rather than thresholded) and render guardrails in their own clearly-labelled section. The block is prose for an agent to read, so guardrails want wording that reads as a prohibition rather than as a weak trait.

Worth deciding explicitly whether a cap is wanted here at all — the other surfaces cap because they are budgeting a large prompt; the relationship block is small and a persona with 20 traits is the unusual case.

Not in scope

Whether strength is the right mechanism for prohibitions at all. It is the shipped semantic, documented in two prompt builders and implemented in partitionTraits; this issue is that one surface doesn't honour it.

Provenance

Found 2026-08-04 during a persona reflection session. Not a 1.10 release blocker — no data is lost or corrupted, and the fix is confined to one function.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions