feat: steering messages with ACP (#9560)
Signed-off-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
@@ -667,6 +667,11 @@ pub struct MessageMetadata {
|
||||
pub agent_visible: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub inference: Option<InferenceMetadata>,
|
||||
/// 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)
|
||||
|
||||
@@ -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<ContentBlock>,
|
||||
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)]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
+290
-37
@@ -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<String>,
|
||||
cancel_token: Option<CancellationToken>,
|
||||
active_run_id: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<Agent>,
|
||||
session: &mut GooseAcpSession,
|
||||
cx: &ConnectionTo<Client>,
|
||||
) -> 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<String, serde
|
||||
if let Some(id) = &message.id {
|
||||
goose.insert("messageId".to_string(), serde_json::json!(id));
|
||||
}
|
||||
if message.metadata.steer {
|
||||
goose.insert("steer".to_string(), serde_json::json!(true));
|
||||
}
|
||||
goose
|
||||
}
|
||||
|
||||
@@ -2208,6 +2224,127 @@ impl GooseAcpAgent {
|
||||
Ok(agent)
|
||||
}
|
||||
|
||||
async fn start_active_run(
|
||||
&self,
|
||||
session_id: &str,
|
||||
run_id: String,
|
||||
cancel_token: CancellationToken,
|
||||
) -> 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<String, agent_client_protocol::Error> {
|
||||
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<Client>,
|
||||
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<Client>,
|
||||
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<Agent>,
|
||||
@@ -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<SteerSessionResponse, agent_client_protocol::Error> {
|
||||
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"),
|
||||
|
||||
@@ -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<SteerSessionResponse, agent_client_protocol::Error> {
|
||||
self.on_steer_session(req).await
|
||||
}
|
||||
|
||||
#[custom_method(DeleteSessionRequest)]
|
||||
async fn dispatch_delete_session(
|
||||
&self,
|
||||
|
||||
@@ -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<Option<Container>>,
|
||||
goal: Mutex<Option<String>>,
|
||||
grind: Mutex<Option<String>>,
|
||||
pending_steers: Mutex<HashMap<String, VecDeque<Message>>>,
|
||||
}
|
||||
|
||||
#[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<Message> {
|
||||
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);
|
||||
|
||||
@@ -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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> = 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() {
|
||||
|
||||
@@ -302,6 +302,13 @@ pub fn to_notifications(updates: &[SessionUpdate]) -> Vec<Notification> {
|
||||
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(),
|
||||
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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<SteerSessionResponse_unstable> {
|
||||
const raw = await this.conn.extMethod(
|
||||
"_goose/unstable/session/steer",
|
||||
params,
|
||||
);
|
||||
return zSteerSessionResponse_unstable.parse(
|
||||
raw,
|
||||
) as SteerSessionResponse_unstable;
|
||||
}
|
||||
|
||||
async sessionDelete(params: DeleteSessionRequest): Promise<void> {
|
||||
await this.conn.extMethod("session/delete", params);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<ContentBlock>;
|
||||
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<Role> | 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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user