Add ACP session system prompt setter (#9478)

Signed-off-by: Bradley Axen <baxen@squareup.com>
This commit is contained in:
Bradley Axen
2026-05-28 17:35:12 -07:00
committed by GitHub
parent 1cb5cb06a3
commit 104cc17758
11 changed files with 241 additions and 4 deletions
+31
View File
@@ -108,6 +108,37 @@ pub struct UpdateWorkingDirRequest {
pub working_dir: String,
}
/// How a session system prompt update should be applied.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SessionSystemPromptMode {
/// Replace Goose's base system prompt with the provided text.
Set,
/// Append the provided text under Goose's "Additional Instructions" section.
#[default]
Append,
}
/// Set, append, or clear system prompt text for a session.
///
/// `mode: "set"` replaces Goose's base system prompt. `mode: "append"` adds an
/// instruction under "Additional Instructions". Reusing a key replaces the
/// previous value for that mode/key; sending empty text clears it.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(
method = "_goose/unstable/session/system-prompt/set",
response = EmptyResponse
)]
#[serde(rename_all = "camelCase")]
pub struct SetSessionSystemPromptRequest {
pub session_id: String,
#[serde(default)]
pub mode: SessionSystemPromptMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
pub text: String,
}
/// Delete a session.
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
#[request(method = "session/delete", response = EmptyResponse)]
+5
View File
@@ -30,6 +30,11 @@
"requestType": "UpdateWorkingDirRequest_unstable",
"responseType": "EmptyResponse"
},
{
"method": "_goose/unstable/session/system-prompt/set",
"requestType": "SetSessionSystemPromptRequest_unstable",
"responseType": "EmptyResponse"
},
{
"method": "session/delete",
"requestType": "DeleteSessionRequest",
+52
View File
@@ -167,6 +167,49 @@
"x-side": "agent",
"x-method": "_goose/unstable/session/working-dir/update"
},
"SetSessionSystemPromptRequest_unstable": {
"type": "object",
"properties": {
"sessionId": {
"type": "string"
},
"mode": {
"$ref": "#/$defs/SessionSystemPromptMode",
"default": "append"
},
"key": {
"type": [
"string",
"null"
]
},
"text": {
"type": "string"
}
},
"required": [
"sessionId",
"text"
],
"description": "Set, append, or clear system prompt text for a session.\n\n`mode: \"set\"` replaces Goose's base system prompt. `mode: \"append\"` adds an\ninstruction under \"Additional Instructions\". Reusing a key replaces the\nprevious value for that mode/key; sending empty text clears it.",
"x-side": "agent",
"x-method": "_goose/unstable/session/system-prompt/set"
},
"SessionSystemPromptMode": {
"oneOf": [
{
"type": "string",
"const": "set",
"description": "Replace Goose's base system prompt with the provided text."
},
{
"type": "string",
"const": "append",
"description": "Append the provided text under Goose's \"Additional Instructions\" section."
}
],
"description": "How a session system prompt update should be applied."
},
"DeleteSessionRequest": {
"type": "object",
"properties": {
@@ -2640,6 +2683,15 @@
"description": "Params for _goose/unstable/session/working-dir/update",
"title": "UpdateWorkingDirRequest_unstable"
},
{
"allOf": [
{
"$ref": "#/$defs/SetSessionSystemPromptRequest_unstable"
}
],
"description": "Params for _goose/unstable/session/system-prompt/set",
"title": "SetSessionSystemPromptRequest_unstable"
},
{
"allOf": [
{
@@ -59,6 +59,14 @@ impl GooseAcpAgent {
self.on_update_working_dir(req).await
}
#[custom_method(SetSessionSystemPromptRequest)]
async fn dispatch_set_session_system_prompt(
&self,
req: SetSessionSystemPromptRequest,
) -> Result<EmptyResponse, agent_client_protocol::Error> {
self.on_set_session_system_prompt(req).await
}
#[custom_method(DeleteSessionRequest)]
async fn dispatch_delete_session(
&self,
+41
View File
@@ -34,6 +34,47 @@ impl GooseAcpAgent {
Ok(EmptyResponse {})
}
pub(super) async fn on_set_session_system_prompt(
&self,
req: SetSessionSystemPromptRequest,
) -> Result<EmptyResponse, agent_client_protocol::Error> {
let session_id = req.session_id.trim();
if session_id.is_empty() {
return Err(
agent_client_protocol::Error::invalid_params().data("sessionId cannot be empty")
);
}
let agent = self.get_session_agent_provider_ready(session_id).await?;
match req.mode {
SessionSystemPromptMode::Set => {
if req.text.trim().is_empty() {
agent.clear_system_prompt_override().await;
} else {
agent.override_system_prompt(req.text).await;
}
}
SessionSystemPromptMode::Append => {
let key = req
.key
.as_deref()
.map(str::trim)
.filter(|key| !key.is_empty())
.ok_or_else(|| {
agent_client_protocol::Error::invalid_params()
.data("key cannot be empty for append mode")
})?;
if req.text.trim().is_empty() {
agent.remove_system_prompt_extra(key).await;
} else {
agent.extend_system_prompt(key.to_string(), req.text).await;
}
}
}
Ok(EmptyResponse {})
}
pub(super) async fn on_delete_session(
&self,
req: DeleteSessionRequest,
+11 -2
View File
@@ -66,8 +66,7 @@ use tracing::{debug, error, info, instrument, warn};
const DEFAULT_MAX_TURNS: u32 = 1000;
const COMPACTION_THINKING_TEXT: &str = "goose is compacting the conversation...";
const DEFAULT_FRONTEND_INSTRUCTIONS: &str =
"The following tools are provided directly by the frontend and will be executed by the frontend when called.";
const DEFAULT_FRONTEND_INSTRUCTIONS: &str = "The following tools are provided directly by the frontend and will be executed by the frontend when called.";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ToolCategory {
@@ -2293,6 +2292,11 @@ impl Agent {
prompt_manager.add_system_prompt_extra(key, instruction);
}
pub async fn remove_system_prompt_extra(&self, key: &str) {
let mut prompt_manager = self.prompt_manager.lock().await;
prompt_manager.remove_system_prompt_extra(key);
}
pub async fn set_goal(&self, goal: Option<String>) {
*self.goal.lock().await = goal;
}
@@ -2463,6 +2467,11 @@ impl Agent {
prompt_manager.set_system_prompt_override(template);
}
pub async fn clear_system_prompt_override(&self) {
let mut prompt_manager = self.prompt_manager.lock().await;
prompt_manager.clear_system_prompt_override();
}
pub async fn list_extension_prompts(&self, session_id: &str) -> HashMap<String, Vec<Prompt>> {
self.extension_manager
.list_prompts(session_id, CancellationToken::default())
+31
View File
@@ -224,6 +224,10 @@ impl PromptManager {
self.system_prompt_extras.insert(key, instruction);
}
pub fn remove_system_prompt_extra(&mut self, key: &str) {
self.system_prompt_extras.shift_remove(key);
}
pub fn record_tool_arguments(
&mut self,
arguments: &Option<serde_json::Map<String, serde_json::Value>>,
@@ -247,6 +251,10 @@ impl PromptManager {
self.system_prompt_override = Some(template);
}
pub fn clear_system_prompt_override(&mut self) {
self.system_prompt_override = None;
}
pub fn builder<'a>(&'a self) -> SystemPromptBuilder<'a, Self> {
SystemPromptBuilder {
manager: self,
@@ -326,6 +334,29 @@ mod tests {
assert!(result.contains("Thirdinstruction"));
}
#[test]
fn test_remove_system_prompt_extra() {
let mut manager = PromptManager::new();
manager.add_system_prompt_extra("agent".to_string(), "Agent instruction".to_string());
manager.add_system_prompt_extra("project".to_string(), "Project instruction".to_string());
manager.remove_system_prompt_extra("agent");
let result = manager.builder().build();
assert!(!result.contains("Agent instruction"));
assert!(result.contains("Project instruction"));
}
#[test]
fn test_clear_system_prompt_override() {
let mut manager = PromptManager::new();
manager.set_system_prompt_override("Replacement prompt".to_string());
assert!(manager.builder().build().contains("Replacement prompt"));
manager.clear_system_prompt_override();
assert!(!manager.builder().build().contains("Replacement prompt"));
}
#[test]
fn test_build_system_prompt_preserves_legitimate_unicode_in_extras() {
let mut manager = PromptManager::new();