feat(acp): implement systemPrompt with protocol version gating#981
Merged
Conversation
Collaborator
Author
|
Context: ACP RFD for This PR implements the This serves as a reference implementation we're dogfooding internally. The RFD is pending adoption by the ACP core team — this PR demonstrates the proposal works in practice with additive semantics and no capability negotiation. |
Implement the systemPrompt field proposed in RFD PR #1237 across both buzz-agent and buzz-acp. The agent receives an optional systemPrompt in session/new params and appends it to its effective system prompt (after env-default + hints). The harness passes its combined [Base] + [System] content via this field instead of embedding it in every user message. Agent side (buzz-agent): - SessionNewParams gains optional systemPrompt field (camelCase serde) - session_new appends client prompt to effective_system_prompt with \n\n separator; rejects combined prompts exceeding 512KB - initialize response advertises systemPrompt: true in promptCapabilities Harness side (buzz-acp): - AcpClient.initialize() parses promptCapabilities.systemPrompt from the agent's init response into a supports_system_prompt bool - session_new_full/session_new accept system_prompt: Option<&str>; field is only included in params when agent advertises support - create_session_and_apply_model combines base_prompt + system_prompt and passes it via session/new when supported - format_prompt skips [Base] and [System] sections when system_prompt_via_session is true (content already in system role) - Initial-message path skips prepend_base_prompt when agent supports systemPrompt (base content already passed via session/new) Backward compatible: old harnesses omitting systemPrompt work unchanged; new harness talking to old agent (no capability) falls back to embedding in user messages. Co-authored-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Add three tests covering the session_new_full systemPrompt wire behavior: - Includes systemPrompt in params when agent advertises capability - Omits systemPrompt when agent does not advertise capability - Omits systemPrompt when value is None (even with capability) Uses bash echo-back pattern: the test script echoes the received request in the response so assertions can inspect the exact JSON sent. Co-authored-by: Will Pfleger <wpfleger@block.xyz> Signed-off-by: Will Pfleger <wpfleger@block.xyz>
Strip [Base]/[System] labels from the systemPrompt wire value — the protocol carries raw content, not harness-specific formatting. Add TODO explaining why agent_core stays in user messages (resolved per-channel after session creation). Move MAX_SYSTEM_PROMPT_BYTES to config.rs alongside other limit constants. New tests: 512KB size gate rejection (integration), empty-string systemPrompt deserialization (unit). Co-authored-by: Will Pfleger <wpfleger@squareup.com> Signed-off-by: Will Pfleger <wpfleger@squareup.com>
Add request-capturing fake LLM server variant and two integration tests that prove the full contract end-to-end without any live LLM dependency: - system_prompt_reaches_llm_system_role: sends systemPrompt via session/new, triggers a prompt, inspects the captured LLM request body to verify the canary string appears in the system role message with correct additive ordering (agent default prompt before client prompt). - system_prompt_absent_no_canary: same flow without systemPrompt field, verifies the canary does NOT appear in the system message while the agent's default prompt is still present. Co-authored-by: Will Pfleger <wpfleger@squareup.com> Signed-off-by: Will Pfleger <wpfleger@squareup.com>
Co-authored-by: Will Pfleger <wpfleger@squareup.com> Signed-off-by: Will Pfleger <wpfleger@squareup.com>
…Prompt The RFD explicitly states 'No separate capability flag is needed' and recommends protocol-version-based detection. Remove the supports_system_prompt field and system_prompt_via_session flag that gated sending systemPrompt on a promptCapabilities boolean. The harness now always passes systemPrompt in session/new when it has system content. Agents that support the field use it; others ignore unknown fields per JSON-RPC. The [Base]/[System] user-message injection continues unchanged for backward compatibility. Co-authored-by: Will Pfleger <wpfleger@squareup.com> Signed-off-by: Will Pfleger <wpfleger@squareup.com>
2dcd358 to
44a7fa6
Compare
PR 981 correctly routes base_prompt and system_prompt into the LLM system role via session/new. However the legacy injection path (format_prompt prepending [Base] and [System] to the user message) was not removed, causing all system-prompt content to appear twice — once in the system role and once in the user message. Remove the [Base] and [System] sections from format_prompt(), remove prepend_base_prompt() calls from heartbeat and initial_message paths, and delete the now-unused prepend_base_prompt function and struct fields. The user message now contains only per-turn dynamic content: Agent Memory, Context, Thread Context, and Events. Co-authored-by: Will Pfleger <wpfleger@squareup.com> Signed-off-by: Will Pfleger <wpfleger@squareup.com>
…ault
When the harness provides a systemPrompt via session/new, use it as the
base content and suppress the DEFAULT_SYSTEM_PROMPT entirely. Hints are
appended last as supplementary reference material.
Previous ordering placed the useless default ("You are buzz-agent...")
first, then hints, then the substantive client_prompt last. This buried
the agent's core identity (base_prompt + persona) under noise.
New behavior:
- systemPrompt present: client_prompt + hints (no default)
- systemPrompt absent: DEFAULT_SYSTEM_PROMPT + hints (legacy fallback)
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Implement protocol version-based capability detection so the harness only sends systemPrompt in session/new and suppresses [Base]/[System] from user messages when the agent declares protocol version >= 2. Legacy agents (goose, claude-code, codex) report version 1 and continue receiving base_prompt and system_prompt via [Base]/[System] sections in the user message — no behavior change for them. Changes: - buzz-agent: bump PROTOCOL_VERSION to 2 - buzz-acp: parse protocolVersion from initialize response, store on OwnedAgent, gate systemPrompt delivery and user-message injection - queue.rs: restore [Base]/[System] emission gated on has_system_prompt_support flag (false for legacy agents) Also applies Thufir's review fixes: - Whitespace guard: trim() before is_empty() check on client_prompt - Arc::from(prompt) instead of Arc::from(prompt.as_str()) Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Legacy agents (protocol_version < 2) lost their base_prompt on heartbeat and initial_message code paths after the systemPrompt refactor moved base_prompt delivery to session/new. Those paths bypass format_prompt, so legacy agents received no [Base] section. Additionally, the harness dishonestly sent protocolVersion: 1 in initialize while buzz-agent unconditionally responded with 2. Now the harness sends 2 and the agent responds with min(requested, supported) for honest negotiation. Changes: - lib.rs dispatch_heartbeat: gate base_prompt prepend on protocol_version < 2 - pool.rs initial_message: gate base_prompt prepend on protocol_version < 2 - acp.rs initialize: send protocolVersion 2 (what we actually support) - buzz-agent initialize: respond with min(requested, PROTOCOL_VERSION) - Update tests to reflect honest negotiation semantics Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The [Base] framing was inlined at three dispatch sites (batch flush, heartbeat, initial message) after prepend_base_prompt was deleted, creating drift risk. Re-extract a single base_section helper so the format is defined in exactly one place, and add a direct unit test covering the trim-and-prepend contract that the regression depended on. Also removes a stray blank line in acp.rs::initialize(). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The round-2 bug shipped because legacy agents silently lost [Base] on the heartbeat and initial-message dispatch paths, and nothing tested that branch. Extract the legacy-prepend gate into prepend_base_for_legacy so both sites share one definition, then add regression tests that fail if the protocol-version gate is flipped or the base_prompt arg is dropped. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The version-as-gate behavior (gating [Base] on protocol_version < 2, and the harness pinning protocolVersion: 2) is intentional but temporary. Document the assumption at both sites so a future genuine upstream-v2 agent silently losing [Base] is a known, revisitable tradeoff rather than a surprise. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…session-new * origin/main: fix(huddle): Pocket TTS quality overhaul — reference parity + cross-message pipelining (#997) Add manual ACP session rotation command (#932) fix(desktop): heal stale persona_team_dir paths in release builds (#1003) ci(docker): publish public ghcr.io/block/buzz image (native multi-arch) (#986) fix(buzz-agent): cap tool-result text at 50 KiB with middle elision (#952) feat(huddle): sentence-at-a-time voice-mode guidelines for lower TTS latency (#996) Shard desktop Playwright CI jobs (#992) chore(release): release version 0.3.18 (#995) Video Player Improvements (#993) Improve first-run welcome setup (#970) fix(release): use legacy updater key secret (#991) Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> # Conflicts: # crates/buzz-acp/src/lib.rs # crates/buzz-agent/src/config.rs
…session-new * origin/main: Add relay disconnect UX: friendly errors, reconnect, cached identity (#1004) feat(agents): add active turn indicators to Agents Menu (#1005) ci: add fork guards to docker, release, and auto-tag workflows (#1007) docs(nip-rs): add optional thread read context scheme (#1006)
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Implements the
systemPromptfield from ACP RFD #1237 with protocol version-based capability detection. Agents declaringprotocolVersion: 2receive their system prompt via thesystemPromptfield insession/new— no duplication in user messages. Legacy agents (goose, Claude Code, codex) stay on version 1 and continue receiving[Base]/[System]sections in the user message.How It Works
The harness parses
protocolVersionfrom the agent'sinitializeresponse and gates behavior on it:systemPromptin session/new[Base]/[System]in user messageZero duplication in either path. Zero breaking change for non-buzz-agent harnesses.
Agent side (
buzz-agent)protocolVersion: 2in initialize responseSessionNewParamsgains optionalsystemPromptfield (camelCase wire format)systemPromptis present: uses it as base content, suppressesDEFAULT_SYSTEM_PROMPT, appends hints lastDEFAULT_SYSTEM_PROMPT+ hints (legacy behavior)systemPrompttreated as absentMAX_SYSTEM_PROMPT_BYTES) with JSON-RPC error (-32602)Harness side (
buzz-acp)protocolVersionfrom initialize response, stores onOwnedAgentstructprotocol_version >= 2: deliverssystemPromptin session/new, setshas_system_prompt_supportflagprotocol_version < 2: skipssystemPrompt,format_promptemits[Base]/[System]in user messageFormatPromptArgsprovides a clean interface for the conditional emission logicBackward Compatibility
systemPrompt: agent uses its own default (Optionfield, defaults toNone)systemPromptto an agent that doesn't handle it: ignored per JSON-RPC (unknown fields discarded)protocolVersion: 2would be treated assystemPrompt-capable and silently lose[Base]/[System]in the legacy user-message paths — inherent to the RFD's version-as-gate design, not a bug in this PR.Design Decisions
systemPromptreplaces the default but hints are always appendedunwrap_or(1)default: agents not declaringprotocolVersionget version 1 (legacy behavior) — safe default