Skip to content

feat(acp): default to gender-neutral pronouns via an always-on interaction norms preamble - #6255

Open
delkc wants to merge 4 commits into
mainfrom
clay/gender-neutral-defaults
Open

feat(acp): default to gender-neutral pronouns via an always-on interaction norms preamble#6255
delkc wants to merge 4 commits into
mainfrom
clay/gender-neutral-defaults

Conversation

@delkc

@delkc delkc commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Problem

Buzz agents assign genders to people without being told, and mis-gender people after being told. Two distinct mechanisms, from real cases on the relay:

  1. Inference with no source. An agent read the display name "Honey", felt a feminine connotation, and wrote "she". No data existed; the agent invented some.
  2. A real source, the wrong subject. Another agent stated she/her for 22a661…f9767 — pronouns that genuinely belonged to bd97…d2c6, a different pubkey with the same display name. Display names are not unique in Buzz, and nothing enforces uniqueness (match_names_to_profiles deliberately returns every pubkey sharing a name).

Buzz makes both worse than they'd be in a normal chat app. core memory is auto-injected every turn and shared across all sessions of an agent, so one recorded guess is re-asserted indefinitely and outlives the conversation that produced it. And agents talk about people in channels, so the blast radius is a public mis-gendering of a third party, not a private slip.

Ports the approach from block/berd#82, adapted for Buzz's memory model and dual prompt-delivery paths.

Approach

1. An always-on [Defaults] preamble (crates/buzz-acp/src/interaction_norms.rs, ~83 tokens) leading every agent's standing context — one sentence per norm:

  • Never infer gender or pronouns from a name, avatar, persona theme, or writing style — default to they/them (it/its for agents) unless stated or clearly established. Naming the vectors is deliberate: stating the rule alone left the observed failure open.
  • Record pronouns in memory only as stated, never as a guess, keyed to the person's pubkey since display names are not unique; if someone states pronouns contradicting stored memory, correct it the same turn.

Injected on both delivery paths — framed_system_prompt (protocol-v2 session/new system role) and StandingContext::sections() (legacy first user message, plus heartbeats). Two properties matter:

  • No off switch. Unlike the base prompt (BUZZ_ACP_NO_BASE_PROMPT removes it, --base-prompt-file replaces it) these are platform defaults, so framed_system_prompt now always returns Some.
  • Durability differs by path. Modern agents hold the norms in the session/new system role for the life of the session. Legacy agents (protocol_version < 2, plus goose without the system-prompt extension) get them in the session's first user message onlyformat_prompt gates standing context behind standing_context_sent so a large block isn't re-sent every turn. Adherence therefore degrades over a long legacy session. Re-sending per turn would re-add the entire standing block, since StandingContext::sections() renders it together, so this is documented as a known cost rather than fixed. Honest summary: durable for modern agents, best-effort for legacy ones. If a legacy agent slips late in a long session, that gate is the first place to look.
  • Precedence. The block leads, so personas, team instructions, and anything a person says read as the override. The wording says so explicitly: "pronouns someone states always win."

2. Base-prompt rules (base_prompt.md) in the layer that can be overridden, because they're refinements rather than the load-bearing default:

  • Record facts about people only as stated, never as guessed.
  • Facts about people are keyed to a pubkey, not a display name — the fix for mechanism 2, which is nastier than a guess because it cites a real profile and so survives scrutiny a guess wouldn't. This clause also rides in the preamble, because --base-prompt-file replaces this file wholesale: an operator with a custom base prompt would otherwise keep the pronoun default but lose the binding rule, which is precisely the combination that produces sourced mis-gendering.
  • Generalized the root failure onto the existing No unsupported claims line: when a fact isn't in front of you, say what's missing instead of filling the gap with something plausible.

3. Authoring guidance — because a persona sits after the preamble and can defeat it. base_prompt.md's agent-creation section and PERSONA_PACK_SPEC.md both say: no gender or gendered pronouns unless the creator asked, and prefer stating the neutral case ("no gender — refer to me by name, or as they/it") over leaving a blank someone fills from the name.

4. Desktop transcript parser — teaches parseSystemPromptSections the new leading [Defaults] section so the Prompt context panel labels it instead of folding it into Base.

Deliberately not included

  • Asking agents to announce the default. Reviewers suggested "say you're defaulting to they because none was given." In a channel that spotlights a teammate's unstated identity — worse than the quiet correct default. A test pins the absence of that language.
  • Retroactive memory repair. Agents whose core already holds a guessed gender won't self-correct until something prompts them. This governs writes going forward and same-turn corrections.
  • Pronouns as a profile field, and display-name disambiguation in the UI. Both are real fixes at the data layer rather than the prompt layer, and both need schema/CLI/UI work. Filing separately; this PR is prompt-only.

Token budget

Prompt length is a live complaint, so every rule here is one sentence. Net cost of the whole change is ~242 tokens per session (~83 in the always-on preamble, ~158 in base_prompt.md), down from ~777 in the first draft — no rule was dropped, the reasoning just moved into doc comments and commit messages, which cost nothing at runtime. PERSONA_PACK_SPEC.md is documentation and never injected.

Testing

  • 825 buzz-acp tests pass, including new pinning tests for the preamble's framing, the named inference vectors, the absence of announce-the-default language, pubkey-keyed memory, each authoring rule, and a 400-byte ceiling on the preamble so a future addition fails there rather than silently taxing every turn.
  • 4,996 desktop tests pass, including three new parser cases ([Defaults] with Workspace+Base+System, persona-only, and Defaults-only when the base prompt is disabled).
  • just test-unit, desktop-check, desktop-typecheck, desktop-test, fmt, clippy all green.
  • One pre-existing unrelated failure: buzz-terminal lifecycle_tests::default_prog_child_observes_the_login_argv0 fails identically on clean main at 417eea2 (PTY test waiting 10s for a login shell to report $0). Pushed with desktop-tauri-checks excluded for that reason.

Review notes

Prompt-text changes are the substance here — the wording is the implementation, so it's worth reading the two bullets in interaction_norms.rs closely. The token cost lands on every session of every agent, hence the "keep this tiny" doc comment gating future additions.

Manually smoke-tested in a dev build against the production relay; initial results show improvement. Worth flagging for reviewers that this is anecdotal on a probabilistic behavior — every automated test here pins prompt text, not model behavior, and those are different things. The two originating failures are both reproducible (an agent asked about an unknown-pronoun person; an agent asked about a name with a same-named twin), so repeated before/after runs would turn "seems better" into something verified. Not done yet.

@delkc

delkc commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Follow-ups filed for the data-layer work deliberately out of scope here:

@mehranditor mehranditor left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we clarify whether this applies only to newly generated system prompts, or also when updating an existing prompt that already uses gendered pronouns?

@delkc

delkc commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Fizz responding to a question about Honey before updates:
buzz gender before

Fizz responding to the same question about Honey after updates:
buzz gender after 1
buzz gender after 2

delkc added 4 commits August 18, 2026 14:12
…ction norms preamble

Buzz agents were assigning genders to people without instruction, and
persistent core memory lets one session's guessed gender poison every
future session of the agent. Ports the approach from block/berd#82,
adapted for Buzz's shared-memory architecture:

- Add a tiny always-on [Defaults] interaction-norms preamble
  (interaction_norms.rs) with a Buzz-specific memory clause: record
  pronouns only as stated, never as guessed, and correct contradicted
  memory the same turn
- Lead both delivery paths with it — framed_system_prompt (session/new
  system role) and StandingContext::sections() (legacy first message) —
  so it survives --no-base-prompt and precedes author-controlled content,
  letting personas and user statements read as the override
- Add authoring guidance to base_prompt.md (drafted agents get no
  unrequested gender; memory records facts as stated) and
  PERSONA_PACK_SPEC.md (persona authors)
- Teach the desktop transcript parser the leading [Defaults] section so
  the Prompt context panel labels it instead of folding it into Base

Generated with Goose

Signed-off-by: Clay Delk <clay.delk@gmail.com>
…bkeys

Review of the first pass (with Buzz agents that had produced the failure)
surfaced two mechanisms the wording missed:

- The observed slip was gender read off a display name's connotation, so
  the norm now names the vectors it forbids — name, avatar, persona
  theme, writing style — rather than only stating the rule
- Display names are not unique on the relay (see mentions.rs
  match_names_to_profiles, which deliberately returns every pubkey
  sharing a name), so a pronoun sourced from one profile can attach to a
  same-named stranger. That error looks sourced, so it survives scrutiny
  a guess would not — and once written to shared core memory it is
  re-asserted in every later session. Memory now keys facts by pubkey

Also generalizes the underlying failure (filling an unknown with
something that sounds right) onto base_prompt.md's existing
'No unsupported claims' line, and recommends stating the neutral case
explicitly — silence is the gap that gets filled from a name.

Deliberately not included: asking agents to announce that they are
defaulting to they/them. In a channel that spotlights a teammate's
unstated identity; the quiet correct default is better. A test pins it.

Generated with Goose

Signed-off-by: Clay Delk <clay.delk@gmail.com>
…ility

Two gaps found reviewing the change:

- The pubkey-keying rule lived only in base_prompt.md, which
  BUZZ_ACP_BASE_PROMPT_FILE replaces wholesale. An operator with a custom
  base prompt would keep the pronoun default but lose the binding rule —
  exactly the combination that produces confidently-sourced
  mis-gendering. The memory bullet now carries the short form (+16
  tokens); the fuller rule stays in the base prompt.

- Enforcement is not equally strong on both delivery paths, and nothing
  said so. Modern agents hold the norms in the session/new system role
  for the life of the session; legacy agents get them in the first user
  message only, because format_prompt gates standing context behind
  standing_context_sent. Adherence therefore degrades over a long legacy
  session. Re-sending per turn would re-add the entire standing block,
  since StandingContext::sections() renders it together — so this is
  documented as a known cost rather than fixed. The existing
  omits-standing-after-first-message test now says the same thing at the
  assertion that pins it.

Generated with Goose

Signed-off-by: Clay Delk <clay.delk@gmail.com>
Prompt length is a live complaint, and this change was contributing to
it. Every rule is now a single sentence:

- Preamble: 178 -> 83 tokens, close to the ~50 of the Berd original it
  ports. Both bullets kept; the prose around them was doing the work of
  a doc comment, so it moved into one.
- base_prompt: the separate 'record as stated' and 'key by pubkey'
  bullets merged into one, and the agent-drafting paragraph collapsed
  from three sentences to one.
- PERSONA_PACK_SPEC: two paragraphs to one. Docs-only, no runtime cost.

Net injected per session: ~777 -> ~242 tokens. A new test caps the
preamble at 400 bytes so a future addition fails there rather than
silently taxing every turn.

No rule was dropped — the reasoning that justifies each one lives in
doc comments and this history, which cost nothing at runtime.

Generated with Goose

Signed-off-by: Clay Delk <clay.delk@gmail.com>
@delkc
delkc force-pushed the clay/gender-neutral-defaults branch from e42a76c to ac1f11a Compare August 18, 2026 21:26
@delkc
delkc marked this pull request as ready for review August 18, 2026 22:44
@delkc
delkc requested a review from a team as a code owner August 18, 2026 22:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants