mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(buzz-acp): route goose systemPrompt via ACP+ _meta extension
For goose agents, systemPrompt must be passed via _meta["systemPrompt"] rather than as a first-class session/new field, per the ACP extensibility convention (ACP+ / _meta approach). Buzz-agent native agents continue to use the top-level systemPrompt field unchanged. Add session_new_full_goose to AcpClient, which puts systemPrompt inside _meta instead of at the top level. Add agent_command to PromptContext so create_session_and_apply_model can detect goose (via normalize_agent_command_identity) and dispatch to the correct variant. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
co-authored by
Will Pfleger
parent
116a445aab
commit
79eabd4a15
@@ -321,6 +321,34 @@ impl AcpClient {
|
||||
})
|
||||
}
|
||||
|
||||
/// Send `session/new` for a goose agent, passing `systemPrompt` via `_meta`
|
||||
/// per the ACP+ extensibility convention (goose reads it from `_meta["systemPrompt"]`
|
||||
/// rather than as a first-class field).
|
||||
pub async fn session_new_full_goose(
|
||||
&mut self,
|
||||
cwd: &str,
|
||||
mcp_servers: Vec<McpServer>,
|
||||
system_prompt: Option<&str>,
|
||||
) -> Result<SessionNewResponse, AcpError> {
|
||||
let mut params = serde_json::json!({
|
||||
"cwd": cwd,
|
||||
"mcpServers": mcp_servers,
|
||||
});
|
||||
if let Some(sp) = system_prompt {
|
||||
params["_meta"] = serde_json::json!({ "systemPrompt": sp });
|
||||
}
|
||||
let result = self.send_request("session/new", params).await?;
|
||||
let session_id = result["sessionId"]
|
||||
.as_str()
|
||||
.ok_or_else(|| AcpError::Protocol("session/new response missing sessionId".into()))?
|
||||
.to_owned();
|
||||
tracing::info!(target: "acp::session", "session created: {session_id}");
|
||||
Ok(SessionNewResponse {
|
||||
session_id,
|
||||
raw: result,
|
||||
})
|
||||
}
|
||||
|
||||
/// Send `session/new` and return only the `sessionId` string.
|
||||
///
|
||||
/// Convenience wrapper around [`session_new_full`].
|
||||
@@ -2068,4 +2096,67 @@ mod tests {
|
||||
"systemPrompt should NOT be in params when value is None"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_new_full_goose_includes_system_prompt_in_meta_when_some() {
|
||||
// Goose path: systemPrompt must appear in _meta, NOT as a top-level field.
|
||||
let script = r#"
|
||||
read -t 2 _init
|
||||
echo '{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":1,"agentCapabilities":{}}}'
|
||||
read -t 2 REQ
|
||||
echo '{"jsonrpc":"2.0","id":1,"result":{"sessionId":"ses_goose","_receivedRequest":'"$REQ"'}}'
|
||||
sleep 1
|
||||
"#;
|
||||
let mut client = spawn_script(script).await;
|
||||
client
|
||||
.initialize()
|
||||
.await
|
||||
.expect("initialize should succeed");
|
||||
|
||||
let resp = client
|
||||
.session_new_full_goose("/tmp", vec![], Some("Be concise."))
|
||||
.await
|
||||
.expect("session_new_full_goose should succeed");
|
||||
|
||||
assert_eq!(resp.session_id, "ses_goose");
|
||||
let received = &resp.raw["_receivedRequest"];
|
||||
assert_eq!(
|
||||
received["params"]["_meta"]["systemPrompt"].as_str(),
|
||||
Some("Be concise."),
|
||||
"systemPrompt should be in _meta when Some"
|
||||
);
|
||||
assert!(
|
||||
received["params"]["systemPrompt"].is_null(),
|
||||
"systemPrompt should NOT appear as a top-level field"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_new_full_goose_omits_meta_when_none() {
|
||||
// Goose path: when system_prompt is None, _meta should not appear in params.
|
||||
let script = r#"
|
||||
read -t 2 _init
|
||||
echo '{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":1,"agentCapabilities":{}}}'
|
||||
read -t 2 REQ
|
||||
echo '{"jsonrpc":"2.0","id":1,"result":{"sessionId":"ses_goose","_receivedRequest":'"$REQ"'}}'
|
||||
sleep 1
|
||||
"#;
|
||||
let mut client = spawn_script(script).await;
|
||||
client
|
||||
.initialize()
|
||||
.await
|
||||
.expect("initialize should succeed");
|
||||
|
||||
let resp = client
|
||||
.session_new_full_goose("/tmp", vec![], None)
|
||||
.await
|
||||
.expect("session_new_full_goose should succeed");
|
||||
|
||||
assert_eq!(resp.session_id, "ses_goose");
|
||||
let received = &resp.raw["_receivedRequest"];
|
||||
assert!(
|
||||
received["params"]["_meta"].is_null(),
|
||||
"_meta should NOT be in params when system_prompt is None"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,7 +492,7 @@ fn validate_allowlist(entries: &[String]) -> Result<HashSet<String>, ConfigError
|
||||
Ok(validated)
|
||||
}
|
||||
|
||||
fn normalize_agent_command_identity(command: &str) -> String {
|
||||
pub(crate) fn normalize_agent_command_identity(command: &str) -> String {
|
||||
let normalized = command.trim().replace('\\', "/");
|
||||
let trimmed = normalized.trim_end_matches('/');
|
||||
let basename = trimmed
|
||||
|
||||
@@ -1277,6 +1277,7 @@ async fn tokio_main() -> Result<()> {
|
||||
.as_deref()
|
||||
.and_then(|hex| nostr::PublicKey::from_hex(hex).ok()),
|
||||
memory_enabled: config.memory_enabled,
|
||||
agent_command: config.agent_command.clone(),
|
||||
});
|
||||
|
||||
if !config.memory_enabled {
|
||||
|
||||
@@ -32,7 +32,7 @@ use crate::acp::{
|
||||
extract_model_config_options, extract_model_state, resolve_model_switch_method, AcpClient,
|
||||
AcpError, McpServer, ModelSwitchMethod, StopReason,
|
||||
};
|
||||
use crate::config::{DedupMode, PermissionMode};
|
||||
use crate::config::{normalize_agent_command_identity, DedupMode, PermissionMode};
|
||||
use crate::observer;
|
||||
use crate::queue::{
|
||||
ContextMessage, ConversationContext, FlushBatch, PromptChannelInfo, PromptProfile,
|
||||
@@ -250,6 +250,9 @@ pub struct PromptContext {
|
||||
/// `[Agent Memory — core]` section. On by default; disabled via
|
||||
/// `--no-memory` / `BUZZ_ACP_NO_MEMORY`.
|
||||
pub memory_enabled: bool,
|
||||
/// Agent command (binary name or path). Used to detect goose agents and
|
||||
/// route `systemPrompt` via `_meta` rather than as a first-class field.
|
||||
pub agent_command: String,
|
||||
}
|
||||
|
||||
impl AgentPool {
|
||||
@@ -438,14 +441,25 @@ async fn create_session_and_apply_model(
|
||||
None
|
||||
};
|
||||
|
||||
let resp = agent
|
||||
.acp
|
||||
.session_new_full(
|
||||
&ctx.cwd,
|
||||
ctx.mcp_servers.clone(),
|
||||
combined_system_prompt.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let resp = if normalize_agent_command_identity(&ctx.agent_command) == "goose" {
|
||||
agent
|
||||
.acp
|
||||
.session_new_full_goose(
|
||||
&ctx.cwd,
|
||||
ctx.mcp_servers.clone(),
|
||||
combined_system_prompt.as_deref(),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
agent
|
||||
.acp
|
||||
.session_new_full(
|
||||
&ctx.cwd,
|
||||
ctx.mcp_servers.clone(),
|
||||
combined_system_prompt.as_deref(),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
// Populate model capabilities on first session creation.
|
||||
if agent.model_capabilities.is_none() {
|
||||
|
||||
Reference in New Issue
Block a user