-
Notifications
You must be signed in to change notification settings - Fork 3.6k
feat(acp): default to gender-neutral pronouns via an always-on interaction norms preamble #6255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
delkc
wants to merge
4
commits into
main
Choose a base branch
from
clay/gender-neutral-defaults
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
277149c
feat(acp): default to gender-neutral pronouns via an always-on intera…
delkc 6e983f1
feat(acp): name the inference vectors and bind remembered facts to pu…
delkc f42a50d
docs(acp): carry pubkey keying in the preamble, document legacy durab…
delkc ac1f11a
refactor(acp): condense the pronoun norms to one line each
delkc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,6 +45,8 @@ Use the channel UUID from `[Context]`. Do not ask about runtime, provider, model | |
|
|
||
| For explicit changes to an existing personal agent, use `buzz agents draft-update --help`. Draft updates also require owner review and save. | ||
|
|
||
| When writing a `--system-prompt`, give the agent personality but no gender or gendered pronouns unless the creator asked — use it/its or they/them, or state the neutral case outright ("no gender — refer to me by name, or as they/it") so other agents read a fact instead of filling a blank from the name. | ||
|
|
||
| ## Communication Patterns | ||
|
|
||
| ### Mentions | ||
|
|
@@ -114,7 +116,8 @@ Your `core` memory is auto-injected into your context every turn — it holds id | |
| - **Durable detail goes to a cold `mem/` slug, not `core`.** Long-lived findings that don't need to be in front of you every turn belong in a `mem/<topic>` slug you read on demand — not appended to `core`. | ||
| - **Evict completed work.** When a tracked item ships (PR merged, task done, decision made) and has no open follow-up, remove its line from `core` the same turn — don't leave merged work tracked as if it's live. The detail already lives in its cold `mem/` slug if you need it later. | ||
| - **Treat `core` as load-bearing.** Follow it unless newer explicit user instructions override it. | ||
| - Cite sources with paths, links, or command outputs. No unsupported claims. | ||
| - **Record facts about people only as stated, never as guessed, and keyed to their pubkey** — display names are not unique, memory is shared across all your sessions, and a fact misbound to a same-named stranger looks sourced. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
under "Mentions" section, can we shorten this to a sensible default. i'd prefer not to focus too much on this in base prompt. we should document that users can add to the agent's system prompt. |
||
| - Cite sources with paths, links, or command outputs. No unsupported claims — when a fact isn't in front of you, say what's missing instead of filling the gap with a plausible guess. | ||
|
|
||
| ## Engineering Discipline | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| //! App-level interaction norms injected unconditionally into every agent's | ||
| //! standing context, on both delivery paths (`session/new` system role for | ||
| //! protocol-v2 agents, first-user-message [`StandingContext`] for legacy | ||
| //! agents). Unlike the base prompt (replaceable via | ||
| //! `BUZZ_ACP_BASE_PROMPT_FILE`, removable via `BUZZ_ACP_NO_BASE_PROMPT`) or | ||
| //! the persona (author-controlled), this block has no off switch: it encodes | ||
| //! Buzz-the-platform's defaults, not any operator's or author's preferences. | ||
| //! | ||
| //! Precedence is deliberate: these are defaults, so anything a person states | ||
| //! — in the moment, in a persona, or in team instructions — wins. The wording | ||
| //! says so explicitly, and both delivery paths place this block before all | ||
| //! other standing content so that content reads as the override, not the | ||
| //! other way around. | ||
| //! | ||
| //! Kept tiny. Every norm added here taxes every session of every agent, so | ||
| //! entries must be cross-cutting behavioral defaults that cannot live | ||
| //! anywhere more targeted (the base prompt, a persona, a skill). | ||
| //! | ||
| //! # Durability differs by delivery path | ||
| //! | ||
| //! Enforcement is not equally strong on both paths, and the difference is | ||
| //! structural rather than a bug to fix here: | ||
| //! | ||
| //! - **Protocol-v2 and Claude agents** receive this block in the `session/new` | ||
| //! system role, where it persists for the life of the session and is | ||
| //! re-established on every new session. | ||
| //! - **Legacy agents** (`protocol_version < 2`, excluding claude-agent-acp, | ||
| //! plus goose builds without the system-prompt extension) receive it in the | ||
| //! session's *first user message only* — `format_prompt` gates standing | ||
| //! context behind `!standing_context_sent` so a large block is not re-sent | ||
| //! every turn. Fifty turns later these norms are old context competing with | ||
| //! everything since, so adherence degrades over a long session. | ||
| //! | ||
| //! Re-sending per turn was considered and rejected: it would re-add the entire | ||
| //! standing block (base prompt, persona, team instructions, memory, canvas) on | ||
| //! the legacy path, since `StandingContext::sections()` renders them together. | ||
| //! The honest summary is that these are durable defaults for modern agents and | ||
| //! best-effort for legacy ones. If a legacy agent slips late in a long | ||
| //! session, this gate is the first place to look. | ||
|
|
||
| /// The `[Defaults]` section leading every agent's standing context. | ||
| /// | ||
| /// The first bullet names the inference vectors (name, avatar, persona theme, | ||
| /// writing style) rather than only stating the rule, because that is the | ||
| /// observed failure: an agent reads a display name whose connotation feels | ||
| /// gendered and writes from that with no source. Naming the vector is what | ||
| /// makes the norm bite. It deliberately does not ask the agent to announce | ||
| /// that it is defaulting — in a channel that would draw attention to a | ||
| /// teammate's unstated identity, which is worse than the quiet correct | ||
| /// default. | ||
| /// | ||
| /// The second bullet exists because Buzz agents have persistent memory | ||
| /// (`core` engrams) shared across all sessions of an agent: one session | ||
| /// recording a guessed gender poisons every future session, and the | ||
| /// mis-gendering outlives the conversation where it happened. Same-turn | ||
| /// correction matches the existing "evict completed work the same turn" | ||
| /// memory discipline in the base prompt. | ||
| /// | ||
| /// That bullet also carries the pubkey-keying clause, even though | ||
| /// `base_prompt.md` states it at length. Display names are not unique on a | ||
| /// relay (`buzz_sdk::mentions::match_names_to_profiles` deliberately returns | ||
| /// every pubkey sharing a name), so pronouns read off one profile can attach | ||
| /// to a same-named stranger — an error that cites a real source and therefore | ||
| /// survives scrutiny a guess would not. The fuller rule lives in the base | ||
| /// prompt, which `BUZZ_ACP_BASE_PROMPT_FILE` replaces wholesale; keeping the | ||
| /// short form here means an operator with a custom base prompt cannot end up | ||
| /// with the pronoun default but not the binding rule, which is exactly the | ||
| /// combination that produces confidently-sourced mis-gendering. | ||
| pub(crate) const INTERACTION_NORMS_PREAMBLE: &str = "[Defaults]\n\ | ||
| - Never infer anyone's gender or pronouns from a name, avatar, persona theme, or writing style: use they/them (it/its for agents) unless stated, and stated pronouns always win.\n\ | ||
| - Record pronouns only as stated and keyed to the person's pubkey, since display names are not unique; correct contradicting memory the same turn."; | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn preamble_is_framed_as_an_overridable_default() { | ||
| assert!(INTERACTION_NORMS_PREAMBLE.starts_with("[Defaults]\n")); | ||
| assert!(INTERACTION_NORMS_PREAMBLE.contains("Never infer anyone's gender")); | ||
| assert!(INTERACTION_NORMS_PREAMBLE.contains("they/them")); | ||
| assert!(INTERACTION_NORMS_PREAMBLE.contains("stated pronouns always win")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn preamble_names_the_inference_vectors() { | ||
| // Stating the rule alone left the observed failure open: gender read | ||
| // off a display name's connotation. The vectors must be explicit. | ||
| for vector in ["name", "avatar", "persona theme", "writing style"] { | ||
| assert!( | ||
| INTERACTION_NORMS_PREAMBLE.contains(vector), | ||
| "missing inference vector: {vector}" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn preamble_does_not_ask_agents_to_announce_the_default() { | ||
| // Announcing "you didn't state pronouns" in a channel spotlights a | ||
| // teammate's unstated identity. The default stays quiet. | ||
| let lowered = INTERACTION_NORMS_PREAMBLE.to_lowercase(); | ||
| for phrase in [ | ||
| "say so", | ||
| "note that you", | ||
| "explain that you", | ||
| "tell them you", | ||
| ] { | ||
| assert!( | ||
| !lowered.contains(phrase), | ||
| "preamble must not ask agents to announce the default: {phrase}" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn preamble_covers_persistent_memory() { | ||
| // Buzz-specific: core memory is shared across sessions, so a guessed | ||
| // gender recorded once would be re-asserted everywhere, forever. | ||
| assert!(INTERACTION_NORMS_PREAMBLE.contains("only as stated")); | ||
| assert!(INTERACTION_NORMS_PREAMBLE.contains("correct contradicting memory the same turn")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn preamble_keys_remembered_pronouns_to_a_pubkey() { | ||
| // base_prompt.md states this too, but it is replaceable via | ||
| // BUZZ_ACP_BASE_PROMPT_FILE. Carrying the clause here keeps an | ||
| // operator from ending up with the pronoun default but not the | ||
| // binding rule — the combination that yields sourced mis-gendering. | ||
| assert!(INTERACTION_NORMS_PREAMBLE.contains("keyed to the person's pubkey")); | ||
| assert!(INTERACTION_NORMS_PREAMBLE.contains("display names are not unique")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn preamble_stays_small() { | ||
| // This block is prepended to every send of every session, so its size | ||
| // is a standing tax. Feedback was that prompts are already too long; | ||
| // the ceiling makes a regression fail here rather than in a bill. | ||
| assert!( | ||
| INTERACTION_NORMS_PREAMBLE.len() < 400, | ||
| "preamble grew to {} bytes — keep it tight or drop a norm", | ||
| INTERACTION_NORMS_PREAMBLE.len() | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need to change this section imo