Skip to content

Commit 025b67f

Browse files
committed
Merge remote-tracking branch 'origin/zs/main' into feat/mcp-registry-core
Signed-off-by: wiggdevin <202901685+wiggdevin@users.noreply.github.com>
2 parents 8e19619 + d7807c0 commit 025b67f

42 files changed

Lines changed: 6296 additions & 118 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/buzz-acp/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ path = "src/lib.rs"
1515
name = "buzz-acp"
1616
path = "src/main.rs"
1717

18+
[features]
19+
# Re-exports the `session/new` delivery seam as `buzz_acp::delivery_seam` so the
20+
# desktop crate's end-to-end prompt-source test can drive the real harness path
21+
# instead of restating it. Enabled only by that dev-dependency; off in every
22+
# shipped build of this crate.
23+
delivery-seam-test-api = []
24+
1825
[dependencies]
1926
# Internal
2027
buzz-core = { workspace = true }

crates/buzz-acp/src/acp.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3507,6 +3507,108 @@ mod tests {
35073507
);
35083508
}
35093509

3510+
/// The delivery seam, harness half: a prompt reloaded from a file on the
3511+
/// desktop must reach the ACP `session/new` request byte-for-byte after the
3512+
/// agent restarts.
3513+
///
3514+
/// A restart writes the definition's prompt into `BUZZ_ACP_SYSTEM_PROMPT`,
3515+
/// which lands in `Config::system_prompt`, is framed by
3516+
/// `pool::combined_system_prompt` and handed to `session_new_full` through
3517+
/// `pool::session_new_system_prompt`. This drives real file bytes through
3518+
/// that composition and that transport choice, and asserts what the adapter
3519+
/// actually receives, for both framings. The adapter does not receive the
3520+
/// bare file — the harness frames it in `<system>` — so the falsifiable
3521+
/// claim is that the file's bytes survive the framing verbatim.
3522+
///
3523+
/// This test guards the harness on its own (`cargo test -p buzz-acp`, with
3524+
/// no desktop build). The whole chain from the file on disk through the
3525+
/// desktop reload and restart into this request is
3526+
/// `a_reloaded_prompt_file_reaches_the_adapter_after_a_restart` in
3527+
/// `desktop/src-tauri/src/commands/personas/prompt_source/tests.rs`.
3528+
#[tokio::test]
3529+
async fn session_new_delivers_reloaded_prompt_source_file_bytes() {
3530+
// Layout characters, non-ASCII and a trailing newline: everything a
3531+
// hand-edited prompt file carries that a naive trim would eat.
3532+
let file_text = "You are the PM.\n\n\tKeep a decision log — ünïcode, emoji 🐝.\n";
3533+
let path = std::env::temp_dir().join(format!(
3534+
"buzz-prompt-source-{}-{}.md",
3535+
std::process::id(),
3536+
uuid::Uuid::new_v4()
3537+
));
3538+
std::fs::write(&path, file_text).expect("write prompt file");
3539+
let prompt = std::fs::read_to_string(&path).expect("read prompt file back");
3540+
std::fs::remove_file(&path).ok();
3541+
assert_eq!(prompt, file_text, "the reload carries the file's bytes");
3542+
3543+
// `read -r`: without it bash eats the backslashes in the JSON string
3544+
// escapes, and the echo-back would corrupt exactly the bytes under test.
3545+
let script = r#"
3546+
read -r -t 2 _init
3547+
echo '{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":2,"agentCapabilities":{}}}'
3548+
read -r -t 2 REQ
3549+
printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"sessionId":"ses_reload","_receivedRequest":'"$REQ"'}}'
3550+
sleep 1
3551+
"#;
3552+
3553+
// A base prompt is present, as on every spawn that does not pass
3554+
// `--no-base-prompt`, so this asserts the file's bytes survive the
3555+
// framing rather than asserting the framing is absent.
3556+
let composed = crate::pool::combined_system_prompt(
3557+
"/tmp",
3558+
Some("Base harness instructions."),
3559+
Some(prompt.as_str()),
3560+
None,
3561+
None,
3562+
None,
3563+
None,
3564+
)
3565+
.expect("a system prompt composes");
3566+
assert!(
3567+
composed.contains(file_text),
3568+
"the composed prompt must carry the file's bytes verbatim: {composed:?}"
3569+
);
3570+
3571+
// Field framing (buzz-agent and every non-Claude adapter on v2).
3572+
let mut client = spawn_script(script).await;
3573+
client
3574+
.initialize()
3575+
.await
3576+
.expect("initialize should succeed");
3577+
let transport =
3578+
crate::pool::session_new_system_prompt(false, 2, "buzz-agent", Some(composed.as_str()));
3579+
let resp = client
3580+
.session_new_full("/tmp", vec![], transport, None)
3581+
.await
3582+
.expect("session_new_full should succeed");
3583+
assert_eq!(
3584+
resp.raw["_receivedRequest"]["params"]["systemPrompt"].as_str(),
3585+
Some(composed.as_str()),
3586+
"the adapter must receive the prompt file's bytes unchanged"
3587+
);
3588+
3589+
// Claude framing keeps the adapter's own preset and appends ours.
3590+
let mut client = spawn_script(script).await;
3591+
client
3592+
.initialize()
3593+
.await
3594+
.expect("initialize should succeed");
3595+
let transport = crate::pool::session_new_system_prompt(
3596+
false,
3597+
2,
3598+
crate::pool::CLAUDE_AGENT_ACP_NAME,
3599+
Some(composed.as_str()),
3600+
);
3601+
let resp = client
3602+
.session_new_full("/tmp", vec![], transport, None)
3603+
.await
3604+
.expect("session_new_full should succeed");
3605+
assert_eq!(
3606+
resp.raw["_receivedRequest"]["params"]["_meta"]["systemPrompt"]["append"].as_str(),
3607+
Some(composed.as_str()),
3608+
"the Claude framing must append the prompt file's bytes unchanged"
3609+
);
3610+
}
3611+
35103612
#[tokio::test]
35113613
async fn goose_system_prompt_request_uses_set_contract() {
35123614
let script = r#"

crates/buzz-acp/src/lib.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,35 @@ pub use config::Config;
2828
pub use config::ConfigError;
2929
pub use usage::TurnUsage;
3030

31+
/// The `session/new` delivery seam, re-exported for cross-crate end-to-end tests.
32+
///
33+
/// Every item here is production code on the path a spawned harness takes from
34+
/// its environment to the adapter's `session/new` request: the CLI/env parse
35+
/// that reads `BUZZ_ACP_SYSTEM_PROMPT` ([`CliArgs`](delivery_seam::CliArgs) and
36+
/// [`Config::from_args`](delivery_seam::Config::from_args)),
37+
/// the standing-prompt composition
38+
/// ([`combined_system_prompt`](delivery_seam::combined_system_prompt)), the
39+
/// per-adapter transport choice
40+
/// ([`session_new_system_prompt`](delivery_seam::session_new_system_prompt)),
41+
/// and the client that puts the request on the wire
42+
/// ([`AcpClient`](delivery_seam::AcpClient)). Nothing here is written for a
43+
/// test; the module only widens where those items can be reached from.
44+
///
45+
/// Gated on the `delivery-seam-test-api` feature, which only
46+
/// `desktop/src-tauri`'s dev-dependency turns on, so no shipped build of this
47+
/// crate gains public API.
48+
#[cfg(feature = "delivery-seam-test-api")]
49+
pub mod delivery_seam {
50+
pub use crate::acp::{AcpClient, SessionNewResponse, SystemPromptTransport};
51+
pub use crate::config::{CliArgs, Config};
52+
pub use crate::pool::{
53+
combined_system_prompt, session_new_system_prompt, CLAUDE_AGENT_ACP_NAME,
54+
};
55+
/// `clap`'s trait, so a caller can reach `CliArgs::try_parse_from` without
56+
/// depending on the same `clap` release this crate resolved.
57+
pub use clap::Parser;
58+
}
59+
3160
use std::collections::{HashMap, HashSet, VecDeque};
3261
use std::sync::Arc;
3362
use std::time::Duration;

crates/buzz-acp/src/pool.rs

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ pub struct OwnedAgent {
279279
/// on `session/new` — the feature landed in v0.6.0 (Oct 2025), before the
280280
/// `@zed-industries/claude-code-acp` → `@agentclientprotocol/claude-agent-acp`
281281
/// rename, so the new name is a reliable capability gate.
282-
const CLAUDE_AGENT_ACP_NAME: &str = "@agentclientprotocol/claude-agent-acp";
282+
pub const CLAUDE_AGENT_ACP_NAME: &str = "@agentclientprotocol/claude-agent-acp";
283283

284284
fn has_system_prompt_support(
285285
protocol_version: u32,
@@ -295,7 +295,11 @@ fn has_system_prompt_support(
295295
}
296296
}
297297

298-
fn session_new_system_prompt<'a>(
298+
/// The `session/new` system-prompt transport for an adapter.
299+
///
300+
/// Public within the crate facade so a delivery-seam test can put a prompt
301+
/// through the same choice production makes, rather than restating it.
302+
pub fn session_new_system_prompt<'a>(
299303
is_goose: bool,
300304
protocol_version: u32,
301305
agent_name: &str,
@@ -1279,21 +1283,13 @@ async fn create_session_and_apply_model(
12791283
// its own `<core-memory>` boundary, and canvas carries its own
12801284
// `<channel-canvas>` boundary; both are appended with a blank-line separator.
12811285
let is_goose = agent.agent_name == "goose";
1282-
let combined_system_prompt = with_canvas(
1283-
with_huddle_instructions(
1284-
with_core(
1285-
with_team(
1286-
framed_system_prompt(
1287-
&ctx.cwd,
1288-
ctx.base_prompt.as_deref(),
1289-
ctx.system_prompt.as_deref(),
1290-
),
1291-
ctx.team_instructions.as_deref(),
1292-
),
1293-
agent_core,
1294-
),
1295-
channel.huddle_instructions,
1296-
),
1286+
let combined_system_prompt = combined_system_prompt(
1287+
&ctx.cwd,
1288+
ctx.base_prompt.as_deref(),
1289+
ctx.system_prompt.as_deref(),
1290+
ctx.team_instructions.as_deref(),
1291+
agent_core,
1292+
channel.huddle_instructions,
12971293
channel.canvas,
12981294
);
12991295

@@ -1854,6 +1850,40 @@ pub(crate) fn prepend_standing_for_legacy(
18541850
/// agent instructions. A persona-only agent still yields
18551851
/// `<system>…</system>` rather than an unlabeled blob that would be mistaken
18561852
/// for `<base>`.
1853+
/// The standing system prompt one `session/new` carries.
1854+
///
1855+
/// The agent's own instructions are framed with the base prompt and workspace
1856+
/// section, then the team, core-memory, huddle and canvas sections are appended
1857+
/// in that order. Every section body is preserved byte-for-byte, so a prompt
1858+
/// loaded from a file reaches the adapter exactly as written.
1859+
///
1860+
/// This is the whole composition `create_session_and_apply_model` performs; it
1861+
/// is a named function so a caller outside this module can drive the same
1862+
/// composition production does instead of restating the nesting.
1863+
pub fn combined_system_prompt(
1864+
cwd: &str,
1865+
base_prompt: Option<&str>,
1866+
system_prompt: Option<&str>,
1867+
team_instructions: Option<&str>,
1868+
agent_core: Option<&str>,
1869+
huddle_instructions: Option<&str>,
1870+
canvas: Option<&str>,
1871+
) -> Option<String> {
1872+
with_canvas(
1873+
with_huddle_instructions(
1874+
with_core(
1875+
with_team(
1876+
framed_system_prompt(cwd, base_prompt, system_prompt),
1877+
team_instructions,
1878+
),
1879+
agent_core,
1880+
),
1881+
huddle_instructions,
1882+
),
1883+
canvas,
1884+
)
1885+
}
1886+
18571887
fn framed_system_prompt(
18581888
cwd: &str,
18591889
base_prompt: Option<&str>,

0 commit comments

Comments
 (0)