diff --git a/crates/goose-providers/src/conversation/message.rs b/crates/goose-providers/src/conversation/message.rs index a89d4760b1e5..5b0a1d047dc7 100644 --- a/crates/goose-providers/src/conversation/message.rs +++ b/crates/goose-providers/src/conversation/message.rs @@ -667,6 +667,11 @@ pub struct MessageMetadata { pub agent_visible: bool, #[serde(skip_serializing_if = "Option::is_none")] pub inference: Option, + /// Whether this message is a steer injected into an active run. UI-only: + /// surfaced as `_meta.goose.steer` so clients can mark the steer boundary + /// without matching user-visible text. Never sent to providers. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub steer: bool, } impl Default for MessageMetadata { @@ -675,6 +680,7 @@ impl Default for MessageMetadata { user_visible: true, agent_visible: true, inference: None, + steer: false, } } } @@ -743,6 +749,11 @@ impl MessageMetadata { self.inference = Some(inference); self } + + pub fn with_steer(mut self) -> Self { + self.steer = true; + self + } } #[derive(ToSchema, Clone, PartialEq, Serialize, Deserialize, Debug)] @@ -1028,6 +1039,11 @@ impl Message { self } + pub fn with_steer(mut self) -> Self { + self.metadata.steer = true; + self + } + pub fn with_inference_if_assistant(self, inference: InferenceMetadata) -> Self { if self.role == Role::Assistant && self.metadata.inference.is_none() { self.with_inference(inference) diff --git a/crates/goose-sdk-types/src/custom_requests.rs b/crates/goose-sdk-types/src/custom_requests.rs index c69654032dbd..b90515977cd7 100644 --- a/crates/goose-sdk-types/src/custom_requests.rs +++ b/crates/goose-sdk-types/src/custom_requests.rs @@ -1,4 +1,4 @@ -use agent_client_protocol::schema::McpServer; +use agent_client_protocol::schema::{ContentBlock, McpServer}; use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -140,6 +140,30 @@ pub struct SetSessionSystemPromptRequest { pub text: String, } +/// Add user input to the currently active prompt without starting a new prompt. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/session/steer", + response = SteerSessionResponse +)] +#[serde(rename_all = "camelCase")] +pub struct SteerSessionRequest { + pub session_id: String, + #[serde(default)] + pub prompt: Vec, + pub expected_run_id: String, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct SteerSessionResponse { + pub run_id: String, + /// Stable id of the queued steer message. The same id later appears as + /// `messageId` on the streamed `UserMessageChunk` (with `_meta.goose.steer`), + /// letting clients correlate a queued steer with its pickup. + pub message_id: String, +} + /// Delete a session. #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] #[request(method = "session/delete", response = EmptyResponse)] diff --git a/crates/goose/acp-meta.json b/crates/goose/acp-meta.json index 7be80cc9d4cc..8d5be7fe34b8 100644 --- a/crates/goose/acp-meta.json +++ b/crates/goose/acp-meta.json @@ -35,6 +35,11 @@ "requestType": "SetSessionSystemPromptRequest_unstable", "responseType": "EmptyResponse" }, + { + "method": "_goose/unstable/session/steer", + "requestType": "SteerSessionRequest_unstable", + "responseType": "SteerSessionResponse_unstable" + }, { "method": "session/delete", "requestType": "DeleteSessionRequest", diff --git a/crates/goose/acp-schema.json b/crates/goose/acp-schema.json index 1cdb6a3e9eaf..e818403b5b3a 100644 --- a/crates/goose/acp-schema.json +++ b/crates/goose/acp-schema.json @@ -210,6 +210,434 @@ ], "description": "How a session system prompt update should be applied." }, + "SteerSessionRequest_unstable": { + "type": "object", + "properties": { + "sessionId": { + "type": "string" + }, + "prompt": { + "type": "array", + "items": { + "$ref": "#/$defs/ContentBlock" + }, + "default": [] + }, + "expectedRunId": { + "type": "string" + } + }, + "required": [ + "sessionId", + "expectedRunId" + ], + "description": "Add user input to the currently active prompt without starting a new prompt.", + "x-side": "agent", + "x-method": "_goose/unstable/session/steer" + }, + "ContentBlock": { + "oneOf": [ + { + "$ref": "#/$defs/TextContent", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "text" + } + }, + "required": [ + "type" + ], + "description": "Text content. May be plain text or formatted with Markdown.\n\nAll agents MUST support text content blocks in prompts.\nClients SHOULD render this text as Markdown." + }, + { + "$ref": "#/$defs/ImageContent", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "image" + } + }, + "required": [ + "type" + ], + "description": "Images for visual context or analysis.\n\nRequires the `image` prompt capability when included in prompts." + }, + { + "$ref": "#/$defs/AudioContent", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "audio" + } + }, + "required": [ + "type" + ], + "description": "Audio data for transcription or analysis.\n\nRequires the `audio` prompt capability when included in prompts." + }, + { + "$ref": "#/$defs/ResourceLink", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource_link" + } + }, + "required": [ + "type" + ], + "description": "References to resources that the agent can access.\n\nAll agents MUST support resource links in prompts." + }, + { + "$ref": "#/$defs/EmbeddedResource", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource" + } + }, + "required": [ + "type" + ], + "description": "Complete resource contents embedded directly in the message.\n\nPreferred for including context as it avoids extra round-trips.\n\nRequires the `embeddedContext` prompt capability when included in prompts." + } + ], + "description": "Content blocks represent displayable information in the Agent Client Protocol.\n\nThey provide a structured way to handle various types of user-facing content—whether\nit's text from language models, images for analysis, or embedded resources for context.\n\nContent blocks appear in:\n- User prompts sent via `session/prompt`\n- Language model output streamed through `session/update` notifications\n- Progress updates and results from tool calls\n\nThis structure is compatible with the Model Context Protocol (MCP), enabling\nagents to seamlessly forward content from MCP tool outputs without transformation.\n\nSee protocol docs: [Content](https://agentclientprotocol.com/protocol/content)", + "discriminator": { + "propertyName": "type" + } + }, + "Annotations": { + "type": "object", + "properties": { + "audience": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/$defs/Role" + } + }, + "lastModified": { + "type": [ + "string", + "null" + ] + }, + "priority": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" + } + }, + "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed" + }, + "Role": { + "type": "string", + "enum": [ + "assistant", + "user" + ], + "description": "The sender or recipient of messages and data in a conversation." + }, + "TextContent": { + "type": "object", + "properties": { + "annotations": { + "anyOf": [ + { + "$ref": "#/$defs/Annotations" + }, + { + "type": "null" + } + ] + }, + "text": { + "type": "string" + }, + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" + } + }, + "required": [ + "text" + ], + "description": "Text provided to or from an LLM." + }, + "ImageContent": { + "type": "object", + "properties": { + "annotations": { + "anyOf": [ + { + "$ref": "#/$defs/Annotations" + }, + { + "type": "null" + } + ] + }, + "data": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "uri": { + "type": [ + "string", + "null" + ] + }, + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" + } + }, + "required": [ + "data", + "mimeType" + ], + "description": "An image provided to or from an LLM." + }, + "AudioContent": { + "type": "object", + "properties": { + "annotations": { + "anyOf": [ + { + "$ref": "#/$defs/Annotations" + }, + { + "type": "null" + } + ] + }, + "data": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" + } + }, + "required": [ + "data", + "mimeType" + ], + "description": "Audio provided to or from an LLM." + }, + "ResourceLink": { + "type": "object", + "properties": { + "annotations": { + "anyOf": [ + { + "$ref": "#/$defs/Annotations" + }, + { + "type": "null" + } + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "mimeType": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "size": { + "type": [ + "integer", + "null" + ] + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "uri": { + "type": "string" + }, + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" + } + }, + "required": [ + "name", + "uri" + ], + "description": "A resource that the server is capable of reading, included in a prompt or tool call result." + }, + "EmbeddedResourceResource": { + "anyOf": [ + { + "$ref": "#/$defs/TextResourceContents" + }, + { + "$ref": "#/$defs/BlobResourceContents" + } + ], + "description": "Resource content that can be embedded in a message." + }, + "TextResourceContents": { + "type": "object", + "properties": { + "mimeType": { + "type": [ + "string", + "null" + ] + }, + "text": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" + } + }, + "required": [ + "text", + "uri" + ], + "description": "Text-based resource contents." + }, + "BlobResourceContents": { + "type": "object", + "properties": { + "blob": { + "type": "string" + }, + "mimeType": { + "type": [ + "string", + "null" + ] + }, + "uri": { + "type": "string" + }, + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" + } + }, + "required": [ + "blob", + "uri" + ], + "description": "Binary resource contents." + }, + "EmbeddedResource": { + "type": "object", + "properties": { + "annotations": { + "anyOf": [ + { + "$ref": "#/$defs/Annotations" + }, + { + "type": "null" + } + ] + }, + "resource": { + "$ref": "#/$defs/EmbeddedResourceResource" + }, + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" + } + }, + "required": [ + "resource" + ], + "description": "The contents of a resource, embedded into a prompt or tool call result." + }, + "SteerSessionResponse_unstable": { + "type": "object", + "properties": { + "runId": { + "type": "string" + }, + "messageId": { + "type": "string", + "description": "Stable id of the queued steer message. The same id later appears as\n`messageId` on the streamed `UserMessageChunk` (with `_meta.goose.steer`),\nletting clients correlate a queued steer with its pickup." + } + }, + "required": [ + "runId", + "messageId" + ], + "x-side": "agent", + "x-method": "_goose/unstable/session/steer" + }, "DeleteSessionRequest": { "type": "object", "properties": { @@ -3325,6 +3753,15 @@ "description": "Params for _goose/unstable/session/system-prompt/set", "title": "SetSessionSystemPromptRequest_unstable" }, + { + "allOf": [ + { + "$ref": "#/$defs/SteerSessionRequest_unstable" + } + ], + "description": "Params for _goose/unstable/session/steer", + "title": "SteerSessionRequest_unstable" + }, { "allOf": [ { @@ -3855,6 +4292,14 @@ ], "title": "ReadResourceResponse_unstable" }, + { + "allOf": [ + { + "$ref": "#/$defs/SteerSessionResponse_unstable" + } + ], + "title": "SteerSessionResponse_unstable" + }, { "allOf": [ { diff --git a/crates/goose/src/acp/server.rs b/crates/goose/src/acp/server.rs index 1fa5046c88fe..31df35f0dc33 100644 --- a/crates/goose/src/acp/server.rs +++ b/crates/goose/src/acp/server.rs @@ -79,6 +79,7 @@ use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, warn}; use url::Url; +use uuid::Uuid; mod config; mod custom_dispatch; @@ -170,6 +171,7 @@ struct GooseAcpSession { /// Idempotence guard so we summarize each chain at most once. summarized_chains: HashSet, cancel_token: Option, + active_run_id: Option, } /// A run of consecutive ToolRequest blocks within one assistant message, @@ -1189,6 +1191,7 @@ impl GooseAcpAgent { responded_tool_ids: HashSet::new(), summarized_chains: HashSet::new(), cancel_token: None, + active_run_id: None, }; self.sessions.lock().await.insert(session_id, acp_session); } @@ -1286,19 +1289,22 @@ impl GooseAcpAgent { session_id_str: &str, message_id: Option<&str>, message_created: i64, + role: &Role, + steer: bool, agent: &Arc, session: &mut GooseAcpSession, cx: &ConnectionTo, ) -> Result<(), agent_client_protocol::Error> { match content_item { MessageContent::Text(text) => { - cx.send_notification(SessionNotification::new( - session_id.clone(), - SessionUpdate::AgentMessageChunk( - ContentChunk::new(ContentBlock::Text(TextContent::new(text.text.clone()))) - .meta(message_update_meta(message_id, message_created)), - ), - ))?; + let chunk = + ContentChunk::new(ContentBlock::Text(TextContent::new(text.text.clone()))) + .meta(message_update_meta(message_id, message_created, steer)); + let update = match role { + Role::User => SessionUpdate::UserMessageChunk(chunk), + Role::Assistant => SessionUpdate::AgentMessageChunk(chunk), + }; + cx.send_notification(SessionNotification::new(session_id.clone(), update))?; } MessageContent::ToolRequest(tool_request) => { self.handle_tool_request( @@ -1329,7 +1335,11 @@ impl GooseAcpAgent { ContentChunk::new(ContentBlock::Text(TextContent::new( thinking.thinking.clone(), ))) - .meta(message_update_meta(message_id, message_created)), + .meta(message_update_meta( + message_id, + message_created, + steer, + )), ), ))?; } @@ -1995,15 +2005,18 @@ fn send_elicitation_interaction_update( } fn interaction_update_meta(message_id: Option<&str>, created: i64) -> serde_json::Value { - serde_json::Value::Object(message_update_meta(message_id, created)) + serde_json::Value::Object(message_update_meta(message_id, created, false)) } -fn message_update_meta(message_id: Option<&str>, created: i64) -> Meta { +fn message_update_meta(message_id: Option<&str>, created: i64, steer: bool) -> Meta { let mut goose = serde_json::Map::new(); goose.insert("created".to_string(), serde_json::json!(created)); if let Some(id) = message_id { goose.insert("messageId".to_string(), serde_json::json!(id)); } + if steer { + goose.insert("steer".to_string(), serde_json::json!(true)); + } let mut meta = serde_json::Map::new(); meta.insert("goose".to_string(), serde_json::Value::Object(goose)); @@ -2040,6 +2053,9 @@ fn replay_message_goose_meta(message: &Message) -> serde_json::Map Result<(), agent_client_protocol::Error> { + let mut sessions = self.sessions.lock().await; + let session = sessions.get_mut(session_id).ok_or_else(|| { + agent_client_protocol::Error::resource_not_found(Some(session_id.to_string())) + .data(format!("Session not found: {}", session_id)) + })?; + + if let Some(active_run_id) = &session.active_run_id { + return Err(agent_client_protocol::Error::invalid_params().data(format!( + "session already has active run `{active_run_id}`; use _goose/unstable/session/steer" + ))); + } + + session.cancel_token = Some(cancel_token); + session.active_run_id = Some(run_id); + Ok(()) + } + + async fn clear_active_run(&self, session_id: &str, run_id: &str) { + let agent = { + let mut sessions = self.sessions.lock().await; + let Some(session) = sessions.get_mut(session_id) else { + return; + }; + if session.active_run_id.as_deref() != Some(run_id) { + return; + } + session.cancel_token = None; + session.active_run_id = None; + session.agent.clone() + }; + agent.discard_pending_steers(session_id).await; + } + + async fn require_active_run( + &self, + session_id: &str, + expected_run_id: &str, + ) -> Result { + if expected_run_id.is_empty() { + return Err(agent_client_protocol::Error::invalid_params() + .data("expectedRunId must not be empty")); + } + + let sessions = self.sessions.lock().await; + let session = sessions.get(session_id).ok_or_else(|| { + agent_client_protocol::Error::resource_not_found(Some(session_id.to_string())) + .data(format!("Session not found: {}", session_id)) + })?; + let active_run_id = session.active_run_id.as_ref().ok_or_else(|| { + agent_client_protocol::Error::invalid_params().data("no active run to steer") + })?; + if active_run_id != expected_run_id { + return Err( + agent_client_protocol::Error::invalid_params().data(serde_json::json!({ + "message": format!( + "expected active run id `{expected_run_id}` but found `{active_run_id}`" + ), + "expectedRunId": expected_run_id, + "actualRunId": active_run_id, + })), + ); + } + Ok(active_run_id.clone()) + } + + fn active_run_meta(active_run_id: Option<&str>) -> Meta { + let mut goose = serde_json::Map::new(); + goose.insert( + "activeRunId".to_string(), + active_run_id + .map(|run_id| serde_json::Value::String(run_id.to_string())) + .unwrap_or(serde_json::Value::Null), + ); + + let mut meta = serde_json::Map::new(); + meta.insert("goose".to_string(), serde_json::Value::Object(goose)); + meta + } + + fn send_active_run_update( + cx: &ConnectionTo, + session_id: &SessionId, + active_run_id: Option<&str>, + ) -> Result<(), agent_client_protocol::Error> { + cx.send_notification(SessionNotification::new( + session_id.clone(), + SessionUpdate::SessionInfoUpdate( + SessionInfoUpdate::new().meta(Self::active_run_meta(active_run_id)), + ), + )) + } + + fn send_queued_steer_update( + cx: &ConnectionTo, + session_id: &SessionId, + message_id: &str, + run_id: &str, + ) -> Result<(), agent_client_protocol::Error> { + let mut goose = serde_json::Map::new(); + goose.insert( + "queuedSteer".to_string(), + serde_json::json!({ + "messageId": message_id, + "runId": run_id, + }), + ); + let mut meta = serde_json::Map::new(); + meta.insert("goose".to_string(), serde_json::Value::Object(goose)); + + cx.send_notification(SessionNotification::new( + session_id.clone(), + SessionUpdate::SessionInfoUpdate(SessionInfoUpdate::new().meta(meta)), + )) + } + #[allow(dead_code)] async fn add_mcp_extensions( agent: &Arc, @@ -2263,10 +2400,23 @@ impl GooseAcpAgent { let sid = sid_short(&session_id); let t_start = std::time::Instant::now(); + let run_id = format!("run_{}", Uuid::new_v4()); let cancel_token = CancellationToken::new(); - let agent = self - .get_session_agent(&session_id, Some(cancel_token.clone())) + self.start_active_run(&session_id, run_id.clone(), cancel_token.clone()) .await?; + if let Err(error) = Self::send_active_run_update(cx, &args.session_id, Some(&run_id)) { + self.clear_active_run(&session_id, &run_id).await; + return Err(error); + } + + let agent = match self.get_session_agent(&session_id, None).await { + Ok(agent) => agent, + Err(error) => { + self.clear_active_run(&session_id, &run_id).await; + let _ = Self::send_active_run_update(cx, &args.session_id, None); + return Err(error); + } + }; let user_message = Self::convert_acp_prompt_to_message(&args.prompt); @@ -2281,7 +2431,7 @@ impl GooseAcpAgent { ) { if recipe_path.exists() { - cx.send_notification(SessionNotification::new( + if let Err(error) = cx.send_notification(SessionNotification::new( args.session_id.clone(), SessionUpdate::AgentMessageChunk(ContentChunk::new( ContentBlock::Text(TextContent::new(format!( @@ -2289,7 +2439,11 @@ impl GooseAcpAgent { full_command ))), )), - ))?; + )) { + self.clear_active_run(&session_id, &run_id).await; + let _ = Self::send_active_run_update(cx, &args.session_id, None); + return Err(error); + } } } } @@ -2302,10 +2456,18 @@ impl GooseAcpAgent { retry_config: None, }; - let mut stream = agent + let mut stream = match agent .reply(user_message, session_config, Some(cancel_token.clone())) .await - .internal_err_ctx("Error getting agent reply")?; + { + Ok(stream) => stream, + Err(error) => { + self.clear_active_run(&session_id, &run_id).await; + let _ = Self::send_active_run_update(cx, &args.session_id, None); + return Err(agent_client_protocol::Error::internal_error() + .data(format!("Error getting agent reply: {error}"))); + } + }; let mut was_cancelled = false; let mut first_event_logged = false; @@ -2321,6 +2483,7 @@ impl GooseAcpAgent { // `handle_tool_response` finds the chain when subsequent responses // are processed. let mut chain_buffer: Vec<(String, String)> = Vec::new(); + let mut stream_error = None; while let Some(event) = stream.next().await { if cancel_token.is_cancelled() { @@ -2344,15 +2507,18 @@ impl GooseAcpAgent { let stored_message_id = message.id.clone(); let mut sessions = self.sessions.lock().await; - let session = sessions.get_mut(&session_id).ok_or_else(|| { - agent_client_protocol::Error::invalid_params() - .data(format!("Session not found: {}", session_id)) - })?; + let Some(session) = sessions.get_mut(&session_id) else { + stream_error = Some( + agent_client_protocol::Error::invalid_params() + .data(format!("Session not found: {}", session_id)), + ); + break; + }; for content_item in &message.content { if let Some(error) = prompt_error_from_message_content(content_item) { - session.cancel_token = None; - return Err(error); + stream_error = Some(error); + break; } match content_item { @@ -2382,23 +2548,36 @@ impl GooseAcpAgent { } } - self.handle_message_content( - content_item, - &args.session_id, - &session_id, - stored_message_id.as_deref(), - message.created, - &agent, - session, - cx, - ) - .await?; + if let Err(error) = self + .handle_message_content( + content_item, + &args.session_id, + &session_id, + stored_message_id.as_deref(), + message.created, + &message.role, + message.metadata.steer, + &agent, + session, + cx, + ) + .await + { + stream_error = Some(error); + break; + } + } + if stream_error.is_some() { + break; } } Ok(_) => {} Err(e) => { - return Err(agent_client_protocol::Error::internal_error() - .data(format!("Error in agent response stream: {}", e))); + stream_error = Some( + agent_client_protocol::Error::internal_error() + .data(format!("Error in agent response stream: {}", e)), + ); + break; } } } @@ -2411,9 +2590,13 @@ impl GooseAcpAgent { // registered. (Eager registration during the loop usually // covers this.) extend_chain_membership(&chain_buffer, &mut session.chain_membership); - session.cancel_token = None; } } + self.clear_active_run(&session_id, &run_id).await; + Self::send_active_run_update(cx, &args.session_id, None)?; + if let Some(error) = stream_error { + return Err(error); + } let session = self .session_manager @@ -2454,6 +2637,48 @@ impl GooseAcpAgent { Ok(response) } + async fn on_steer_session( + &self, + req: SteerSessionRequest, + ) -> Result { + if req.prompt.is_empty() { + return Err( + agent_client_protocol::Error::invalid_params().data("prompt must not be empty") + ); + } + + self.require_active_run(&req.session_id, &req.expected_run_id) + .await?; + let agent = self.get_session_agent(&req.session_id, None).await?; + let active_run_id = self + .require_active_run(&req.session_id, &req.expected_run_id) + .await?; + + let message = Self::convert_acp_prompt_to_message(&req.prompt); + if message.content.is_empty() { + return Err(agent_client_protocol::Error::invalid_params() + .data("prompt must contain steerable content")); + } + + let message_id = format!("steer_{}", Uuid::new_v4()); + let message = message.with_id(message_id.clone()); + agent.steer(&req.session_id, message).await; + + if let Some(cx) = self.client_cx.get() { + let _ = Self::send_queued_steer_update( + cx, + &SessionId::new(req.session_id.clone()), + &message_id, + &active_run_id, + ); + } + + Ok(SteerSessionResponse { + run_id: active_run_id, + message_id, + }) + } + async fn on_cancel( &self, args: CancelNotification, @@ -3382,9 +3607,37 @@ print(\"hello, world\") ); } + #[test] + fn test_merge_replay_message_meta_includes_steer_marker() { + let message = Message::new(Role::User, 1_700_000_000, vec![]) + .with_id("msg_steer") + .with_steer(); + + let merged = merge_replay_message_meta(None, &message); + + assert_eq!( + merged.get("goose"), + Some(&serde_json::json!({ + "created": 1_700_000_000, + "messageId": "msg_steer", + "steer": true, + })), + "replay must carry the steer marker so the boundary survives reload" + ); + } + + #[test] + fn test_merge_replay_message_meta_omits_steer_when_not_set() { + let message = Message::new(Role::Assistant, 1_700_000_000, vec![]).with_id("msg_plain"); + + let merged = merge_replay_message_meta(None, &message); + + assert_eq!(merged.get("goose").and_then(|g| g.get("steer")), None); + } + #[test] fn test_message_update_meta_includes_created_and_message_id() { - let meta = message_update_meta(Some("msg_live"), 1_700_000_000); + let meta = message_update_meta(Some("msg_live"), 1_700_000_000, false); assert_eq!( meta.get("goose"), diff --git a/crates/goose/src/acp/server/custom_dispatch.rs b/crates/goose/src/acp/server/custom_dispatch.rs index fe0df9040662..91c1377cfcee 100644 --- a/crates/goose/src/acp/server/custom_dispatch.rs +++ b/crates/goose/src/acp/server/custom_dispatch.rs @@ -67,6 +67,14 @@ impl GooseAcpAgent { self.on_set_session_system_prompt(req).await } + #[custom_method(SteerSessionRequest)] + async fn dispatch_steer_session( + &self, + req: SteerSessionRequest, + ) -> Result { + self.on_steer_session(req).await + } + #[custom_method(DeleteSessionRequest)] async fn dispatch_delete_session( &self, diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index c77b39b1a2cb..07d34995b5cc 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::fmt; use std::future::Future; use std::pin::Pin; @@ -247,6 +247,7 @@ pub struct Agent { container: Mutex>, goal: Mutex>, grind: Mutex>, + pending_steers: Mutex>>, } #[derive(Clone, Debug)] @@ -368,6 +369,7 @@ impl Agent { container: Mutex::new(None), goal: Mutex::new(None), grind: Mutex::new(None), + pending_steers: Mutex::new(HashMap::new()), } } @@ -403,6 +405,36 @@ impl Agent { .await; } + pub async fn steer(&self, session_id: &str, message: Message) { + self.pending_steers + .lock() + .await + .entry(session_id.to_string()) + .or_default() + .push_back(message); + } + + pub async fn discard_pending_steers(&self, session_id: &str) { + self.pending_steers.lock().await.remove(session_id); + } + + async fn has_pending_steers(&self, session_id: &str) -> bool { + self.pending_steers + .lock() + .await + .get(session_id) + .is_some_and(|messages| !messages.is_empty()) + } + + async fn drain_pending_steers(&self, session_id: &str) -> Vec { + self.pending_steers + .lock() + .await + .remove(session_id) + .map(|messages| messages.into_iter().map(Message::with_steer).collect()) + .unwrap_or_default() + } + async fn emit_pre_tool_extended_hooks( &self, tool_name: &str, @@ -1703,12 +1735,35 @@ impl Agent { let mut retrying_after_stop_hook_denial = false; let mut consecutive_stop_hook_blocks = 0u32; let stop_hook_block_cap = self.stop_hook_block_cap(); + let mut can_drain_pending_steers = false; loop { if is_token_cancelled(&cancel_token) { break; } + if can_drain_pending_steers { + for message in self.drain_pending_steers(&session_config.id).await { + let message_text = message.as_concat_text(); + if self + .hook_manager + .has_hooks(crate::hooks::HookEvent::UserPromptSubmit) + { + let ctx = crate::hooks::HookContext::new( + crate::hooks::HookEvent::UserPromptSubmit, + &session_config.id, + ) + .with_message(message_text); + self.hook_manager + .emit(crate::hooks::HookEvent::UserPromptSubmit, ctx) + .await; + } + session_manager.add_message(&session_config.id, &message).await?; + conversation.push(message.clone()); + yield AgentEvent::Message(message); + } + } + let final_output = { let mut guard = self.final_output_tool.lock().await; guard.as_mut().and_then(|fot| fot.final_output.take()) @@ -2213,6 +2268,8 @@ impl Agent { } } } + can_drain_pending_steers = true; + if tools_updated { (tools, toolshim_tools, system_prompt) = self.prepare_tools_and_prompt(&session_config.id, &session.working_dir).await?; @@ -2252,6 +2309,7 @@ impl Agent { None if did_recovery_compact_this_iteration => { // continue from last user message after recovery compact } + None if self.has_pending_steers(&session_config.id).await => {} None if self.goal.lock().await.is_some() && !goal_check_pending => { goal_check_pending = true; let goal = self.goal.lock().await.clone().unwrap(); @@ -2375,6 +2433,10 @@ impl Agent { } conversation.extend(messages_to_add); + if exit_chat && self.has_pending_steers(&session_config.id).await { + exit_chat = false; + } + if exit_chat { let ctx = crate::hooks::HookContext::new( crate::hooks::HookEvent::Stop, @@ -3412,6 +3474,25 @@ exit 0 Ok(()) } + #[tokio::test] + async fn discard_pending_steers_clears_queued_messages() { + let agent = Agent::new(); + let session_id = "session-discard"; + + agent + .steer(session_id, Message::user().with_text("queued steer")) + .await; + assert!(agent.has_pending_steers(session_id).await); + + agent.discard_pending_steers(session_id).await; + + assert!( + !agent.has_pending_steers(session_id).await, + "discarding must drop steers orphaned by a cancelled run so they cannot leak into a later prompt" + ); + assert!(agent.drain_pending_steers(session_id).await.is_empty()); + } + #[test] fn categorize_tool_recognizes_conventional_names() { assert_eq!(categorize_tool("developer__shell"), ToolCategory::Shell); diff --git a/crates/goose/tests/acp_custom_requests_test.rs b/crates/goose/tests/acp_custom_requests_test.rs index c70f83381ed7..5b8a336b9f67 100644 --- a/crates/goose/tests/acp_custom_requests_test.rs +++ b/crates/goose/tests/acp_custom_requests_test.rs @@ -2,6 +2,9 @@ #[path = "acp_common_tests/mod.rs"] mod common_tests; +use agent_client_protocol::schema::{ + ContentBlock, PromptRequest, SessionUpdate, StopReason, TextContent, +}; use common_tests::fixtures::server::AcpServerConnection; use common_tests::fixtures::{ run_test, send_custom, Connection, PermissionDecision, Session, SessionData, @@ -15,6 +18,7 @@ use goose_test_support::{EnforceSessionId, IgnoreSessionId}; use serial_test::serial; use std::path::PathBuf; use std::sync::{Arc, LazyLock, Mutex}; +use std::time::Duration; use common_tests::fixtures::OpenAiFixture; @@ -78,6 +82,88 @@ impl Provider for MockProvider { } } +fn active_run_id_from_update(update: &SessionUpdate) -> Option { + let SessionUpdate::SessionInfoUpdate(info) = update else { + return None; + }; + info.meta + .as_ref()? + .get("goose")? + .get("activeRunId")? + .as_str() + .map(ToString::to_string) +} + +fn queued_steer_message_ids(updates: &[SessionUpdate]) -> Vec { + updates + .iter() + .filter_map(|update| { + let SessionUpdate::SessionInfoUpdate(info) = update else { + return None; + }; + info.meta + .as_ref()? + .get("goose")? + .get("queuedSteer")? + .get("messageId")? + .as_str() + .map(ToString::to_string) + }) + .collect() +} + +fn steer_chunk_message_ids(updates: &[SessionUpdate]) -> Vec { + updates + .iter() + .filter_map(|update| { + let SessionUpdate::UserMessageChunk(chunk) = update else { + return None; + }; + let goose = chunk.meta.as_ref()?.get("goose")?; + goose.get("steer")?.as_bool().filter(|b| *b)?; + goose.get("messageId")?.as_str().map(ToString::to_string) + }) + .collect() +} + +fn steer_chunk_texts(updates: &[SessionUpdate]) -> Vec { + updates + .iter() + .filter_map(|update| { + // A steered message is a user message injected mid-run, so it must + // arrive as a UserMessageChunk (matching the replay path), never an + // AgentMessageChunk. + let SessionUpdate::UserMessageChunk(chunk) = update else { + return None; + }; + let ContentBlock::Text(text) = &chunk.content else { + return None; + }; + let is_steer = chunk + .meta + .as_ref() + .and_then(|m| m.get("goose")) + .and_then(|g| g.get("steer")) + .and_then(|s| s.as_bool()) + .unwrap_or(false); + is_steer.then(|| text.text.clone()) + }) + .collect() +} + +fn collect_agent_text(updates: &[SessionUpdate]) -> String { + updates + .iter() + .filter_map(|update| match update { + SessionUpdate::AgentMessageChunk(chunk) => match &chunk.content { + ContentBlock::Text(text) => Some(text.text.as_str()), + _ => None, + }, + _ => None, + }) + .collect() +} + #[test] #[serial] fn test_custom_get_tools() { @@ -273,6 +359,121 @@ fn test_custom_get_available_extensions() { }); } +#[test] +#[serial] +fn test_steer_session_adds_input_to_active_prompt() { + write_acp_global_config(DEFAULT_ACP_TEST_CONFIG); + run_test(async move { + // Two-turn exchange: the first turn ends the turn with plain text. A + // steer queued before the turn ends keeps the loop alive (it flips + // `exit_chat` back to false), so a second provider request fires whose + // body must now contain the steered text. + let openai = OpenAiFixture::new( + vec![ + ( + "start work".to_string(), + include_str!("acp_test_data/openai_steer_first.txt"), + ), + ( + "steer while active".to_string(), + include_str!("acp_test_data/openai_steer_second.txt"), + ), + ], + Arc::new(IgnoreSessionId), + ) + .await; + let mut conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await; + + let SessionData { session, .. } = conn.new_session().await.unwrap(); + let session_id = session.session_id().0.to_string(); + let acp_session_id = session.session_id().clone(); + + let mut prompt = Box::pin( + conn.cx() + .send_request(PromptRequest::new( + acp_session_id, + vec![ContentBlock::Text(TextContent::new("start work"))], + )) + .block_task(), + ); + let mut steer_sent = false; + let mut steer_message_id: Option = None; + let mut final_response = None; + let deadline = tokio::time::Instant::now() + Duration::from_secs(3); + + while tokio::time::Instant::now() < deadline { + tokio::select! { + response = &mut prompt => { + final_response = Some(response.unwrap()); + break; + } + _ = tokio::time::sleep(Duration::from_millis(10)), if !steer_sent => { + let updates = session.session_updates(); + if let Some(run_id) = updates.iter().find_map(active_run_id_from_update) { + let response = send_custom( + conn.cx(), + "_goose/unstable/session/steer", + serde_json::json!({ + "sessionId": session_id, + "expectedRunId": run_id, + "prompt": [ + { "type": "text", "text": "steer while active" } + ] + }), + ) + .await + .unwrap(); + assert_eq!(response["runId"], run_id); + let mid = response["messageId"].as_str(); + assert!( + mid.is_some_and(|id| !id.is_empty()), + "steer response must return a messageId for correlation, got: {response:?}" + ); + steer_message_id = mid.map(ToString::to_string); + steer_sent = true; + } + } + } + } + + let response = final_response.expect("prompt did not complete"); + assert_eq!(response.stop_reason, StopReason::EndTurn); + assert!(steer_sent, "test never observed an active run id"); + + let updates = session.session_updates(); + let agent_text = collect_agent_text(&updates); + assert!( + agent_text.contains("saw steer"), + "expected provider to receive steered input, got: {agent_text:?}" + ); + + // The echoed steer prompt must be marked structurally so the client + // can locate the boundary without matching user-visible text. + let steer_chunks = steer_chunk_texts(&updates); + assert!( + steer_chunks + .iter() + .any(|t| t.contains("steer while active")), + "expected a chunk marked _meta.goose.steer with the steer text, got: {steer_chunks:?}" + ); + + // The queued steer must be announced (so a UI can show it as pending) + // and carry the same messageId returned by the steer response and later + // stamped on the picked-up UserMessageChunk. + let steer_message_id = steer_message_id.expect("steer response had no messageId"); + let queued_ids = queued_steer_message_ids(&updates); + assert!( + queued_ids.contains(&steer_message_id), + "expected a queuedSteer SessionInfoUpdate with messageId {steer_message_id:?}, got: {queued_ids:?}" + ); + let picked_up_ids = steer_chunk_message_ids(&updates); + assert!( + picked_up_ids.contains(&steer_message_id), + "picked-up steer chunk must carry the queued messageId {steer_message_id:?} for correlation, got: {picked_up_ids:?}" + ); + }); +} + #[test] #[serial] fn test_custom_list_builtin_skill_sources() { diff --git a/crates/goose/tests/acp_fixtures/mod.rs b/crates/goose/tests/acp_fixtures/mod.rs index 55370be5dbd0..fd5677da7572 100644 --- a/crates/goose/tests/acp_fixtures/mod.rs +++ b/crates/goose/tests/acp_fixtures/mod.rs @@ -302,6 +302,13 @@ pub fn to_notifications(updates: &[SessionUpdate]) -> Vec { SessionUpdate::ConfigOptionUpdate(_) => out.push(Notification::ConfigOption), SessionUpdate::SessionInfoUpdate(update) => { let meta = update.meta.as_ref(); + let is_active_run_update = meta + .and_then(|m| m.get("goose")) + .and_then(|g| g.get("activeRunId")) + .is_some(); + if is_active_run_update { + continue; + } out.push(Notification::SessionInfoUpdate { title: update.title.value().cloned(), updated_at: update.updated_at.value().cloned(), diff --git a/crates/goose/tests/acp_test_data/openai_steer_first.txt b/crates/goose/tests/acp_test_data/openai_steer_first.txt new file mode 100644 index 000000000000..0e0f156c10bc --- /dev/null +++ b/crates/goose/tests/acp_test_data/openai_steer_first.txt @@ -0,0 +1,9 @@ +data: {"id":"chatcmpl-steer1","object":"chat.completion.chunk","created":1766229303,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]} + +data: {"id":"chatcmpl-steer1","object":"chat.completion.chunk","created":1766229303,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":"first response"},"finish_reason":null}]} + +data: {"id":"chatcmpl-steer1","object":"chat.completion.chunk","created":1766229303,"model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} + +data: {"id":"chatcmpl-steer1","object":"chat.completion.chunk","created":1766229303,"model":"gpt-4o","choices":[],"usage":{"prompt_tokens":100,"completion_tokens":10,"total_tokens":110}} + +data: [DONE] diff --git a/crates/goose/tests/acp_test_data/openai_steer_second.txt b/crates/goose/tests/acp_test_data/openai_steer_second.txt new file mode 100644 index 000000000000..79d3b8a733d4 --- /dev/null +++ b/crates/goose/tests/acp_test_data/openai_steer_second.txt @@ -0,0 +1,9 @@ +data: {"id":"chatcmpl-steer2","object":"chat.completion.chunk","created":1766229304,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]} + +data: {"id":"chatcmpl-steer2","object":"chat.completion.chunk","created":1766229304,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":"saw steer"},"finish_reason":null}]} + +data: {"id":"chatcmpl-steer2","object":"chat.completion.chunk","created":1766229304,"model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} + +data: {"id":"chatcmpl-steer2","object":"chat.completion.chunk","created":1766229304,"model":"gpt-4o","choices":[],"usage":{"prompt_tokens":120,"completion_tokens":10,"total_tokens":130}} + +data: [DONE] diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 3bd3b51992e8..dce8ef10601a 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -6571,6 +6571,10 @@ ], "nullable": true }, + "steer": { + "type": "boolean", + "description": "Whether this message is a steer injected into an active run. UI-only:\nsurfaced as `_meta.goose.steer` so clients can mark the steer boundary\nwithout matching user-visible text. Never sent to providers." + }, "userVisible": { "type": "boolean", "description": "Whether the message should be visible to the user in the UI" diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index fda990138938..426dc28d3f54 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -770,6 +770,12 @@ export type MessageMetadata = { */ agentVisible: boolean; inference?: InferenceMetadata | null; + /** + * Whether this message is a steer injected into an active run. UI-only: + * surfaced as `_meta.goose.steer` so clients can mark the steer boundary + * without matching user-visible text. Never sent to providers. + */ + steer?: boolean; /** * Whether the message should be visible to the user in the UI */ diff --git a/ui/sdk/src/generated/client.gen.ts b/ui/sdk/src/generated/client.gen.ts index a11a411cd94c..27a00c33eeb8 100644 --- a/ui/sdk/src/generated/client.gen.ts +++ b/ui/sdk/src/generated/client.gen.ts @@ -98,6 +98,8 @@ import type { RenameSessionRequest_unstable, SetConfigExtensionEnabledRequest_unstable, SetSessionSystemPromptRequest_unstable, + SteerSessionRequest_unstable, + SteerSessionResponse_unstable, UnarchiveSessionRequest_unstable, UpdateSessionProjectRequest_unstable, UpdateSourceRequest_unstable, @@ -139,6 +141,7 @@ import { zProviderSupportedModelsListResponse_unstable, zReadResourceResponse_unstable, zRefreshProviderInventoryResponse_unstable, + zSteerSessionResponse_unstable, zUpdateSourceResponse_unstable, } from './zod.gen.js'; @@ -206,6 +209,18 @@ export class GooseExtClient { ); } + async sessionSteer_unstable( + params: SteerSessionRequest_unstable, + ): Promise { + const raw = await this.conn.extMethod( + "_goose/unstable/session/steer", + params, + ); + return zSteerSessionResponse_unstable.parse( + raw, + ) as SteerSessionResponse_unstable; + } + async sessionDelete(params: DeleteSessionRequest): Promise { await this.conn.extMethod("session/delete", params); } diff --git a/ui/sdk/src/generated/index.ts b/ui/sdk/src/generated/index.ts index ea3d436b478d..6ae520e74860 100644 --- a/ui/sdk/src/generated/index.ts +++ b/ui/sdk/src/generated/index.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -export type { AddConfigExtensionRequest_unstable, AddExtensionRequest_unstable, ArchiveSessionRequest_unstable, CreateSourceRequest_unstable, CreateSourceResponse_unstable, CustomProviderConfigDto, CustomProviderCreateRequest_unstable, CustomProviderCreateResponse_unstable, CustomProviderDeleteRequest_unstable, CustomProviderDeleteResponse_unstable, CustomProviderReadRequest_unstable, CustomProviderReadResponse_unstable, CustomProviderUpdateRequest_unstable, CustomProviderUpdateResponse_unstable, DefaultsReadRequest_unstable, DefaultsReadResponse_unstable, DefaultsSaveRequest_unstable, DeleteSessionRequest, DeleteSourceRequest_unstable, DictationConfigRequest_unstable, DictationConfigResponse_unstable, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest_unstable, DictationModelDeleteRequest_unstable, DictationModelDownloadProgressRequest_unstable, DictationModelDownloadProgressResponse_unstable, DictationModelDownloadRequest_unstable, DictationModelOption, DictationModelSelectRequest_unstable, DictationModelsListRequest_unstable, DictationModelsListResponse_unstable, DictationProviderStatusEntry, DictationSecretDeleteRequest_unstable, DictationSecretSaveRequest_unstable, DictationTranscribeRequest_unstable, DictationTranscribeResponse_unstable, ElicitationRespondRequest_unstable, EmptyResponse, EnvVariable, ExportSessionRequest_unstable, ExportSessionResponse_unstable, ExportSourceRequest_unstable, ExportSourceResponse_unstable, ExtNotification, ExtRequest, ExtResponse, GetAvailableExtensionsRequest_unstable, GetAvailableExtensionsResponse_unstable, GetConfigExtensionsRequest_unstable, GetConfigExtensionsResponse_unstable, GetSessionExtensionsRequest_unstable, GetSessionExtensionsResponse_unstable, GetToolsRequest_unstable, GetToolsResponse_unstable, GooseExtension, GooseExtensionEntry, GooseSessionNotification_unstable, GooseSessionUpdate, GooseToolCallRequest_unstable, GooseToolCallResponse_unstable, HttpHeader, ImportSessionRequest_unstable, ImportSessionResponse_unstable, ImportSourcesRequest_unstable, ImportSourcesResponse_unstable, Interaction, InteractionState, InteractionUpdate, ListProvidersRequest_unstable, ListProvidersResponse_unstable, ListSourcesRequest_unstable, ListSourcesResponse_unstable, McpServer, McpServerHttp, McpServerSse, McpServerStdio, OnboardingImportApplyRequest_unstable, OnboardingImportApplyResponse_unstable, OnboardingImportCandidate, OnboardingImportCounts, OnboardingImportScanRequest_unstable, OnboardingImportScanResponse_unstable, OnboardingImportSourceKind, PreferenceKey, PreferencesReadRequest_unstable, PreferencesReadResponse_unstable, PreferencesRemoveRequest_unstable, PreferencesSaveRequest_unstable, PreferenceValue, ProviderCatalogListRequest_unstable, ProviderCatalogListResponse_unstable, ProviderCatalogTemplateRequest_unstable, ProviderCatalogTemplateResponse_unstable, ProviderConfigAuthenticateRequest_unstable, ProviderConfigChangeResponse_unstable, ProviderConfigDeleteRequest_unstable, ProviderConfigFieldUpdate, ProviderConfigFieldValueDto, ProviderConfigKey, ProviderConfigReadRequest_unstable, ProviderConfigReadResponse_unstable, ProviderConfigSaveRequest_unstable, ProviderConfigStatusDto, ProviderConfigStatusRequest_unstable, ProviderConfigStatusResponse_unstable, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderSetupCatalogEntryDto, ProviderSetupCatalogListRequest_unstable, ProviderSetupCatalogListResponse_unstable, ProviderSetupCategoryDto, ProviderSetupFieldDto, ProviderSetupGroupDto, ProviderSetupMethodDto, ProviderSupportedModelsListRequest_unstable, ProviderSupportedModelsListResponse_unstable, ProviderTemplateCapabilitiesDto, ProviderTemplateCatalogEntryDto, ProviderTemplateDto, ProviderTemplateModelDto, ReadResourceRequest_unstable, ReadResourceResponse_unstable, RefreshProviderInventoryRequest_unstable, RefreshProviderInventoryResponse_unstable, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest_unstable, RemoveExtensionRequest_unstable, RenameSessionRequest_unstable, SessionSystemPromptMode, SessionUsageUpdate, SetConfigExtensionEnabledRequest_unstable, SetSessionSystemPromptRequest_unstable, SourceEntry, SourceScope, SourceType, StatusMessage, StatusMessageUpdate, UnarchiveSessionRequest_unstable, UpdateSessionProjectRequest_unstable, UpdateSourceRequest_unstable, UpdateSourceResponse_unstable, UpdateWorkingDirRequest_unstable } from './types.gen.js'; +export type { AddConfigExtensionRequest_unstable, AddExtensionRequest_unstable, Annotations, ArchiveSessionRequest_unstable, AudioContent, BlobResourceContents, ContentBlock, CreateSourceRequest_unstable, CreateSourceResponse_unstable, CustomProviderConfigDto, CustomProviderCreateRequest_unstable, CustomProviderCreateResponse_unstable, CustomProviderDeleteRequest_unstable, CustomProviderDeleteResponse_unstable, CustomProviderReadRequest_unstable, CustomProviderReadResponse_unstable, CustomProviderUpdateRequest_unstable, CustomProviderUpdateResponse_unstable, DefaultsReadRequest_unstable, DefaultsReadResponse_unstable, DefaultsSaveRequest_unstable, DeleteSessionRequest, DeleteSourceRequest_unstable, DictationConfigRequest_unstable, DictationConfigResponse_unstable, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest_unstable, DictationModelDeleteRequest_unstable, DictationModelDownloadProgressRequest_unstable, DictationModelDownloadProgressResponse_unstable, DictationModelDownloadRequest_unstable, DictationModelOption, DictationModelSelectRequest_unstable, DictationModelsListRequest_unstable, DictationModelsListResponse_unstable, DictationProviderStatusEntry, DictationSecretDeleteRequest_unstable, DictationSecretSaveRequest_unstable, DictationTranscribeRequest_unstable, DictationTranscribeResponse_unstable, ElicitationRespondRequest_unstable, EmbeddedResource, EmbeddedResourceResource, EmptyResponse, EnvVariable, ExportSessionRequest_unstable, ExportSessionResponse_unstable, ExportSourceRequest_unstable, ExportSourceResponse_unstable, ExtNotification, ExtRequest, ExtResponse, GetAvailableExtensionsRequest_unstable, GetAvailableExtensionsResponse_unstable, GetConfigExtensionsRequest_unstable, GetConfigExtensionsResponse_unstable, GetSessionExtensionsRequest_unstable, GetSessionExtensionsResponse_unstable, GetToolsRequest_unstable, GetToolsResponse_unstable, GooseExtension, GooseExtensionEntry, GooseSessionNotification_unstable, GooseSessionUpdate, GooseToolCallRequest_unstable, GooseToolCallResponse_unstable, HttpHeader, ImageContent, ImportSessionRequest_unstable, ImportSessionResponse_unstable, ImportSourcesRequest_unstable, ImportSourcesResponse_unstable, Interaction, InteractionState, InteractionUpdate, ListProvidersRequest_unstable, ListProvidersResponse_unstable, ListSourcesRequest_unstable, ListSourcesResponse_unstable, McpServer, McpServerHttp, McpServerSse, McpServerStdio, OnboardingImportApplyRequest_unstable, OnboardingImportApplyResponse_unstable, OnboardingImportCandidate, OnboardingImportCounts, OnboardingImportScanRequest_unstable, OnboardingImportScanResponse_unstable, OnboardingImportSourceKind, PreferenceKey, PreferencesReadRequest_unstable, PreferencesReadResponse_unstable, PreferencesRemoveRequest_unstable, PreferencesSaveRequest_unstable, PreferenceValue, ProviderCatalogListRequest_unstable, ProviderCatalogListResponse_unstable, ProviderCatalogTemplateRequest_unstable, ProviderCatalogTemplateResponse_unstable, ProviderConfigAuthenticateRequest_unstable, ProviderConfigChangeResponse_unstable, ProviderConfigDeleteRequest_unstable, ProviderConfigFieldUpdate, ProviderConfigFieldValueDto, ProviderConfigKey, ProviderConfigReadRequest_unstable, ProviderConfigReadResponse_unstable, ProviderConfigSaveRequest_unstable, ProviderConfigStatusDto, ProviderConfigStatusRequest_unstable, ProviderConfigStatusResponse_unstable, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderSetupCatalogEntryDto, ProviderSetupCatalogListRequest_unstable, ProviderSetupCatalogListResponse_unstable, ProviderSetupCategoryDto, ProviderSetupFieldDto, ProviderSetupGroupDto, ProviderSetupMethodDto, ProviderSupportedModelsListRequest_unstable, ProviderSupportedModelsListResponse_unstable, ProviderTemplateCapabilitiesDto, ProviderTemplateCatalogEntryDto, ProviderTemplateDto, ProviderTemplateModelDto, ReadResourceRequest_unstable, ReadResourceResponse_unstable, RefreshProviderInventoryRequest_unstable, RefreshProviderInventoryResponse_unstable, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest_unstable, RemoveExtensionRequest_unstable, RenameSessionRequest_unstable, ResourceLink, Role, SessionSystemPromptMode, SessionUsageUpdate, SetConfigExtensionEnabledRequest_unstable, SetSessionSystemPromptRequest_unstable, SourceEntry, SourceScope, SourceType, StatusMessage, StatusMessageUpdate, SteerSessionRequest_unstable, SteerSessionResponse_unstable, TextContent, TextResourceContents, UnarchiveSessionRequest_unstable, UpdateSessionProjectRequest_unstable, UpdateSourceRequest_unstable, UpdateSourceResponse_unstable, UpdateWorkingDirRequest_unstable } from './types.gen.js'; export const GOOSE_EXT_METHODS = [ { @@ -38,6 +38,11 @@ export const GOOSE_EXT_METHODS = [ requestType: "SetSessionSystemPromptRequest_unstable", responseType: "EmptyResponse", }, + { + method: "_goose/unstable/session/steer", + requestType: "SteerSessionRequest_unstable", + responseType: "SteerSessionResponse_unstable", + }, { method: "session/delete", requestType: "DeleteSessionRequest", diff --git a/ui/sdk/src/generated/types.gen.ts b/ui/sdk/src/generated/types.gen.ts index 3dfe18d0cb15..62dea2d35c77 100644 --- a/ui/sdk/src/generated/types.gen.ts +++ b/ui/sdk/src/generated/types.gen.ts @@ -109,6 +109,218 @@ export type SetSessionSystemPromptRequest_unstable = { */ export type SessionSystemPromptMode = 'set' | 'append'; +/** + * Add user input to the currently active prompt without starting a new prompt. + */ +export type SteerSessionRequest_unstable = { + sessionId: string; + prompt?: Array; + expectedRunId: string; +}; + +/** + * Content blocks represent displayable information in the Agent Client Protocol. + * + * They provide a structured way to handle various types of user-facing content—whether + * it's text from language models, images for analysis, or embedded resources for context. + * + * Content blocks appear in: + * - User prompts sent via `session/prompt` + * - Language model output streamed through `session/update` notifications + * - Progress updates and results from tool calls + * + * This structure is compatible with the Model Context Protocol (MCP), enabling + * agents to seamlessly forward content from MCP tool outputs without transformation. + * + * See protocol docs: [Content](https://agentclientprotocol.com/protocol/content) + */ +export type ContentBlock = ({ + type: 'TextContent'; +} & TextContent) | ({ + type: 'ImageContent'; +} & ImageContent) | ({ + type: 'AudioContent'; +} & AudioContent) | ({ + type: 'ResourceLink'; +} & ResourceLink) | ({ + type: 'EmbeddedResource'; +} & EmbeddedResource); + +/** + * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed + */ +export type Annotations = { + audience?: Array | null; + lastModified?: string | null; + priority?: number | null; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; +}; + +/** + * The sender or recipient of messages and data in a conversation. + */ +export type Role = 'assistant' | 'user'; + +/** + * Text provided to or from an LLM. + */ +export type TextContent = { + annotations?: Annotations | null; + text: string; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; +}; + +/** + * An image provided to or from an LLM. + */ +export type ImageContent = { + annotations?: Annotations | null; + data: string; + mimeType: string; + uri?: string | null; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; +}; + +/** + * Audio provided to or from an LLM. + */ +export type AudioContent = { + annotations?: Annotations | null; + data: string; + mimeType: string; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; +}; + +/** + * A resource that the server is capable of reading, included in a prompt or tool call result. + */ +export type ResourceLink = { + annotations?: Annotations | null; + description?: string | null; + mimeType?: string | null; + name: string; + size?: number | null; + title?: string | null; + uri: string; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; +}; + +/** + * Resource content that can be embedded in a message. + */ +export type EmbeddedResourceResource = TextResourceContents | BlobResourceContents; + +/** + * Text-based resource contents. + */ +export type TextResourceContents = { + mimeType?: string | null; + text: string; + uri: string; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; +}; + +/** + * Binary resource contents. + */ +export type BlobResourceContents = { + blob: string; + mimeType?: string | null; + uri: string; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; +}; + +/** + * The contents of a resource, embedded into a prompt or tool call result. + */ +export type EmbeddedResource = { + annotations?: Annotations | null; + resource: EmbeddedResourceResource; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; +}; + +export type SteerSessionResponse_unstable = { + runId: string; + /** + * Stable id of the queued steer message. The same id later appears as + * `messageId` on the streamed `UserMessageChunk` (with `_meta.goose.steer`), + * letting clients correlate a queued steer with its pickup. + */ + messageId: string; +}; + /** * Delete a session. */ @@ -1345,14 +1557,14 @@ export type InteractionUpdate = { export type ExtRequest = { id: string; method: string; - params?: AddExtensionRequest_unstable | RemoveExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | ElicitationRespondRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | { + params?: AddExtensionRequest_unstable | RemoveExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | SteerSessionRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | ElicitationRespondRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | { [key: string]: unknown; } | null; }; export type ExtResponse = { id: string; - result?: EmptyResponse | GetToolsResponse_unstable | GooseToolCallResponse_unstable | ReadResourceResponse_unstable | GetConfigExtensionsResponse_unstable | GetAvailableExtensionsResponse_unstable | GetSessionExtensionsResponse_unstable | ListProvidersResponse_unstable | ProviderSupportedModelsListResponse_unstable | ProviderCatalogListResponse_unstable | ProviderSetupCatalogListResponse_unstable | ProviderCatalogTemplateResponse_unstable | CustomProviderCreateResponse_unstable | CustomProviderReadResponse_unstable | CustomProviderUpdateResponse_unstable | CustomProviderDeleteResponse_unstable | RefreshProviderInventoryResponse_unstable | ProviderConfigReadResponse_unstable | ProviderConfigStatusResponse_unstable | ProviderConfigChangeResponse_unstable | PreferencesReadResponse_unstable | DefaultsReadResponse_unstable | OnboardingImportScanResponse_unstable | OnboardingImportApplyResponse_unstable | ExportSessionResponse_unstable | ImportSessionResponse_unstable | CreateSourceResponse_unstable | ListSourcesResponse_unstable | UpdateSourceResponse_unstable | ExportSourceResponse_unstable | ImportSourcesResponse_unstable | DictationTranscribeResponse_unstable | DictationConfigResponse_unstable | DictationModelsListResponse_unstable | DictationModelDownloadProgressResponse_unstable | unknown; + result?: EmptyResponse | GetToolsResponse_unstable | GooseToolCallResponse_unstable | ReadResourceResponse_unstable | SteerSessionResponse_unstable | GetConfigExtensionsResponse_unstable | GetAvailableExtensionsResponse_unstable | GetSessionExtensionsResponse_unstable | ListProvidersResponse_unstable | ProviderSupportedModelsListResponse_unstable | ProviderCatalogListResponse_unstable | ProviderSetupCatalogListResponse_unstable | ProviderCatalogTemplateResponse_unstable | CustomProviderCreateResponse_unstable | CustomProviderReadResponse_unstable | CustomProviderUpdateResponse_unstable | CustomProviderDeleteResponse_unstable | RefreshProviderInventoryResponse_unstable | ProviderConfigReadResponse_unstable | ProviderConfigStatusResponse_unstable | ProviderConfigChangeResponse_unstable | PreferencesReadResponse_unstable | DefaultsReadResponse_unstable | OnboardingImportScanResponse_unstable | OnboardingImportApplyResponse_unstable | ExportSessionResponse_unstable | ImportSessionResponse_unstable | CreateSourceResponse_unstable | ListSourcesResponse_unstable | UpdateSourceResponse_unstable | ExportSourceResponse_unstable | ImportSourcesResponse_unstable | DictationTranscribeResponse_unstable | DictationConfigResponse_unstable | DictationModelsListResponse_unstable | DictationModelDownloadProgressResponse_unstable | unknown; } | { error: { code: number; diff --git a/ui/sdk/src/generated/zod.gen.ts b/ui/sdk/src/generated/zod.gen.ts index 24e0f637b189..e40fb2d73701 100644 --- a/ui/sdk/src/generated/zod.gen.ts +++ b/ui/sdk/src/generated/zod.gen.ts @@ -105,6 +105,219 @@ export const zSetSessionSystemPromptRequest_unstable = z.object({ text: z.string() }); +/** + * The sender or recipient of messages and data in a conversation. + */ +export const zRole = z.enum(['assistant', 'user']); + +/** + * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed + */ +export const zAnnotations = z.object({ + audience: z.union([ + z.array(zRole), + z.null() + ]).optional(), + lastModified: z.union([ + z.string(), + z.null() + ]).optional(), + priority: z.union([ + z.number(), + z.null() + ]).optional(), + _meta: z.union([ + z.record(z.unknown()), + z.null() + ]).optional() +}); + +/** + * Text provided to or from an LLM. + */ +export const zTextContent = z.object({ + annotations: z.union([ + zAnnotations, + z.null() + ]).optional(), + text: z.string(), + _meta: z.union([ + z.record(z.unknown()), + z.null() + ]).optional() +}); + +/** + * An image provided to or from an LLM. + */ +export const zImageContent = z.object({ + annotations: z.union([ + zAnnotations, + z.null() + ]).optional(), + data: z.string(), + mimeType: z.string(), + uri: z.union([ + z.string(), + z.null() + ]).optional(), + _meta: z.union([ + z.record(z.unknown()), + z.null() + ]).optional() +}); + +/** + * Audio provided to or from an LLM. + */ +export const zAudioContent = z.object({ + annotations: z.union([ + zAnnotations, + z.null() + ]).optional(), + data: z.string(), + mimeType: z.string(), + _meta: z.union([ + z.record(z.unknown()), + z.null() + ]).optional() +}); + +/** + * A resource that the server is capable of reading, included in a prompt or tool call result. + */ +export const zResourceLink = z.object({ + annotations: z.union([ + zAnnotations, + z.null() + ]).optional(), + description: z.union([ + z.string(), + z.null() + ]).optional(), + mimeType: z.union([ + z.string(), + z.null() + ]).optional(), + name: z.string(), + size: z.union([ + z.number().int(), + z.null() + ]).optional(), + title: z.union([ + z.string(), + z.null() + ]).optional(), + uri: z.string(), + _meta: z.union([ + z.record(z.unknown()), + z.null() + ]).optional() +}); + +/** + * Text-based resource contents. + */ +export const zTextResourceContents = z.object({ + mimeType: z.union([ + z.string(), + z.null() + ]).optional(), + text: z.string(), + uri: z.string(), + _meta: z.union([ + z.record(z.unknown()), + z.null() + ]).optional() +}); + +/** + * Binary resource contents. + */ +export const zBlobResourceContents = z.object({ + blob: z.string(), + mimeType: z.union([ + z.string(), + z.null() + ]).optional(), + uri: z.string(), + _meta: z.union([ + z.record(z.unknown()), + z.null() + ]).optional() +}); + +/** + * Resource content that can be embedded in a message. + */ +export const zEmbeddedResourceResource = z.union([ + zTextResourceContents, + zBlobResourceContents +]); + +/** + * The contents of a resource, embedded into a prompt or tool call result. + */ +export const zEmbeddedResource = z.object({ + annotations: z.union([ + zAnnotations, + z.null() + ]).optional(), + resource: zEmbeddedResourceResource, + _meta: z.union([ + z.record(z.unknown()), + z.null() + ]).optional() +}); + +/** + * Content blocks represent displayable information in the Agent Client Protocol. + * + * They provide a structured way to handle various types of user-facing content—whether + * it's text from language models, images for analysis, or embedded resources for context. + * + * Content blocks appear in: + * - User prompts sent via `session/prompt` + * - Language model output streamed through `session/update` notifications + * - Progress updates and results from tool calls + * + * This structure is compatible with the Model Context Protocol (MCP), enabling + * agents to seamlessly forward content from MCP tool outputs without transformation. + * + * See protocol docs: [Content](https://agentclientprotocol.com/protocol/content) + */ +export const zContentBlock = z.union([ + z.object({ + type: z.literal('TextContent') + }).and(zTextContent), + z.object({ + type: z.literal('ImageContent') + }).and(zImageContent), + z.object({ + type: z.literal('AudioContent') + }).and(zAudioContent), + z.object({ + type: z.literal('ResourceLink') + }).and(zResourceLink), + z.object({ + type: z.literal('EmbeddedResource') + }).and(zEmbeddedResource) +]); + +/** + * Add user input to the currently active prompt without starting a new prompt. + */ +export const zSteerSessionRequest_unstable = z.object({ + sessionId: z.string(), + prompt: z.array(zContentBlock).optional().default([]), + expectedRunId: z.string() +}); + +export const zSteerSessionResponse_unstable = z.object({ + runId: z.string(), + messageId: z.string() +}); + /** * Delete a session. */ @@ -1347,6 +1560,7 @@ export const zExtRequest = z.object({ zReadResourceRequest_unstable, zUpdateWorkingDirRequest_unstable, zSetSessionSystemPromptRequest_unstable, + zSteerSessionRequest_unstable, zDeleteSessionRequest, zGetConfigExtensionsRequest_unstable, zGetAvailableExtensionsRequest_unstable, @@ -1416,6 +1630,7 @@ export const zExtResponse = z.union([ zGetToolsResponse_unstable, zGooseToolCallResponse_unstable, zReadResourceResponse_unstable, + zSteerSessionResponse_unstable, zGetConfigExtensionsResponse_unstable, zGetAvailableExtensionsResponse_unstable, zGetSessionExtensionsResponse_unstable,