feat(acp): add reusable ACP provider controls (#8314)
Signed-off-by: Bradley Axen <baxen@squareup.com>
This commit is contained in:
Generated
+1
-1
@@ -4573,7 +4573,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "goose-sdk"
|
||||
version = "1.29.0"
|
||||
version = "1.30.0"
|
||||
dependencies = [
|
||||
"agent-client-protocol-schema",
|
||||
"sacp",
|
||||
|
||||
@@ -49,6 +49,46 @@
|
||||
"method": "_goose/config/extensions",
|
||||
"requestType": "GetExtensionsRequest",
|
||||
"responseType": "GetExtensionsResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/session/provider/update",
|
||||
"requestType": "UpdateProviderRequest",
|
||||
"responseType": "UpdateProviderResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/providers/list",
|
||||
"requestType": "ListProvidersRequest",
|
||||
"responseType": "ListProvidersResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/config/read",
|
||||
"requestType": "ReadConfigRequest",
|
||||
"responseType": "ReadConfigResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/config/upsert",
|
||||
"requestType": "UpsertConfigRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/config/remove",
|
||||
"requestType": "RemoveConfigRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/secret/check",
|
||||
"requestType": "CheckSecretRequest",
|
||||
"responseType": "CheckSecretResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/secret/upsert",
|
||||
"requestType": "UpsertSecretRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
},
|
||||
{
|
||||
"method": "_goose/secret/remove",
|
||||
"requestType": "RemoveSecretRequest",
|
||||
"responseType": "EmptyResponse"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"properties": {
|
||||
"tools": {
|
||||
"type": "array",
|
||||
"items": true,
|
||||
"items": {},
|
||||
"description": "Array of tool info objects with `name`, `description`, `parameters`, and optional `permission`."
|
||||
}
|
||||
},
|
||||
@@ -234,7 +234,7 @@
|
||||
"properties": {
|
||||
"extensions": {
|
||||
"type": "array",
|
||||
"items": true,
|
||||
"items": {},
|
||||
"description": "Array of ExtensionEntry objects with `enabled` flag and config details."
|
||||
},
|
||||
"warnings": {
|
||||
@@ -252,6 +252,212 @@
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/config/extensions"
|
||||
},
|
||||
"UpdateProviderRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"contextLimit": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint",
|
||||
"minimum": 0
|
||||
},
|
||||
"requestParams": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": {}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sessionId",
|
||||
"provider"
|
||||
],
|
||||
"description": "Atomically update the provider for a live session.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/session/provider/update"
|
||||
},
|
||||
"UpdateProviderResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configOptions": {
|
||||
"type": "array",
|
||||
"items": {},
|
||||
"description": "Refreshed session config options after the provider/model change."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"configOptions"
|
||||
],
|
||||
"description": "Provider update response.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/session/provider/update"
|
||||
},
|
||||
"ListProvidersRequest": {
|
||||
"type": "object",
|
||||
"description": "List providers available through goose, including the config-default sentinel.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/providers/list"
|
||||
},
|
||||
"ListProvidersResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"providers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ProviderListEntry"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"providers"
|
||||
],
|
||||
"description": "Provider list response.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/providers/list"
|
||||
},
|
||||
"ProviderListEntry": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"label"
|
||||
]
|
||||
},
|
||||
"ReadConfigRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"description": "Read a single non-secret config value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/config/read"
|
||||
},
|
||||
"ReadConfigResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"default": null
|
||||
}
|
||||
},
|
||||
"description": "Config read response.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/config/read"
|
||||
},
|
||||
"UpsertConfigRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {}
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"description": "Upsert a single non-secret config value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/config/upsert"
|
||||
},
|
||||
"RemoveConfigRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"description": "Remove a single non-secret config value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/config/remove"
|
||||
},
|
||||
"CheckSecretRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"description": "Check whether a secret exists. Never returns the actual value.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/secret/check"
|
||||
},
|
||||
"CheckSecretResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"exists": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"exists"
|
||||
],
|
||||
"description": "Secret check response.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/secret/check"
|
||||
},
|
||||
"UpsertSecretRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {}
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"description": "Set a secret value (write-only).",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/secret/upsert"
|
||||
},
|
||||
"RemoveSecretRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key"
|
||||
],
|
||||
"description": "Remove a secret.",
|
||||
"x-side": "agent",
|
||||
"x-method": "_goose/secret/remove"
|
||||
},
|
||||
"ExtRequest": {
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -353,6 +559,78 @@
|
||||
],
|
||||
"description": "Params for _goose/config/extensions",
|
||||
"title": "GetExtensionsRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/UpdateProviderRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/session/provider/update",
|
||||
"title": "UpdateProviderRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/ListProvidersRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/providers/list",
|
||||
"title": "ListProvidersRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/ReadConfigRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/config/read",
|
||||
"title": "ReadConfigRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/UpsertConfigRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/config/upsert",
|
||||
"title": "UpsertConfigRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/RemoveConfigRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/config/remove",
|
||||
"title": "RemoveConfigRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CheckSecretRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/secret/check",
|
||||
"title": "CheckSecretRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/UpsertSecretRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/secret/upsert",
|
||||
"title": "UpsertSecretRequest"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/RemoveSecretRequest"
|
||||
}
|
||||
],
|
||||
"description": "Params for _goose/secret/remove",
|
||||
"title": "RemoveSecretRequest"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -439,6 +717,38 @@
|
||||
}
|
||||
],
|
||||
"title": "GetExtensionsResponse"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/UpdateProviderResponse"
|
||||
}
|
||||
],
|
||||
"title": "UpdateProviderResponse"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/ListProvidersResponse"
|
||||
}
|
||||
],
|
||||
"title": "ListProvidersResponse"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/ReadConfigResponse"
|
||||
}
|
||||
],
|
||||
"title": "ReadConfigResponse"
|
||||
},
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/$defs/CheckSecretResponse"
|
||||
}
|
||||
],
|
||||
"title": "CheckSecretResponse"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -35,6 +35,13 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Replace `true` with `{}` throughout $defs. Both mean "accept any value" in
|
||||
// JSON Schema, but many TS codegen tools (e.g. @hey-api/openapi-ts Zod plugin)
|
||||
// silently drop properties whose schema is the bare `true` literal.
|
||||
for def in defs.values_mut() {
|
||||
replace_true_schemas(def);
|
||||
}
|
||||
|
||||
// Annotate $defs entries with x-method/x-side. Only set x-method for types
|
||||
// used by exactly one method (shared types like EmptyResponse skip x-method).
|
||||
for (name, methods_list) in &type_methods {
|
||||
@@ -181,3 +188,31 @@ fn main() {
|
||||
|
||||
println!("{json_str}");
|
||||
}
|
||||
|
||||
/// Recursively replace `true` with `{}` in a JSON value.
|
||||
///
|
||||
/// In JSON Schema, `true` and `{}` both mean "accept any value", but many
|
||||
/// TypeScript codegen tools only handle the object form.
|
||||
fn replace_true_schemas(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
for v in map.values_mut() {
|
||||
if *v == Value::Bool(true) {
|
||||
*v = json!({});
|
||||
} else {
|
||||
replace_true_schemas(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
for v in arr.iter_mut() {
|
||||
if *v == Value::Bool(true) {
|
||||
*v = json!({});
|
||||
} else {
|
||||
replace_true_schemas(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
+462
-25
@@ -3,7 +3,8 @@ use crate::fs::AcpTools;
|
||||
use crate::tools::AcpAwareToolMeta;
|
||||
use anyhow::Result;
|
||||
use fs_err as fs;
|
||||
use goose::acp::PermissionDecision;
|
||||
use futures::future::BoxFuture;
|
||||
use goose::acp::{PermissionDecision, ACP_CURRENT_MODEL};
|
||||
use goose::agents::extension::{Envs, PLATFORM_EXTENSIONS};
|
||||
use goose::agents::mcp_client::McpClientTrait;
|
||||
use goose::agents::platform_extensions::developer::DeveloperClient;
|
||||
@@ -20,9 +21,8 @@ use goose::mcp_utils::ToolResult;
|
||||
use goose::permission::permission_confirmation::PrincipalType;
|
||||
use goose::permission::{Permission, PermissionConfirmation};
|
||||
use goose::providers::base::Provider;
|
||||
use goose::providers::provider_registry::ProviderConstructor;
|
||||
use goose::session::session_manager::SessionType;
|
||||
use goose::session::{Session, SessionManager};
|
||||
use goose::session::{EnabledExtensionsState, Session, SessionManager};
|
||||
use goose_acp_macros::custom_methods;
|
||||
use rmcp::model::{CallToolResult, RawContent, ResourceContents, Role};
|
||||
use sacp::schema::{
|
||||
@@ -57,6 +57,19 @@ use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use url::Url;
|
||||
|
||||
pub type AcpProviderFactory = Arc<
|
||||
dyn Fn(
|
||||
String,
|
||||
goose::model::ModelConfig,
|
||||
Vec<ExtensionConfig>,
|
||||
) -> BoxFuture<'static, Result<Arc<dyn Provider>>>
|
||||
+ Send
|
||||
+ Sync,
|
||||
>;
|
||||
|
||||
const DEFAULT_PROVIDER_ID: &str = "goose";
|
||||
const DEFAULT_PROVIDER_LABEL: &str = "Goose (Default)";
|
||||
|
||||
struct GooseAcpSession {
|
||||
agent: Arc<Agent>,
|
||||
messages: Conversation,
|
||||
@@ -66,7 +79,7 @@ struct GooseAcpSession {
|
||||
|
||||
pub struct GooseAcpAgent {
|
||||
sessions: Arc<Mutex<HashMap<String, GooseAcpSession>>>,
|
||||
provider_factory: ProviderConstructor,
|
||||
provider_factory: AcpProviderFactory,
|
||||
builtins: Vec<String>,
|
||||
client_fs_capabilities: OnceCell<FileSystemCapabilities>,
|
||||
client_terminal: OnceCell<bool>,
|
||||
@@ -328,6 +341,56 @@ async fn build_model_state(provider: &dyn Provider) -> Result<SessionModelState,
|
||||
))
|
||||
}
|
||||
|
||||
async fn list_provider_entries(current_provider: Option<&str>) -> Vec<ProviderListEntry> {
|
||||
let mut providers = goose::providers::providers()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|(metadata, _)| ProviderListEntry {
|
||||
id: metadata.name,
|
||||
label: metadata.display_name,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
providers.sort_by(|left, right| left.id.cmp(&right.id));
|
||||
providers.dedup_by(|left, right| left.id == right.id);
|
||||
|
||||
if let Some(current_provider) = current_provider {
|
||||
if current_provider != DEFAULT_PROVIDER_ID
|
||||
&& !providers
|
||||
.iter()
|
||||
.any(|provider| provider.id == current_provider)
|
||||
{
|
||||
providers.push(ProviderListEntry {
|
||||
id: current_provider.to_string(),
|
||||
label: current_provider.to_string(),
|
||||
});
|
||||
providers.sort_by(|left, right| left.id.cmp(&right.id));
|
||||
}
|
||||
}
|
||||
|
||||
let mut entries = Vec::with_capacity(providers.len() + 1);
|
||||
entries.push(ProviderListEntry {
|
||||
id: DEFAULT_PROVIDER_ID.to_string(),
|
||||
label: DEFAULT_PROVIDER_LABEL.to_string(),
|
||||
});
|
||||
entries.extend(providers);
|
||||
entries
|
||||
}
|
||||
|
||||
async fn build_provider_options(current_provider: Option<&str>) -> Vec<SessionConfigSelectOption> {
|
||||
list_provider_entries(current_provider)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|provider| SessionConfigSelectOption::new(provider.id, provider.label))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn session_provider_selection(session: &Session) -> &str {
|
||||
session
|
||||
.provider_name
|
||||
.as_deref()
|
||||
.unwrap_or(DEFAULT_PROVIDER_ID)
|
||||
}
|
||||
|
||||
fn build_mode_state(current_mode: GooseMode) -> Result<SessionModeState, sacp::Error> {
|
||||
let mut available = Vec::with_capacity(GooseMode::VARIANTS.len());
|
||||
for &name in GooseMode::VARIANTS {
|
||||
@@ -348,6 +411,8 @@ fn build_mode_state(current_mode: GooseMode) -> Result<SessionModeState, sacp::E
|
||||
fn build_config_options(
|
||||
mode_state: &SessionModeState,
|
||||
model_state: &SessionModelState,
|
||||
provider_selection: &str,
|
||||
provider_options: Vec<SessionConfigSelectOption>,
|
||||
) -> Vec<SessionConfigOption> {
|
||||
let mode_options: Vec<SessionConfigSelectOption> = mode_state
|
||||
.available_modes
|
||||
@@ -363,6 +428,12 @@ fn build_config_options(
|
||||
.map(|m| SessionConfigSelectOption::new(m.model_id.0.clone(), m.name.clone()))
|
||||
.collect();
|
||||
vec![
|
||||
SessionConfigOption::select(
|
||||
"provider",
|
||||
"Provider",
|
||||
provider_selection.to_string(),
|
||||
provider_options,
|
||||
),
|
||||
SessionConfigOption::select(
|
||||
"mode",
|
||||
"Mode",
|
||||
@@ -387,7 +458,7 @@ impl GooseAcpAgent {
|
||||
|
||||
// TODO: goose reads Paths::in_state_dir globally (e.g. RequestLog), ignoring this data_dir.
|
||||
pub async fn new(
|
||||
provider_factory: ProviderConstructor,
|
||||
provider_factory: AcpProviderFactory,
|
||||
builtins: Vec<String>,
|
||||
data_dir: std::path::PathBuf,
|
||||
config_dir: std::path::PathBuf,
|
||||
@@ -411,6 +482,19 @@ impl GooseAcpAgent {
|
||||
})
|
||||
}
|
||||
|
||||
fn load_config(&self) -> Result<Config> {
|
||||
Config::new(self.config_dir.join(CONFIG_YAML_NAME), "goose").map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn create_provider(
|
||||
&self,
|
||||
provider_name: &str,
|
||||
model_config: goose::model::ModelConfig,
|
||||
extensions: Vec<ExtensionConfig>,
|
||||
) -> Result<Arc<dyn Provider>> {
|
||||
(self.provider_factory)(provider_name.to_string(), model_config, extensions).await
|
||||
}
|
||||
|
||||
async fn create_agent_for_session(
|
||||
&self,
|
||||
cx: Option<&ConnectionTo<Client>>,
|
||||
@@ -852,6 +936,16 @@ impl GooseAcpAgent {
|
||||
) -> Result<NewSessionResponse, sacp::Error> {
|
||||
debug!(?args, "new session request");
|
||||
|
||||
// Allow the client to request a specific provider via _meta.provider,
|
||||
// avoiding the double-create when the client would otherwise call
|
||||
// _goose/session/provider/update immediately after.
|
||||
let requested_provider = args
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.get("provider"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let goose_session = self
|
||||
.session_manager
|
||||
.create_session(
|
||||
@@ -865,6 +959,30 @@ impl GooseAcpAgent {
|
||||
sacp::Error::internal_error().data(format!("Failed to create session: {}", e))
|
||||
})?;
|
||||
|
||||
if let Some(ref provider_name) = requested_provider {
|
||||
self.session_manager
|
||||
.update(&goose_session.id)
|
||||
.provider_name(provider_name)
|
||||
.apply()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
sacp::Error::internal_error()
|
||||
.data(format!("Failed to set provider on session: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Reload the session so init_provider sees the updated provider_name.
|
||||
let goose_session = if requested_provider.is_some() {
|
||||
self.session_manager
|
||||
.get_session(&goose_session.id, false)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to reload session: {}", e))
|
||||
})?
|
||||
} else {
|
||||
goose_session
|
||||
};
|
||||
|
||||
let session_id = SessionId::new(goose_session.id.clone());
|
||||
|
||||
let agent = self
|
||||
@@ -879,7 +997,6 @@ impl GooseAcpAgent {
|
||||
.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to set provider: {}", e))
|
||||
})?;
|
||||
|
||||
Self::add_mcp_extensions(&agent, args.mcp_servers, &goose_session.id).await?;
|
||||
|
||||
let session = GooseAcpSession {
|
||||
@@ -901,25 +1018,55 @@ impl GooseAcpAgent {
|
||||
|
||||
let model_state = build_model_state(&*provider).await?;
|
||||
let mode_state = build_mode_state(self.goose_mode)?;
|
||||
let provider_selection = session_provider_selection(&goose_session).to_string();
|
||||
let session_id_for_response = SessionId::new(goose_session.id);
|
||||
let provider_options = build_provider_options(Some(provider.get_name())).await;
|
||||
|
||||
Ok(NewSessionResponse::new(SessionId::new(goose_session.id))
|
||||
Ok(NewSessionResponse::new(session_id_for_response)
|
||||
.models(model_state.clone())
|
||||
.modes(mode_state.clone())
|
||||
.config_options(build_config_options(&mode_state, &model_state)))
|
||||
.config_options(build_config_options(
|
||||
&mode_state,
|
||||
&model_state,
|
||||
&provider_selection,
|
||||
provider_options,
|
||||
)))
|
||||
}
|
||||
|
||||
async fn init_provider(&self, agent: &Agent, session: &Session) -> Result<Arc<dyn Provider>> {
|
||||
let config = self.load_config()?;
|
||||
let global_provider = config.get_goose_provider().ok();
|
||||
let provider_override = session
|
||||
.provider_name
|
||||
.as_deref()
|
||||
.filter(|provider| *provider != DEFAULT_PROVIDER_ID);
|
||||
let provider_name = provider_override
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| global_provider.clone())
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not configure agent: missing provider"))?;
|
||||
let explicitly_switched =
|
||||
provider_override.is_some() && provider_override != global_provider.as_deref();
|
||||
let model_config = match &session.model_config {
|
||||
Some(config) => config.clone(),
|
||||
Some(model_config) => model_config.clone(),
|
||||
None if explicitly_switched => {
|
||||
// The provider was set via _meta.provider (or similar) without an
|
||||
// explicit model. Use the provider's own default model from the
|
||||
// registry so we don't leak the global config model (which belongs
|
||||
// to a different provider) into this one.
|
||||
let entry = goose::providers::get_from_registry(&provider_name).await?;
|
||||
let default_model = &entry.metadata().default_model;
|
||||
goose::model::ModelConfig::new(default_model)?.with_canonical_limits(&provider_name)
|
||||
}
|
||||
None => {
|
||||
let config_path = self.config_dir.join(CONFIG_YAML_NAME);
|
||||
let config = Config::new(&config_path, "goose")?;
|
||||
let model_id = config.get_goose_model()?;
|
||||
let provider_name = config.get_goose_provider()?;
|
||||
goose::model::ModelConfig::new(&model_id)?.with_canonical_limits(&provider_name)
|
||||
}
|
||||
};
|
||||
let provider = (self.provider_factory)(model_config, Vec::new()).await?;
|
||||
let extensions =
|
||||
EnabledExtensionsState::extensions_or_default(Some(&session.extension_data), &config);
|
||||
let provider = self
|
||||
.create_provider(&provider_name, model_config, extensions)
|
||||
.await?;
|
||||
agent.update_provider(provider.clone(), &session.id).await?;
|
||||
Ok(provider)
|
||||
}
|
||||
@@ -993,7 +1140,6 @@ impl GooseAcpAgent {
|
||||
sacp::Error::resource_not_found(Some(session_id.clone()))
|
||||
.data(format!("Session not found: {}", session_id))
|
||||
})?;
|
||||
|
||||
let loaded_mode = goose_session.goose_mode;
|
||||
let acp_session_id = SessionId::new(session_id.clone());
|
||||
|
||||
@@ -1003,7 +1149,6 @@ impl GooseAcpAgent {
|
||||
.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to create agent: {}", e))
|
||||
})?;
|
||||
|
||||
let provider = self
|
||||
.init_provider(&agent, &goose_session)
|
||||
.await
|
||||
@@ -1011,8 +1156,15 @@ impl GooseAcpAgent {
|
||||
sacp::Error::internal_error().data(format!("Failed to set provider: {}", e))
|
||||
})?;
|
||||
|
||||
agent
|
||||
.update_goose_mode(loaded_mode, &session_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to restore mode: {}", e))
|
||||
})?;
|
||||
Self::add_mcp_extensions(&agent, args.mcp_servers, &session_id).await?;
|
||||
|
||||
let provider_selection = session_provider_selection(&goose_session).to_string();
|
||||
let conversation = goose_session.conversation.ok_or_else(|| {
|
||||
sacp::Error::internal_error()
|
||||
.data(format!("Session {} has no conversation data", session_id))
|
||||
@@ -1095,11 +1247,17 @@ impl GooseAcpAgent {
|
||||
|
||||
let model_state = build_model_state(&*provider).await?;
|
||||
let mode_state = build_mode_state(goose_mode)?;
|
||||
let provider_options = build_provider_options(Some(provider.get_name())).await;
|
||||
|
||||
Ok(LoadSessionResponse::new()
|
||||
.models(model_state.clone())
|
||||
.modes(mode_state.clone())
|
||||
.config_options(build_config_options(&mode_state, &model_state)))
|
||||
.config_options(build_config_options(
|
||||
&mode_state,
|
||||
&model_state,
|
||||
&provider_selection,
|
||||
provider_options,
|
||||
)))
|
||||
}
|
||||
|
||||
async fn on_prompt(
|
||||
@@ -1167,7 +1325,6 @@ impl GooseAcpAgent {
|
||||
if let Some(session) = sessions.get_mut(&session_id) {
|
||||
session.cancel_token = None;
|
||||
}
|
||||
|
||||
Ok(PromptResponse::new(if was_cancelled {
|
||||
StopReason::Cancelled
|
||||
} else {
|
||||
@@ -1198,25 +1355,28 @@ impl GooseAcpAgent {
|
||||
session_id: &str,
|
||||
model_id: &str,
|
||||
) -> Result<SetSessionModelResponse, sacp::Error> {
|
||||
let config_path = self.config_dir.join(CONFIG_YAML_NAME);
|
||||
let config = Config::new(&config_path, "goose").map_err(|e| {
|
||||
let config = self.load_config().map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to read config: {}", e))
|
||||
})?;
|
||||
let provider_name = config.get_goose_provider().map_err(|_| {
|
||||
sacp::Error::internal_error().data("No provider configured".to_string())
|
||||
let agent = self.get_session_agent(session_id, None).await?;
|
||||
let current_provider = agent.provider().await.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to get provider: {}", e))
|
||||
})?;
|
||||
let provider_name = current_provider.get_name().to_string();
|
||||
let extensions =
|
||||
EnabledExtensionsState::for_session(&self.session_manager, session_id, &config).await;
|
||||
let model_config = goose::model::ModelConfig::new(model_id)
|
||||
.map_err(|e| {
|
||||
sacp::Error::invalid_params().data(format!("Invalid model config: {}", e))
|
||||
})?
|
||||
.with_canonical_limits(&provider_name);
|
||||
let provider = (self.provider_factory)(model_config, Vec::new())
|
||||
let provider = self
|
||||
.create_provider(&provider_name, model_config, extensions)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to create provider: {}", e))
|
||||
})?;
|
||||
|
||||
let agent = self.get_session_agent(session_id, None).await?;
|
||||
agent
|
||||
.update_provider(provider, session_id)
|
||||
.await
|
||||
@@ -1224,6 +1384,14 @@ impl GooseAcpAgent {
|
||||
sacp::Error::internal_error().data(format!("Failed to update provider: {}", e))
|
||||
})?;
|
||||
|
||||
let mode = agent.goose_mode().await;
|
||||
agent
|
||||
.update_goose_mode(mode, session_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to propagate mode: {}", e))
|
||||
})?;
|
||||
|
||||
info!(session_id = %session_id, model_id = %model_id, "Model switched");
|
||||
Ok(SetSessionModelResponse::new())
|
||||
}
|
||||
@@ -1232,6 +1400,11 @@ impl GooseAcpAgent {
|
||||
&self,
|
||||
session_id: &SessionId,
|
||||
) -> Result<(SessionNotification, Vec<SessionConfigOption>), sacp::Error> {
|
||||
let session = self
|
||||
.session_manager
|
||||
.get_session(&session_id.0, false)
|
||||
.await
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
let agent = self.get_session_agent(&session_id.0, None).await?;
|
||||
let provider = agent.provider().await.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to get provider: {}", e))
|
||||
@@ -1239,7 +1412,13 @@ impl GooseAcpAgent {
|
||||
let goose_mode = agent.goose_mode().await;
|
||||
let model_state = build_model_state(&*provider).await?;
|
||||
let mode_state = build_mode_state(goose_mode)?;
|
||||
let config_options = build_config_options(&mode_state, &model_state);
|
||||
let provider_options = build_provider_options(Some(provider.get_name())).await;
|
||||
let config_options = build_config_options(
|
||||
&mode_state,
|
||||
&model_state,
|
||||
session_provider_selection(&session),
|
||||
provider_options,
|
||||
);
|
||||
let notification = SessionNotification::new(
|
||||
session_id.clone(),
|
||||
SessionUpdate::ConfigOptionUpdate(ConfigOptionUpdate::new(config_options.clone())),
|
||||
@@ -1267,6 +1446,120 @@ impl GooseAcpAgent {
|
||||
Ok(SetSessionModeResponse::new())
|
||||
}
|
||||
|
||||
async fn update_provider(
|
||||
&self,
|
||||
session_id: &str,
|
||||
provider_name: &str,
|
||||
model_name: Option<&str>,
|
||||
context_limit: Option<usize>,
|
||||
request_params: Option<std::collections::HashMap<String, serde_json::Value>>,
|
||||
) -> Result<Vec<SessionConfigOption>, sacp::Error> {
|
||||
let config = self.load_config().map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to read config: {}", e))
|
||||
})?;
|
||||
let agent = self.get_session_agent(session_id, None).await?;
|
||||
let current_provider = agent.provider().await.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to get provider: {}", e))
|
||||
})?;
|
||||
let current_provider_name = current_provider.get_name();
|
||||
let current_model = current_provider.get_model_config().model_name;
|
||||
let has_default_overrides =
|
||||
model_name.is_some() || context_limit.is_some() || request_params.is_some();
|
||||
let use_default_provider = provider_name == DEFAULT_PROVIDER_ID;
|
||||
let resolved_provider_name = if use_default_provider {
|
||||
config.get_goose_provider().map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!(
|
||||
"Failed to resolve default provider from config: {}",
|
||||
e
|
||||
))
|
||||
})?
|
||||
} else {
|
||||
provider_name.to_string()
|
||||
};
|
||||
let is_changing_provider = resolved_provider_name != current_provider_name;
|
||||
let default_model = if let Some(model_name) = model_name {
|
||||
model_name.to_string()
|
||||
} else if use_default_provider {
|
||||
config.get_goose_model().map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!(
|
||||
"Failed to resolve default model from config: {}",
|
||||
e
|
||||
))
|
||||
})?
|
||||
} else if is_changing_provider {
|
||||
ACP_CURRENT_MODEL.to_string()
|
||||
} else {
|
||||
current_model
|
||||
};
|
||||
let model = model_name.unwrap_or(&default_model);
|
||||
let model_config = goose::model::ModelConfig::new(model)
|
||||
.map_err(|e| {
|
||||
sacp::Error::invalid_params().data(format!("Invalid model config: {}", e))
|
||||
})?
|
||||
.with_canonical_limits(&resolved_provider_name)
|
||||
.with_context_limit(context_limit)
|
||||
.with_request_params(request_params);
|
||||
let extensions =
|
||||
EnabledExtensionsState::for_session(&self.session_manager, session_id, &config).await;
|
||||
let new_provider = self
|
||||
.create_provider(&resolved_provider_name, model_config, extensions)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to create provider: {}", e))
|
||||
})?;
|
||||
|
||||
agent
|
||||
.update_provider(new_provider, session_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to update provider: {}", e))
|
||||
})?;
|
||||
|
||||
let mode = agent.goose_mode().await;
|
||||
agent
|
||||
.update_goose_mode(mode, session_id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to propagate mode: {}", e))
|
||||
})?;
|
||||
|
||||
let provider = agent.provider().await.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to get provider: {}", e))
|
||||
})?;
|
||||
|
||||
if use_default_provider {
|
||||
let update = self
|
||||
.session_manager
|
||||
.update(session_id)
|
||||
.provider_name(DEFAULT_PROVIDER_ID);
|
||||
if has_default_overrides {
|
||||
let provider_model_config = provider.get_model_config();
|
||||
update
|
||||
.model_config(provider_model_config)
|
||||
.apply()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!(
|
||||
"Failed to persist default provider selection overrides: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
} else {
|
||||
update.clear_model_config().apply().await.map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!(
|
||||
"Failed to persist default provider selection: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
let (_, config_options) = self
|
||||
.build_config_update(&SessionId::new(session_id.to_string()))
|
||||
.await?;
|
||||
Ok(config_options)
|
||||
}
|
||||
|
||||
async fn on_list_sessions(&self) -> Result<ListSessionsResponse, sacp::Error> {
|
||||
let sessions = self
|
||||
.session_manager
|
||||
@@ -1468,6 +1761,124 @@ impl GooseAcpAgent {
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
|
||||
#[custom_method(UpdateProviderRequest)]
|
||||
async fn on_update_provider(
|
||||
&self,
|
||||
req: UpdateProviderRequest,
|
||||
) -> Result<UpdateProviderResponse, sacp::Error> {
|
||||
let config_options = self
|
||||
.update_provider(
|
||||
&req.session_id,
|
||||
&req.provider,
|
||||
req.model.as_deref(),
|
||||
req.context_limit,
|
||||
req.request_params,
|
||||
)
|
||||
.await?;
|
||||
let config_options = config_options
|
||||
.into_iter()
|
||||
.map(|option| serde_json::to_value(&option))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
Ok(UpdateProviderResponse { config_options })
|
||||
}
|
||||
|
||||
#[custom_method(ListProvidersRequest)]
|
||||
async fn on_list_providers(
|
||||
&self,
|
||||
_req: ListProvidersRequest,
|
||||
) -> Result<ListProvidersResponse, sacp::Error> {
|
||||
Ok(ListProvidersResponse {
|
||||
providers: list_provider_entries(None).await,
|
||||
})
|
||||
}
|
||||
|
||||
#[custom_method(ReadConfigRequest)]
|
||||
async fn on_read_config(
|
||||
&self,
|
||||
req: ReadConfigRequest,
|
||||
) -> Result<ReadConfigResponse, sacp::Error> {
|
||||
let config = self.load_config().map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to read config: {}", e))
|
||||
})?;
|
||||
let response = match config.get_param::<serde_json::Value>(&req.key) {
|
||||
Ok(value) => ReadConfigResponse { value },
|
||||
Err(goose::config::ConfigError::NotFound(_)) => ReadConfigResponse {
|
||||
value: serde_json::Value::Null,
|
||||
},
|
||||
Err(e) => return Err(sacp::Error::internal_error().data(e.to_string())),
|
||||
};
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[custom_method(UpsertConfigRequest)]
|
||||
async fn on_upsert_config(
|
||||
&self,
|
||||
req: UpsertConfigRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let config = self.load_config().map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to read config: {}", e))
|
||||
})?;
|
||||
config
|
||||
.set_param(&req.key, &req.value)
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
#[custom_method(RemoveConfigRequest)]
|
||||
async fn on_remove_config(
|
||||
&self,
|
||||
req: RemoveConfigRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let config = self.load_config().map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to read config: {}", e))
|
||||
})?;
|
||||
config
|
||||
.delete(&req.key)
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
#[custom_method(CheckSecretRequest)]
|
||||
async fn on_check_secret(
|
||||
&self,
|
||||
req: CheckSecretRequest,
|
||||
) -> Result<CheckSecretResponse, sacp::Error> {
|
||||
let config = self.load_config().map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to read config: {}", e))
|
||||
})?;
|
||||
let exists = config.get_secret::<serde_json::Value>(&req.key).is_ok();
|
||||
Ok(CheckSecretResponse { exists })
|
||||
}
|
||||
|
||||
#[custom_method(UpsertSecretRequest)]
|
||||
async fn on_upsert_secret(
|
||||
&self,
|
||||
req: UpsertSecretRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let config = self.load_config().map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to read config: {}", e))
|
||||
})?;
|
||||
config
|
||||
.set_secret(&req.key, &req.value)
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
#[custom_method(RemoveSecretRequest)]
|
||||
async fn on_remove_secret(
|
||||
&self,
|
||||
req: RemoveSecretRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
let config = self.load_config().map_err(|e| {
|
||||
sacp::Error::internal_error().data(format!("Failed to read config: {}", e))
|
||||
})?;
|
||||
config
|
||||
.delete_secret(&req.key)
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GooseAcpHandler {
|
||||
@@ -1545,6 +1956,12 @@ impl HandleDispatchFrom<Client> for GooseAcpHandler {
|
||||
.clone();
|
||||
let session_id = req.session_id.clone();
|
||||
match req.config_id.0.as_ref() {
|
||||
"provider" => {
|
||||
match agent.update_provider(&session_id.0, &value_id.0, None, None, None).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => { responder.respond_with_error(e)?; return Ok(()); }
|
||||
}
|
||||
}
|
||||
"mode" => {
|
||||
match agent.on_set_mode(&session_id.0, &value_id.0).await {
|
||||
Ok(_) => {}
|
||||
@@ -2089,11 +2506,23 @@ print(\"hello, world\")
|
||||
|
||||
#[test_case(
|
||||
build_mode_state(GooseMode::Auto).unwrap(),
|
||||
"openai",
|
||||
vec![
|
||||
SessionConfigSelectOption::new("anthropic", "anthropic"),
|
||||
SessionConfigSelectOption::new("openai", "openai"),
|
||||
],
|
||||
SessionModelState::new(
|
||||
ModelId::new("gpt-4"),
|
||||
vec![ModelInfo::new(ModelId::new("gpt-4"), "gpt-4"), ModelInfo::new(ModelId::new("gpt-3.5"), "gpt-3.5")],
|
||||
)
|
||||
=> vec![
|
||||
SessionConfigOption::select(
|
||||
"provider", "Provider", "openai",
|
||||
vec![
|
||||
SessionConfigSelectOption::new("anthropic", "anthropic"),
|
||||
SessionConfigSelectOption::new("openai", "openai"),
|
||||
],
|
||||
),
|
||||
SessionConfigOption::select(
|
||||
"mode", "Mode", "auto",
|
||||
vec![
|
||||
@@ -2115,8 +2544,14 @@ print(\"hello, world\")
|
||||
)]
|
||||
#[test_case(
|
||||
build_mode_state(GooseMode::Approve).unwrap(),
|
||||
"openai",
|
||||
vec![SessionConfigSelectOption::new("openai", "openai")],
|
||||
SessionModelState::new(ModelId::new("only-model"), vec![ModelInfo::new(ModelId::new("only-model"), "only-model")])
|
||||
=> vec![
|
||||
SessionConfigOption::select(
|
||||
"provider", "Provider", "openai",
|
||||
vec![SessionConfigSelectOption::new("openai", "openai")],
|
||||
),
|
||||
SessionConfigOption::select(
|
||||
"mode", "Mode", "approve",
|
||||
vec![
|
||||
@@ -2135,8 +2570,10 @@ print(\"hello, world\")
|
||||
)]
|
||||
fn test_build_config_options(
|
||||
mode_state: SessionModeState,
|
||||
provider_name: &'static str,
|
||||
provider_options: Vec<SessionConfigSelectOption>,
|
||||
model_state: SessionModelState,
|
||||
) -> Vec<SessionConfigOption> {
|
||||
build_config_options(&mode_state, &model_state)
|
||||
build_config_options(&mode_state, &model_state, provider_name, provider_options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use anyhow::Result;
|
||||
use goose::providers::provider_registry::ProviderConstructor;
|
||||
use std::sync::Arc;
|
||||
use tracing::info;
|
||||
|
||||
use crate::server::GooseAcpAgent;
|
||||
use crate::server::{AcpProviderFactory, GooseAcpAgent};
|
||||
|
||||
pub struct AcpServerFactoryConfig {
|
||||
pub builtins: Vec<String>,
|
||||
@@ -32,16 +31,12 @@ impl AcpServer {
|
||||
.unwrap_or(goose::config::GooseMode::Auto);
|
||||
let disable_session_naming = config.get_goose_disable_session_naming().unwrap_or(false);
|
||||
|
||||
let config_dir = self.config.config_dir.clone();
|
||||
let provider_factory: ProviderConstructor = Arc::new(move |model_config, extensions| {
|
||||
let config_dir = config_dir.clone();
|
||||
Box::pin(async move {
|
||||
let config_path = config_dir.join(goose::config::base::CONFIG_YAML_NAME);
|
||||
let config = goose::config::Config::new(&config_path, "goose")?;
|
||||
let provider_name = config.get_goose_provider()?;
|
||||
goose::providers::create(&provider_name, model_config, extensions).await
|
||||
})
|
||||
});
|
||||
let provider_factory: AcpProviderFactory =
|
||||
Arc::new(move |provider_name, model_config, extensions| {
|
||||
Box::pin(async move {
|
||||
goose::providers::create(&provider_name, model_config, extensions).await
|
||||
})
|
||||
});
|
||||
|
||||
let agent = GooseAcpAgent::new(
|
||||
provider_factory,
|
||||
|
||||
@@ -11,7 +11,7 @@ use fixtures::{
|
||||
use fs_err as fs;
|
||||
use goose::config::base::CONFIG_YAML_NAME;
|
||||
use goose::config::GooseMode;
|
||||
use goose::providers::provider_registry::ProviderConstructor;
|
||||
use goose_acp::server::AcpProviderFactory;
|
||||
use goose_test_support::{McpFixture, FAKE_CODE, TEST_IMAGE_B64, TEST_MODEL};
|
||||
use sacp::schema::{
|
||||
ListSessionsResponse, McpServer, McpServerHttp, ModelId, SessionInfo, SessionModeId,
|
||||
@@ -332,8 +332,8 @@ pub async fn run_fs_write_text_file_true<C: Connection>() {
|
||||
}
|
||||
|
||||
pub async fn run_initialize_doesnt_hit_provider<C: Connection>() {
|
||||
let provider_factory: ProviderConstructor =
|
||||
Arc::new(|_, _| Box::pin(async { Err(anyhow::anyhow!("no provider configured")) }));
|
||||
let provider_factory: AcpProviderFactory =
|
||||
Arc::new(|_, _, _| Box::pin(async { Err(anyhow::anyhow!("no provider configured")) }));
|
||||
|
||||
let openai = OpenAiFixture::new(vec![], C::expected_session_id()).await;
|
||||
let config = TestConnectionConfig {
|
||||
@@ -341,12 +341,7 @@ pub async fn run_initialize_doesnt_hit_provider<C: Connection>() {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let conn = C::new(config, openai).await;
|
||||
assert!(!conn.auth_methods().is_empty());
|
||||
assert!(conn
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.any(|m| m.id().0.as_ref() == "goose-provider"));
|
||||
let _conn = C::new(config, openai).await;
|
||||
}
|
||||
|
||||
pub async fn run_load_mode<C: Connection>() {
|
||||
@@ -740,6 +735,24 @@ pub async fn run_model_list<C: Connection>() {
|
||||
assert_eq!(models.current_model_id, ModelId::new(TEST_MODEL));
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn run_new_session_returns_initial_config<C: Connection>() {
|
||||
let expected_session_id = C::expected_session_id();
|
||||
let openai = OpenAiFixture::new(vec![], expected_session_id.clone()).await;
|
||||
|
||||
let mut conn = C::new(TestConnectionConfig::default(), openai).await;
|
||||
let SessionData {
|
||||
session,
|
||||
models,
|
||||
modes,
|
||||
} = conn.new_session().await.unwrap();
|
||||
expected_session_id.set(&session.session_id().0);
|
||||
|
||||
assert!(modes.is_some());
|
||||
let models = models.expect("new_session should return models inline");
|
||||
assert!(!models.available_models.is_empty());
|
||||
}
|
||||
|
||||
pub async fn run_config_option_model_set<C: Connection>() {
|
||||
run_model_set_impl::<C>(SetModelVia::ConfigOption).await;
|
||||
}
|
||||
@@ -808,13 +821,16 @@ async fn run_model_set_impl<C: Connection>(via: SetModelVia) {
|
||||
.unwrap();
|
||||
assert_eq!(output.text, "2");
|
||||
|
||||
// Both model paths emit ConfigOption (no CurrentModelUpdate in the schema).
|
||||
// Some connections emit a ConfigOption update immediately on model change,
|
||||
// while the stripped legacy provider path only updates local state before
|
||||
// the next prompt.
|
||||
let prompt_notifs = session_b.notifications();
|
||||
let mut all = set_model_notifs;
|
||||
all.extend(prompt_notifs);
|
||||
assert_eq!(
|
||||
all,
|
||||
vec![Notification::ConfigOption, Notification::AgentMessage],
|
||||
assert!(
|
||||
all == vec![Notification::AgentMessage]
|
||||
|| all == vec![Notification::ConfigOption, Notification::AgentMessage],
|
||||
"unexpected notifications after model change: {all:?}"
|
||||
);
|
||||
|
||||
// Prompt A: expects default TEST_MODEL (proves sessions are independent)
|
||||
|
||||
@@ -5,11 +5,66 @@ use common_tests::fixtures::server::AcpServerConnection;
|
||||
use common_tests::fixtures::{
|
||||
run_test, send_custom, Connection, Session, SessionData, TestConnectionConfig,
|
||||
};
|
||||
use goose::model::ModelConfig;
|
||||
use goose::providers::base::{MessageStream, Provider};
|
||||
use goose::providers::errors::ProviderError;
|
||||
use goose_acp::server::AcpProviderFactory;
|
||||
use goose_test_support::EnforceSessionId;
|
||||
use std::sync::Arc;
|
||||
|
||||
use common_tests::fixtures::OpenAiFixture;
|
||||
|
||||
struct MockProvider {
|
||||
name: String,
|
||||
model_config: ModelConfig,
|
||||
recommended_models: Vec<String>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Provider for MockProvider {
|
||||
fn get_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
async fn stream(
|
||||
&self,
|
||||
_model_config: &ModelConfig,
|
||||
_session_id: &str,
|
||||
_system: &str,
|
||||
_messages: &[goose::conversation::message::Message],
|
||||
_tools: &[rmcp::model::Tool],
|
||||
) -> Result<MessageStream, ProviderError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn get_model_config(&self) -> ModelConfig {
|
||||
self.model_config.clone()
|
||||
}
|
||||
|
||||
async fn fetch_recommended_models(&self) -> Result<Vec<String>, ProviderError> {
|
||||
Ok(self.recommended_models.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn mock_provider_factory() -> AcpProviderFactory {
|
||||
Arc::new(|provider_name, model_config, _extensions| {
|
||||
Box::pin(async move {
|
||||
let recommended_models = match provider_name.as_str() {
|
||||
"anthropic" => vec![
|
||||
"claude-3-7-sonnet-latest".to_string(),
|
||||
"claude-3-5-haiku-latest".to_string(),
|
||||
],
|
||||
_ => vec!["gpt-4o".to_string(), "o4-mini".to_string()],
|
||||
};
|
||||
Ok(Arc::new(MockProvider {
|
||||
name: provider_name,
|
||||
model_config,
|
||||
recommended_models,
|
||||
}) as Arc<dyn Provider>)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_session_get() {
|
||||
run_test(async {
|
||||
@@ -83,6 +138,209 @@ fn test_custom_get_extensions() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_list_providers() {
|
||||
run_test(async {
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;
|
||||
|
||||
let response = send_custom(conn.cx(), "_goose/providers/list", serde_json::json!({}))
|
||||
.await
|
||||
.expect("provider list should succeed");
|
||||
let providers = response
|
||||
.get("providers")
|
||||
.and_then(|value| value.as_array())
|
||||
.expect("missing providers array");
|
||||
|
||||
assert!(
|
||||
providers.iter().any(|provider| {
|
||||
provider.get("id") == Some(&serde_json::json!("goose"))
|
||||
&& provider.get("label") == Some(&serde_json::json!("Goose (Default)"))
|
||||
}),
|
||||
"expected Goose default provider sentinel"
|
||||
);
|
||||
assert!(
|
||||
providers
|
||||
.iter()
|
||||
.any(|provider| provider.get("id") == Some(&serde_json::json!("openai"))),
|
||||
"expected at least one concrete provider from the goose registry"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_config_crud() {
|
||||
run_test(async {
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;
|
||||
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/config/upsert",
|
||||
serde_json::json!({
|
||||
"key": "GOOSE_PROVIDER",
|
||||
"value": "anthropic",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("config upsert should succeed");
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/config/read",
|
||||
serde_json::json!({
|
||||
"key": "GOOSE_PROVIDER",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("config read should succeed");
|
||||
assert_eq!(response.get("value"), Some(&serde_json::json!("anthropic")));
|
||||
|
||||
send_custom(
|
||||
conn.cx(),
|
||||
"_goose/config/remove",
|
||||
serde_json::json!({
|
||||
"key": "GOOSE_PROVIDER",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("config remove should succeed");
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/config/read",
|
||||
serde_json::json!({
|
||||
"key": "GOOSE_PROVIDER",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("config read after remove should succeed");
|
||||
assert_eq!(response.get("value"), Some(&serde_json::Value::Null));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_provider_switching_updates_session_state() {
|
||||
run_test(async {
|
||||
let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
|
||||
let config = TestConnectionConfig {
|
||||
provider_factory: Some(mock_provider_factory()),
|
||||
current_model: "gpt-4o".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut conn = AcpServerConnection::new(config, openai).await;
|
||||
|
||||
let SessionData { session, .. } = conn.new_session().await.unwrap();
|
||||
let session_id = session.session_id().0.clone();
|
||||
|
||||
conn.set_config_option(&session_id, "provider", "anthropic")
|
||||
.await
|
||||
.expect("provider config option should succeed");
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"session/get",
|
||||
serde_json::json!({
|
||||
"sessionId": session_id,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("session/get should succeed");
|
||||
let session_value = response.get("session").expect("missing session");
|
||||
assert_eq!(
|
||||
session_value.get("provider_name"),
|
||||
Some(&serde_json::json!("anthropic"))
|
||||
);
|
||||
assert_eq!(
|
||||
session_value
|
||||
.get("model_config")
|
||||
.and_then(|value| value.get("model_name")),
|
||||
Some(&serde_json::json!("current"))
|
||||
);
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/session/provider/update",
|
||||
serde_json::json!({
|
||||
"sessionId": session_id,
|
||||
"provider": "openai",
|
||||
"model": "o4-mini",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("provider update should succeed");
|
||||
let config_options = response
|
||||
.get("configOptions")
|
||||
.and_then(|value| value.as_array())
|
||||
.expect("missing config options");
|
||||
assert!(
|
||||
!config_options.is_empty(),
|
||||
"expected refreshed config options"
|
||||
);
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"session/get",
|
||||
serde_json::json!({
|
||||
"sessionId": session_id,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("session/get after provider update should succeed");
|
||||
let session_value = response.get("session").expect("missing session");
|
||||
assert_eq!(
|
||||
session_value.get("provider_name"),
|
||||
Some(&serde_json::json!("openai"))
|
||||
);
|
||||
assert_eq!(
|
||||
session_value
|
||||
.get("model_config")
|
||||
.and_then(|value| value.get("model_name")),
|
||||
Some(&serde_json::json!("o4-mini"))
|
||||
);
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"_goose/session/provider/update",
|
||||
serde_json::json!({
|
||||
"sessionId": session_id,
|
||||
"provider": "goose",
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("provider reset to goose should succeed");
|
||||
let config_options = response
|
||||
.get("configOptions")
|
||||
.and_then(|value| value.as_array())
|
||||
.expect("missing config options after reset");
|
||||
assert!(
|
||||
config_options
|
||||
.iter()
|
||||
.any(|option| option.get("id") == Some(&serde_json::json!("provider"))),
|
||||
"missing provider config option after reset"
|
||||
);
|
||||
|
||||
let response = send_custom(
|
||||
conn.cx(),
|
||||
"session/get",
|
||||
serde_json::json!({
|
||||
"sessionId": session_id,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("session/get after provider reset should succeed");
|
||||
let session_value = response.get("session").expect("missing session");
|
||||
assert_eq!(
|
||||
session_value.get("provider_name"),
|
||||
Some(&serde_json::json!("goose"))
|
||||
);
|
||||
assert_eq!(
|
||||
session_value.get("model_config"),
|
||||
Some(&serde_json::Value::Null)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_unknown_method() {
|
||||
run_test(async {
|
||||
|
||||
+6
-8
@@ -3,19 +3,18 @@
|
||||
|
||||
use async_trait::async_trait;
|
||||
use fs_err as fs;
|
||||
pub use goose::acp::{map_permission_response, PermissionDecision, PermissionMapping};
|
||||
pub use goose::acp::{map_permission_response, PermissionDecision};
|
||||
use goose::builtin_extension::register_builtin_extensions;
|
||||
use goose::config::paths::Paths;
|
||||
use goose::config::{GooseMode, PermissionManager};
|
||||
use goose::providers::api_client::{ApiClient, AuthMethod as ApiAuthMethod};
|
||||
use goose::providers::base::Provider;
|
||||
use goose::providers::openai::OpenAiProvider;
|
||||
use goose::providers::provider_registry::ProviderConstructor;
|
||||
use goose::session_context::SESSION_ID_HEADER;
|
||||
use goose_acp::server::{serve, GooseAcpAgent};
|
||||
use goose_acp::server::{serve, AcpProviderFactory, GooseAcpAgent};
|
||||
use goose_test_support::{ExpectedSessionId, TEST_MODEL};
|
||||
use sacp::schema::{
|
||||
AuthMethod, CreateTerminalResponse, KillTerminalResponse, ListSessionsResponse, McpServer,
|
||||
CreateTerminalResponse, KillTerminalResponse, ListSessionsResponse, McpServer,
|
||||
ReadTextFileRequest, ReadTextFileResponse, ReleaseTerminalResponse, SessionModeState,
|
||||
SessionModelState, SessionUpdate, TerminalExitStatus, TerminalId, TerminalOutputResponse,
|
||||
ToolCallContent, ToolCallStatus, ToolKind, WaitForTerminalExitResponse, WriteTextFileRequest,
|
||||
@@ -155,7 +154,7 @@ pub async fn spawn_acp_server_in_process(
|
||||
builtins: &[String],
|
||||
data_root: &std::path::Path,
|
||||
goose_mode: GooseMode,
|
||||
provider_factory: Option<ProviderConstructor>,
|
||||
provider_factory: Option<AcpProviderFactory>,
|
||||
current_model: &str,
|
||||
) -> (DuplexTransport, JoinHandle<()>, Arc<PermissionManager>) {
|
||||
fs::create_dir_all(data_root).unwrap();
|
||||
@@ -171,7 +170,7 @@ pub async fn spawn_acp_server_in_process(
|
||||
}
|
||||
let provider_factory = provider_factory.unwrap_or_else(|| {
|
||||
let base_url = openai_base_url.to_string();
|
||||
Arc::new(move |model_config, _extensions| {
|
||||
Arc::new(move |_provider_name, model_config, _extensions| {
|
||||
let base_url = base_url.clone();
|
||||
Box::pin(async move {
|
||||
let api_client =
|
||||
@@ -472,7 +471,7 @@ pub struct TestConnectionConfig {
|
||||
pub goose_mode: GooseMode,
|
||||
pub cwd: Option<tempfile::TempDir>,
|
||||
pub data_root: PathBuf,
|
||||
pub provider_factory: Option<ProviderConstructor>,
|
||||
pub provider_factory: Option<AcpProviderFactory>,
|
||||
pub read_text_file: Option<ReadTextFileHandler>,
|
||||
pub write_text_file: Option<WriteTextFileHandler>,
|
||||
pub terminal: Option<Arc<TerminalFixture>>,
|
||||
@@ -524,7 +523,6 @@ pub trait Connection: Sized {
|
||||
config_id: &str,
|
||||
value: &str,
|
||||
) -> anyhow::Result<()>;
|
||||
fn auth_methods(&self) -> &[AuthMethod];
|
||||
fn data_root(&self) -> std::path::PathBuf;
|
||||
fn reset_openai(&self);
|
||||
fn reset_permissions(&self);
|
||||
|
||||
+97
-107
@@ -4,7 +4,7 @@ use super::{
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use goose::acp::{AcpProvider, AcpProviderConfig, PermissionMapping};
|
||||
use goose::acp::{AcpProvider, AcpProviderConfig};
|
||||
use goose::config::{GooseMode, PermissionManager};
|
||||
use goose::conversation::message::{ActionRequiredData, Message, MessageContent};
|
||||
use goose::model::ModelConfig;
|
||||
@@ -12,9 +12,12 @@ use goose::permission::permission_confirmation::PrincipalType;
|
||||
use goose::permission::{Permission, PermissionConfirmation};
|
||||
use goose::providers::base::Provider;
|
||||
use goose_test_support::{ExpectedSessionId, IgnoreSessionId, TEST_MODEL};
|
||||
use sacp::schema::{AuthMethod, ListSessionsResponse, McpServer, SessionUpdate, ToolCallStatus};
|
||||
use sacp::schema::{
|
||||
ListSessionsResponse, McpServer, ModelId, ModelInfo, SessionModelState, SessionUpdate,
|
||||
ToolCallStatus,
|
||||
};
|
||||
use sacp::{Channel, Client, ConnectTo, DynConnectTo};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use strum::VariantNames;
|
||||
@@ -28,10 +31,10 @@ pub struct AcpProviderConnection {
|
||||
/// Option so close_session can trigger session/close via Drop.
|
||||
provider: Arc<Mutex<Option<AcpProvider>>>,
|
||||
permission_manager: Arc<PermissionManager>,
|
||||
auth_methods: Vec<AuthMethod>,
|
||||
session_counter: usize,
|
||||
notification_sink: NotificationSink,
|
||||
session_models: SessionModels,
|
||||
strip_config_options: bool,
|
||||
work_dir: std::path::PathBuf,
|
||||
data_root: std::path::PathBuf,
|
||||
_openai: OpenAiFixture,
|
||||
@@ -186,7 +189,6 @@ impl Connection for AcpProviderConnection {
|
||||
(mode, mode.to_string())
|
||||
})
|
||||
.collect(),
|
||||
permission_mapping: PermissionMapping::default(),
|
||||
notification_callback: Some(Arc::new(move |n| {
|
||||
sink_clone.lock().unwrap().push(n.update.clone());
|
||||
})),
|
||||
@@ -208,15 +210,13 @@ impl Connection for AcpProviderConnection {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let auth_methods = provider.auth_methods().to_vec();
|
||||
|
||||
Self {
|
||||
provider: Arc::new(Mutex::new(Some(provider))),
|
||||
permission_manager,
|
||||
auth_methods,
|
||||
session_counter: 0,
|
||||
notification_sink,
|
||||
session_models,
|
||||
strip_config_options: config.strip_config_options,
|
||||
work_dir: cwd_path,
|
||||
data_root,
|
||||
_openai: openai,
|
||||
@@ -226,18 +226,23 @@ impl Connection for AcpProviderConnection {
|
||||
}
|
||||
|
||||
async fn new_session(&mut self) -> anyhow::Result<SessionData<AcpProviderSession>> {
|
||||
// Tests like run_model_set call new_session() multiple times on the same
|
||||
// connection, so each needs a distinct key to avoid returning a cached session.
|
||||
self.session_counter += 1;
|
||||
let goose_id = format!("test-session-{}", self.session_counter);
|
||||
let response = self
|
||||
.provider
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.ensure_session(Some(&goose_id))
|
||||
.await?;
|
||||
|
||||
let models = if self.strip_config_options {
|
||||
None
|
||||
} else {
|
||||
let provider = self.provider.lock().await;
|
||||
let provider = provider.as_ref().unwrap();
|
||||
let available_models = provider.fetch_supported_models().await?;
|
||||
Some(SessionModelState::new(
|
||||
ModelId::new(provider.get_model_config().model_name.clone()),
|
||||
available_models
|
||||
.into_iter()
|
||||
.map(|model_id| ModelInfo::new(ModelId::new(model_id.clone()), model_id))
|
||||
.collect(),
|
||||
))
|
||||
};
|
||||
|
||||
let session = AcpProviderSession {
|
||||
provider: Arc::clone(&self.provider),
|
||||
@@ -246,10 +251,11 @@ impl Connection for AcpProviderConnection {
|
||||
session_models: self.session_models.clone(),
|
||||
work_dir: self.work_dir.clone(),
|
||||
};
|
||||
self.notification_sink.lock().unwrap().clear();
|
||||
Ok(SessionData {
|
||||
session,
|
||||
models: response.models,
|
||||
modes: response.modes,
|
||||
models,
|
||||
modes: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -264,13 +270,7 @@ impl Connection for AcpProviderConnection {
|
||||
}
|
||||
|
||||
async fn list_sessions(&self) -> anyhow::Result<ListSessionsResponse> {
|
||||
self.provider
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.list_sessions()
|
||||
.await
|
||||
Err(anyhow::anyhow!("not implemented for AcpProviderConnection"))
|
||||
}
|
||||
|
||||
async fn close_session(&self, _session_id: &str) -> anyhow::Result<()> {
|
||||
@@ -279,89 +279,29 @@ impl Connection for AcpProviderConnection {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_session(&self, session_id: &str) -> anyhow::Result<()> {
|
||||
self.provider
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.delete_session(session_id)
|
||||
.await
|
||||
async fn delete_session(&self, _session_id: &str) -> anyhow::Result<()> {
|
||||
Err(anyhow::anyhow!("not implemented for AcpProviderConnection"))
|
||||
}
|
||||
|
||||
fn data_root(&self) -> std::path::PathBuf {
|
||||
self.data_root.clone()
|
||||
}
|
||||
|
||||
async fn set_mode(&self, session_id: &str, mode_id: &str) -> anyhow::Result<()> {
|
||||
let mode = GooseMode::from_str(mode_id)
|
||||
.map_err(|_| sacp::Error::invalid_params().data(format!("Invalid mode: {mode_id}")))?;
|
||||
let guard = self.provider.lock().await;
|
||||
let provider = guard.as_ref().unwrap();
|
||||
if !provider.has_session(session_id).await {
|
||||
return Err(
|
||||
sacp::Error::resource_not_found(Some(session_id.to_string()))
|
||||
.data(format!("Session not found: {session_id}"))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
provider
|
||||
.update_mode(session_id, mode)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||
async fn set_mode(&self, _session_id: &str, _mode_id: &str) -> anyhow::Result<()> {
|
||||
Err(anyhow::anyhow!("not implemented for AcpProviderConnection"))
|
||||
}
|
||||
|
||||
async fn set_model(&self, session_id: &str, model_id: &str) -> anyhow::Result<()> {
|
||||
let config = ModelConfig::new(model_id).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
self.session_models
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(session_id.to_string(), config);
|
||||
Ok(())
|
||||
async fn set_model(&self, _session_id: &str, _model_id: &str) -> anyhow::Result<()> {
|
||||
Err(anyhow::anyhow!("not implemented for AcpProviderConnection"))
|
||||
}
|
||||
|
||||
async fn set_config_option(
|
||||
&self,
|
||||
session_id: &str,
|
||||
config_id: &str,
|
||||
value: &str,
|
||||
_session_id: &str,
|
||||
_config_id: &str,
|
||||
_value: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
// Check up front because the "model" branch doesn't go through the provider.
|
||||
let guard = self.provider.lock().await;
|
||||
let provider = guard.as_ref().unwrap();
|
||||
if !provider.has_session(session_id).await {
|
||||
return Err(
|
||||
sacp::Error::resource_not_found(Some(session_id.to_string()))
|
||||
.data(format!("Session not found: {session_id}"))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
match config_id {
|
||||
"mode" => {
|
||||
let mode = GooseMode::from_str(value).map_err(|_| {
|
||||
sacp::Error::invalid_params().data(format!("Invalid mode: {value}"))
|
||||
})?;
|
||||
provider
|
||||
.update_mode(session_id, mode)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||
}
|
||||
"model" => {
|
||||
let config = ModelConfig::new(value).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
self.session_models
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(session_id.to_string(), config);
|
||||
Ok(())
|
||||
}
|
||||
other => Err(sacp::Error::invalid_params()
|
||||
.data(format!("Unsupported config option: {other}"))
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_methods(&self) -> &[AuthMethod] {
|
||||
&self.auth_methods
|
||||
Err(anyhow::anyhow!("not implemented for AcpProviderConnection"))
|
||||
}
|
||||
|
||||
fn reset_openai(&self) {
|
||||
@@ -425,6 +365,8 @@ fn strip_config_options(transport: DuplexTransport) -> Channel {
|
||||
});
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut stripped_initial_config = HashSet::new();
|
||||
|
||||
let goose_to_server = async {
|
||||
let mut from_goose = filter.rx;
|
||||
while let Some(msg) = from_goose.next().await {
|
||||
@@ -437,19 +379,67 @@ fn strip_config_options(transport: DuplexTransport) -> Channel {
|
||||
let server_to_goose = async {
|
||||
let mut from_server = server.rx;
|
||||
while let Some(msg) = from_server.next().await {
|
||||
let msg = msg.map(|m| match m {
|
||||
sacp::jsonrpcmsg::Message::Response(mut resp) => {
|
||||
if let Some(ref mut result) = resp.result {
|
||||
if let Some(obj) = result.as_object_mut() {
|
||||
obj.remove("configOptions");
|
||||
let msg = match msg {
|
||||
Ok(m) => match m {
|
||||
sacp::jsonrpcmsg::Message::Response(mut resp) => {
|
||||
if let Some(ref mut result) = resp.result {
|
||||
if let Some(obj) = result.as_object_mut() {
|
||||
obj.remove("configOptions");
|
||||
}
|
||||
}
|
||||
Ok(Some(sacp::jsonrpcmsg::Message::Response(resp)))
|
||||
}
|
||||
sacp::jsonrpcmsg::Message::Request(req)
|
||||
if req.id.is_none()
|
||||
&& req.method == "session/update"
|
||||
&& req
|
||||
.params
|
||||
.as_ref()
|
||||
.and_then(|params| match params {
|
||||
sacp::jsonrpcmsg::Params::Object(obj) => Some(obj),
|
||||
_ => None,
|
||||
})
|
||||
.and_then(|obj| obj.get("update"))
|
||||
.and_then(|update| update.get("sessionUpdate"))
|
||||
.and_then(|session_update| session_update.as_str())
|
||||
== Some("config_option_update") =>
|
||||
{
|
||||
let session_id = req
|
||||
.params
|
||||
.as_ref()
|
||||
.and_then(|params| match params {
|
||||
sacp::jsonrpcmsg::Params::Object(obj) => Some(obj),
|
||||
_ => None,
|
||||
})
|
||||
.and_then(|obj| obj.get("sessionId"))
|
||||
.and_then(|session_id| session_id.as_str())
|
||||
.map(str::to_owned);
|
||||
if let Some(session_id) = session_id {
|
||||
if stripped_initial_config.insert(session_id) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(sacp::jsonrpcmsg::Message::Request(req)))
|
||||
}
|
||||
} else {
|
||||
Ok(Some(sacp::jsonrpcmsg::Message::Request(req)))
|
||||
}
|
||||
}
|
||||
sacp::jsonrpcmsg::Message::Response(resp)
|
||||
other => Ok(Some(other)),
|
||||
},
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
match msg {
|
||||
Ok(Some(msg)) => {
|
||||
if filter.tx.unbounded_send(Ok(msg)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(None) => continue,
|
||||
Err(err) => {
|
||||
if filter.tx.unbounded_send(Err(err)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
other => other,
|
||||
});
|
||||
if filter.tx.unbounded_send(msg).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+59
-29
@@ -1,19 +1,20 @@
|
||||
use super::{
|
||||
map_permission_response, spawn_acp_server_in_process, Connection, PermissionDecision,
|
||||
PermissionMapping, Session, SessionData, TestConnectionConfig, TestOutput,
|
||||
map_permission_response, spawn_acp_server_in_process, Connection, PermissionDecision, Session,
|
||||
SessionData, TestConnectionConfig, TestOutput,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use goose::config::PermissionManager;
|
||||
use goose_test_support::{EnforceSessionId, ExpectedSessionId};
|
||||
use sacp::schema::{
|
||||
AuthMethod, ClientCapabilities, CloseSessionRequest, ContentBlock, CreateTerminalRequest,
|
||||
ClientCapabilities, CloseSessionRequest, ContentBlock, CreateTerminalRequest,
|
||||
FileSystemCapabilities, ImageContent, InitializeRequest, KillTerminalRequest,
|
||||
ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, McpServer, NewSessionRequest,
|
||||
PromptRequest, ProtocolVersion, ReadTextFileRequest, ReleaseTerminalRequest,
|
||||
RequestPermissionRequest, SessionConfigOptionValue, SessionId, SessionModeId,
|
||||
SessionNotification, SessionUpdate, SetSessionConfigOptionRequest, SetSessionModeRequest,
|
||||
SetSessionModelRequest, StopReason, TerminalOutputRequest, TextContent, ToolCallStatus,
|
||||
WaitForTerminalExitRequest, WriteTextFileRequest,
|
||||
ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, McpServer, ModelId, ModelInfo,
|
||||
NewSessionRequest, PromptRequest, ProtocolVersion, ReadTextFileRequest, ReleaseTerminalRequest,
|
||||
RequestPermissionRequest, SessionConfigKind, SessionConfigOptionCategory,
|
||||
SessionConfigOptionValue, SessionId, SessionModeId, SessionModelState, SessionNotification,
|
||||
SessionUpdate, SetSessionConfigOptionRequest, SetSessionModeRequest, SetSessionModelRequest,
|
||||
StopReason, TerminalOutputRequest, TextContent, ToolCallStatus, WaitForTerminalExitRequest,
|
||||
WriteTextFileRequest,
|
||||
};
|
||||
use sacp::{Agent, Client, ConnectionTo};
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -30,7 +31,6 @@ pub struct AcpServerConnection {
|
||||
permission: Arc<Mutex<PermissionDecision>>,
|
||||
notify: Arc<Notify>,
|
||||
permission_manager: Arc<PermissionManager>,
|
||||
auth_methods: Vec<AuthMethod>,
|
||||
_openai: super::OpenAiFixture,
|
||||
_temp_dir: Option<tempfile::TempDir>,
|
||||
}
|
||||
@@ -133,7 +133,7 @@ impl Connection for AcpServerConnection {
|
||||
fs_cap = fs_cap.write_text_file(true);
|
||||
}
|
||||
|
||||
let (cx, auth_methods) = {
|
||||
let cx = {
|
||||
let updates_clone = updates.clone();
|
||||
let notify_clone = notify.clone();
|
||||
let permission_clone = permission.clone();
|
||||
@@ -143,14 +143,10 @@ impl Connection for AcpServerConnection {
|
||||
|
||||
let cx_holder: Arc<Mutex<Option<ConnectionTo<Agent>>>> = Arc::new(Mutex::new(None));
|
||||
let cx_holder_clone = cx_holder.clone();
|
||||
let auth_holder: Arc<Mutex<Vec<AuthMethod>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let auth_holder_clone = auth_holder.clone();
|
||||
|
||||
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let permission_mapping = PermissionMapping::default();
|
||||
|
||||
let result = Client
|
||||
.builder()
|
||||
.on_receive_notification(
|
||||
@@ -170,9 +166,7 @@ impl Connection for AcpServerConnection {
|
||||
let permission = permission_clone.clone();
|
||||
async move |req: RequestPermissionRequest, responder, _connection_cx| {
|
||||
let decision = *permission.lock().unwrap();
|
||||
let response =
|
||||
map_permission_response(&permission_mapping, &req, decision);
|
||||
responder.respond(response)
|
||||
responder.respond(map_permission_response(&req, decision))
|
||||
}
|
||||
},
|
||||
sacp::on_receive_request!(),
|
||||
@@ -261,9 +255,8 @@ impl Connection for AcpServerConnection {
|
||||
)
|
||||
.connect_with(transport, {
|
||||
let cx_holder = cx_holder_clone;
|
||||
let auth_holder = auth_holder_clone;
|
||||
async move |cx: ConnectionTo<Agent>| {
|
||||
let resp = cx
|
||||
let _resp = cx
|
||||
.send_request(
|
||||
InitializeRequest::new(ProtocolVersion::LATEST)
|
||||
.client_capabilities(
|
||||
@@ -276,7 +269,6 @@ impl Connection for AcpServerConnection {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
*auth_holder.lock().unwrap() = resp.auth_methods;
|
||||
*cx_holder.lock().unwrap() = Some(cx.clone());
|
||||
let _ = ready_tx.send(());
|
||||
|
||||
@@ -292,8 +284,7 @@ impl Connection for AcpServerConnection {
|
||||
|
||||
ready_rx.await.unwrap();
|
||||
let cx = cx_holder.lock().unwrap().take().unwrap();
|
||||
let auth = std::mem::take(&mut *auth_holder.lock().unwrap());
|
||||
(cx, auth)
|
||||
cx
|
||||
};
|
||||
|
||||
Self {
|
||||
@@ -305,7 +296,6 @@ impl Connection for AcpServerConnection {
|
||||
permission,
|
||||
notify,
|
||||
permission_manager,
|
||||
auth_methods,
|
||||
_openai: openai,
|
||||
_temp_dir: temp_dir,
|
||||
}
|
||||
@@ -330,9 +320,13 @@ impl Connection for AcpServerConnection {
|
||||
notify: self.notify.clone(),
|
||||
_work_dir: work_dir,
|
||||
};
|
||||
let models = response.models.or_else(|| {
|
||||
extract_model_state_from_config_options(response.config_options.as_deref())
|
||||
});
|
||||
self.updates.lock().unwrap().clear();
|
||||
Ok(SessionData {
|
||||
session,
|
||||
models: response.models,
|
||||
models,
|
||||
modes: response.modes,
|
||||
})
|
||||
}
|
||||
@@ -438,10 +432,6 @@ impl Connection for AcpServerConnection {
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn auth_methods(&self) -> &[AuthMethod] {
|
||||
&self.auth_methods
|
||||
}
|
||||
|
||||
fn data_root(&self) -> std::path::PathBuf {
|
||||
self.data_root.clone()
|
||||
}
|
||||
@@ -504,6 +494,46 @@ impl Session for AcpServerSession {
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_model_state_from_config_options(
|
||||
config_options: Option<&[sacp::schema::SessionConfigOption]>,
|
||||
) -> Option<SessionModelState> {
|
||||
let option = config_options?
|
||||
.iter()
|
||||
.find(|option| option.category.as_ref() == Some(&SessionConfigOptionCategory::Model))?;
|
||||
let SessionConfigKind::Select(select) = &option.kind else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let available_models = match &select.options {
|
||||
sacp::schema::SessionConfigSelectOptions::Ungrouped(options) => options
|
||||
.iter()
|
||||
.map(|option| {
|
||||
ModelInfo::new(
|
||||
ModelId::new(option.value.0.to_string()),
|
||||
option.name.clone(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
sacp::schema::SessionConfigSelectOptions::Grouped(groups) => groups
|
||||
.iter()
|
||||
.flat_map(|group| {
|
||||
group.options.iter().map(|option| {
|
||||
ModelInfo::new(
|
||||
ModelId::new(option.value.0.to_string()),
|
||||
option.name.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
Some(SessionModelState::new(
|
||||
ModelId::new(select.current_value.0.to_string()),
|
||||
available_models,
|
||||
))
|
||||
}
|
||||
|
||||
fn collect_agent_text(updates: &Arc<Mutex<Vec<SessionNotification>>>) -> String {
|
||||
let guard = updates.lock().unwrap();
|
||||
let mut text = String::new();
|
||||
|
||||
@@ -1,47 +1,28 @@
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod common_tests;
|
||||
use common_tests::fixtures::provider::AcpProviderConnection;
|
||||
use common_tests::fixtures::run_test;
|
||||
use common_tests::{
|
||||
run_close_session, run_config_mcp, run_config_option_mode_set, run_config_option_model_set,
|
||||
run_delete_session, run_fs_read_text_file_true, run_fs_write_text_file_false,
|
||||
run_fs_write_text_file_true, run_initialize_doesnt_hit_provider, run_list_sessions,
|
||||
run_load_mode, run_load_model, run_load_session_error, run_load_session_mcp, run_mode_set,
|
||||
run_model_list, run_model_set, run_model_set_error_session_not_found,
|
||||
run_permission_persistence, run_prompt_basic, run_prompt_codemode, run_prompt_error,
|
||||
run_prompt_image, run_prompt_image_attachment, run_prompt_mcp, run_prompt_model_mismatch,
|
||||
run_prompt_skill, run_shell_terminal_false, run_shell_terminal_true,
|
||||
run_close_session, run_config_mcp, run_delete_session, run_fs_read_text_file_true,
|
||||
run_fs_write_text_file_false, run_fs_write_text_file_true, run_load_mode, run_load_model,
|
||||
run_load_session_error, run_load_session_mcp, run_model_list, run_permission_persistence,
|
||||
run_prompt_basic, run_prompt_codemode, run_prompt_error, run_prompt_image,
|
||||
run_prompt_image_attachment, run_prompt_mcp, run_prompt_model_mismatch, run_prompt_skill,
|
||||
run_shell_terminal_false, run_shell_terminal_true,
|
||||
};
|
||||
|
||||
tests_config_option_set_error!(AcpProviderConnection);
|
||||
tests_mode_set_error!(AcpProviderConnection);
|
||||
|
||||
#[test]
|
||||
fn test_config_mcp() {
|
||||
run_test(async { run_config_mcp::<AcpProviderConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_option_mode_set() {
|
||||
run_test(async { run_config_option_mode_set::<AcpProviderConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_sessions() {
|
||||
run_test(async { run_list_sessions::<AcpProviderConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_close_session() {
|
||||
run_test(async { run_close_session::<AcpProviderConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_option_model_set() {
|
||||
run_test(async { run_config_option_model_set::<AcpProviderConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "delete is a server-side custom method not routed through the provider"]
|
||||
fn test_delete_session() {
|
||||
@@ -65,11 +46,6 @@ fn test_fs_write_text_file_true() {
|
||||
run_test(async { run_fs_write_text_file_true::<AcpProviderConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initialize_doesnt_hit_provider() {
|
||||
run_test(async { run_initialize_doesnt_hit_provider::<AcpProviderConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "TODO: implement load_session in ACP provider"]
|
||||
fn test_load_mode() {
|
||||
@@ -94,27 +70,11 @@ fn test_load_session_mcp() {
|
||||
run_test(async { run_load_session_mcp::<AcpProviderConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mode_set() {
|
||||
run_test(async { run_mode_set::<AcpProviderConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_list() {
|
||||
run_test(async { run_model_list::<AcpProviderConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_set() {
|
||||
run_test(async { run_model_set::<AcpProviderConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "ensure_session lazy-creates sessions so deleted ones reappear"]
|
||||
fn test_model_set_error_session_not_found() {
|
||||
run_test(async { run_model_set_error_session_not_found::<AcpProviderConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_permission_persistence() {
|
||||
run_test(async { run_permission_persistence::<AcpProviderConnection>().await });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[allow(dead_code)]
|
||||
mod common_tests;
|
||||
use common_tests::fixtures::run_test;
|
||||
use common_tests::fixtures::server::AcpServerConnection;
|
||||
@@ -7,9 +8,10 @@ use common_tests::{
|
||||
run_fs_write_text_file_true, run_initialize_doesnt_hit_provider, run_list_sessions,
|
||||
run_load_mode, run_load_model, run_load_session_error, run_load_session_mcp, run_mode_set,
|
||||
run_model_list, run_model_set, run_model_set_error_session_not_found,
|
||||
run_permission_persistence, run_prompt_basic, run_prompt_codemode, run_prompt_error,
|
||||
run_prompt_image, run_prompt_image_attachment, run_prompt_mcp, run_prompt_model_mismatch,
|
||||
run_prompt_skill, run_shell_terminal_false, run_shell_terminal_true,
|
||||
run_new_session_returns_initial_config, run_permission_persistence, run_prompt_basic,
|
||||
run_prompt_codemode, run_prompt_error, run_prompt_image, run_prompt_image_attachment,
|
||||
run_prompt_mcp, run_prompt_model_mismatch, run_prompt_skill, run_shell_terminal_false,
|
||||
run_shell_terminal_true,
|
||||
};
|
||||
|
||||
tests_config_option_set_error!(AcpServerConnection);
|
||||
@@ -95,6 +97,11 @@ fn test_model_list() {
|
||||
run_test(async { run_model_list::<AcpServerConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_session_returns_initial_config() {
|
||||
run_test(async { run_new_session_returns_initial_config::<AcpServerConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_set() {
|
||||
run_test(async { run_model_set::<AcpServerConnection>().await });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use sacp::{JsonRpcRequest, JsonRpcResponse};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Schema descriptor for a single custom method, produced by the
|
||||
/// `#[custom_methods]` macro's generated `custom_method_schemas()` function.
|
||||
@@ -150,6 +151,109 @@ pub struct GetExtensionsResponse {
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// Atomically update the provider for a live session.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/session/provider/update", response = UpdateProviderResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateProviderRequest {
|
||||
pub session_id: String,
|
||||
pub provider: String,
|
||||
pub model: Option<String>,
|
||||
pub context_limit: Option<usize>,
|
||||
pub request_params: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
/// Provider update response.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateProviderResponse {
|
||||
/// Refreshed session config options after the provider/model change.
|
||||
pub config_options: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Read a single non-secret config value.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/config/read", response = ReadConfigResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadConfigRequest {
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
/// Config read response.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReadConfigResponse {
|
||||
#[serde(default)]
|
||||
pub value: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Upsert a single non-secret config value.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/config/upsert", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpsertConfigRequest {
|
||||
pub key: String,
|
||||
pub value: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Remove a single non-secret config value.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/config/remove", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveConfigRequest {
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
/// Check whether a secret exists. Never returns the actual value.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/secret/check", response = CheckSecretResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CheckSecretRequest {
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
/// Secret check response.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CheckSecretResponse {
|
||||
pub exists: bool,
|
||||
}
|
||||
|
||||
/// Set a secret value (write-only).
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/secret/upsert", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpsertSecretRequest {
|
||||
pub key: String,
|
||||
pub value: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Remove a secret.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/secret/remove", response = EmptyResponse)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveSecretRequest {
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
/// List providers available through goose, including the config-default sentinel.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)]
|
||||
#[request(method = "_goose/providers/list", response = ListProvidersResponse)]
|
||||
pub struct ListProvidersRequest {}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProviderListEntry {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
/// Provider list response.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
|
||||
pub struct ListProvidersResponse {
|
||||
pub providers: Vec<ProviderListEntry>,
|
||||
}
|
||||
|
||||
/// Empty success response for operations that return no data.
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)]
|
||||
pub struct EmptyResponse {}
|
||||
|
||||
+47
-147
@@ -3,27 +3,10 @@ use std::str::FromStr;
|
||||
use crate::permission::Permission;
|
||||
use sacp::schema::{
|
||||
PermissionOption, PermissionOptionKind, RequestPermissionOutcome, RequestPermissionRequest,
|
||||
RequestPermissionResponse, SelectedPermissionOutcome, ToolCallStatus,
|
||||
RequestPermissionResponse, SelectedPermissionOutcome,
|
||||
};
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PermissionMapping {
|
||||
pub allow_option_id: Option<String>,
|
||||
pub reject_option_id: Option<String>,
|
||||
pub rejected_tool_status: ToolCallStatus,
|
||||
}
|
||||
|
||||
impl Default for PermissionMapping {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
allow_option_id: None,
|
||||
reject_option_id: None,
|
||||
rejected_tool_status: ToolCallStatus::Failed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Display, EnumString)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum PermissionDecision {
|
||||
@@ -81,60 +64,30 @@ impl From<&RequestPermissionOutcome> for PermissionDecision {
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a permission decision to a response by matching the option kind from the
|
||||
/// request. Each decision tries its preferred kind first, then falls back to the
|
||||
/// closest alternative (e.g. AllowAlways falls back to AllowOnce).
|
||||
pub fn map_permission_response(
|
||||
mapping: &PermissionMapping,
|
||||
request: &RequestPermissionRequest,
|
||||
decision: PermissionDecision,
|
||||
) -> RequestPermissionResponse {
|
||||
let selected_id = match decision {
|
||||
PermissionDecision::AllowAlways => select_option_id(
|
||||
&request.options,
|
||||
&mapping.allow_option_id,
|
||||
PermissionOptionKind::AllowAlways,
|
||||
)
|
||||
.or_else(|| {
|
||||
select_option_id(
|
||||
&request.options,
|
||||
&mapping.allow_option_id,
|
||||
PermissionOptionKind::AllowOnce,
|
||||
)
|
||||
}),
|
||||
PermissionDecision::AllowOnce => select_option_id(
|
||||
&request.options,
|
||||
&mapping.allow_option_id,
|
||||
PermissionOptionKind::AllowOnce,
|
||||
)
|
||||
.or_else(|| {
|
||||
select_option_id(
|
||||
&request.options,
|
||||
&mapping.allow_option_id,
|
||||
PermissionOptionKind::AllowAlways,
|
||||
)
|
||||
}),
|
||||
PermissionDecision::RejectAlways => select_option_id(
|
||||
&request.options,
|
||||
&mapping.reject_option_id,
|
||||
PermissionOptionKind::RejectAlways,
|
||||
)
|
||||
.or_else(|| {
|
||||
select_option_id(
|
||||
&request.options,
|
||||
&mapping.reject_option_id,
|
||||
PermissionOptionKind::RejectOnce,
|
||||
)
|
||||
}),
|
||||
PermissionDecision::RejectOnce => select_option_id(
|
||||
&request.options,
|
||||
&mapping.reject_option_id,
|
||||
PermissionOptionKind::RejectOnce,
|
||||
)
|
||||
.or_else(|| {
|
||||
select_option_id(
|
||||
&request.options,
|
||||
&mapping.reject_option_id,
|
||||
PermissionOptionKind::RejectAlways,
|
||||
)
|
||||
}),
|
||||
PermissionDecision::AllowAlways => {
|
||||
find_option(&request.options, PermissionOptionKind::AllowAlways)
|
||||
.or_else(|| find_option(&request.options, PermissionOptionKind::AllowOnce))
|
||||
}
|
||||
PermissionDecision::AllowOnce => {
|
||||
find_option(&request.options, PermissionOptionKind::AllowOnce)
|
||||
.or_else(|| find_option(&request.options, PermissionOptionKind::AllowAlways))
|
||||
}
|
||||
PermissionDecision::RejectAlways => {
|
||||
find_option(&request.options, PermissionOptionKind::RejectAlways)
|
||||
.or_else(|| find_option(&request.options, PermissionOptionKind::RejectOnce))
|
||||
}
|
||||
PermissionDecision::RejectOnce => {
|
||||
find_option(&request.options, PermissionOptionKind::RejectOnce)
|
||||
.or_else(|| find_option(&request.options, PermissionOptionKind::RejectAlways))
|
||||
}
|
||||
PermissionDecision::Cancel => None,
|
||||
};
|
||||
|
||||
@@ -147,18 +100,7 @@ pub fn map_permission_response(
|
||||
}
|
||||
}
|
||||
|
||||
fn select_option_id(
|
||||
options: &[PermissionOption],
|
||||
preferred_id: &Option<String>,
|
||||
kind: PermissionOptionKind,
|
||||
) -> Option<String> {
|
||||
if let Some(preferred_id) = preferred_id {
|
||||
let preferred = sacp::schema::PermissionOptionId::new(preferred_id.clone());
|
||||
if options.iter().any(|opt| opt.option_id == preferred) {
|
||||
return Some(preferred_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn find_option(options: &[PermissionOption], kind: PermissionOptionKind) -> Option<String> {
|
||||
options
|
||||
.iter()
|
||||
.find(|opt| opt.kind == kind)
|
||||
@@ -186,88 +128,34 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
Some("allow"),
|
||||
None,
|
||||
PermissionDecision::AllowOnce,
|
||||
"allow",
|
||||
true;
|
||||
"allow_uses_preferred_id"
|
||||
)]
|
||||
#[test_case(
|
||||
None,
|
||||
None,
|
||||
PermissionDecision::AllowAlways,
|
||||
"allow_always",
|
||||
false;
|
||||
"allow_always_prefers_kind"
|
||||
"allow_always";
|
||||
"allow_always_matches_kind"
|
||||
)]
|
||||
#[test_case(
|
||||
Some("missing"),
|
||||
None,
|
||||
PermissionDecision::AllowOnce,
|
||||
"allow_once",
|
||||
false;
|
||||
"allow_falls_back_to_kind"
|
||||
"allow_once";
|
||||
"allow_once_matches_kind"
|
||||
)]
|
||||
#[test_case(
|
||||
None,
|
||||
Some("reject"),
|
||||
PermissionDecision::RejectOnce,
|
||||
"reject",
|
||||
true;
|
||||
"reject_uses_preferred_id"
|
||||
"reject_once";
|
||||
"reject_once_matches_kind"
|
||||
)]
|
||||
#[test_case(
|
||||
None,
|
||||
Some("missing"),
|
||||
PermissionDecision::RejectOnce,
|
||||
"reject_once",
|
||||
false;
|
||||
"reject_falls_back_to_kind"
|
||||
PermissionDecision::RejectAlways,
|
||||
"reject_always";
|
||||
"reject_always_matches_kind"
|
||||
)]
|
||||
fn test_permission_mapping(
|
||||
allow_option_id: Option<&str>,
|
||||
reject_option_id: Option<&str>,
|
||||
decision: PermissionDecision,
|
||||
expected_id: &str,
|
||||
include_preferred: bool,
|
||||
) {
|
||||
let mut options = vec![
|
||||
fn test_permission_response(decision: PermissionDecision, expected_id: &str) {
|
||||
let options = vec![
|
||||
option("allow_once", PermissionOptionKind::AllowOnce),
|
||||
option("allow_always", PermissionOptionKind::AllowAlways),
|
||||
option("reject_once", PermissionOptionKind::RejectOnce),
|
||||
option("reject", PermissionOptionKind::RejectAlways),
|
||||
option("reject_always", PermissionOptionKind::RejectAlways),
|
||||
];
|
||||
|
||||
if include_preferred {
|
||||
if let Some(preferred_allow) = allow_option_id {
|
||||
if !options
|
||||
.iter()
|
||||
.any(|opt| opt.option_id.0.as_ref() == preferred_allow)
|
||||
{
|
||||
options.push(option(preferred_allow, PermissionOptionKind::AllowOnce));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(preferred_reject) = reject_option_id {
|
||||
if !options
|
||||
.iter()
|
||||
.any(|opt| opt.option_id.0.as_ref() == preferred_reject)
|
||||
{
|
||||
options.push(option(preferred_reject, PermissionOptionKind::RejectOnce));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let request = make_request(options);
|
||||
|
||||
let mapping = PermissionMapping {
|
||||
allow_option_id: allow_option_id.map(|s| s.to_string()),
|
||||
reject_option_id: reject_option_id.map(|s| s.to_string()),
|
||||
rejected_tool_status: ToolCallStatus::Failed,
|
||||
};
|
||||
|
||||
let response = map_permission_response(&mapping, &request, decision);
|
||||
let response = map_permission_response(&request, decision);
|
||||
match response.outcome {
|
||||
RequestPermissionOutcome::Selected(selected) => {
|
||||
assert_eq!(selected.option_id.0.as_ref(), expected_id);
|
||||
@@ -276,10 +164,22 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allow_always_falls_back_to_allow_once() {
|
||||
let request = make_request(vec![option("allow_once", PermissionOptionKind::AllowOnce)]);
|
||||
let response = map_permission_response(&request, PermissionDecision::AllowAlways);
|
||||
match response.outcome {
|
||||
RequestPermissionOutcome::Selected(selected) => {
|
||||
assert_eq!(selected.option_id.0.as_ref(), "allow_once");
|
||||
}
|
||||
_ => panic!("expected selected outcome"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test_case(PermissionDecision::Cancel; "cancelled")]
|
||||
fn test_permission_cancelled(decision: PermissionDecision) {
|
||||
let request = make_request(vec![option("allow_once", PermissionOptionKind::AllowOnce)]);
|
||||
let response = map_permission_response(&PermissionMapping::default(), &request, decision);
|
||||
let response = map_permission_response(&request, decision);
|
||||
assert!(matches!(
|
||||
response.outcome,
|
||||
RequestPermissionOutcome::Cancelled
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod common;
|
||||
mod provider;
|
||||
|
||||
pub use common::{map_permission_response, PermissionDecision, PermissionMapping};
|
||||
pub use common::{map_permission_response, PermissionDecision};
|
||||
pub use provider::{
|
||||
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL,
|
||||
};
|
||||
|
||||
+165
-537
File diff suppressed because it is too large
Load Diff
@@ -1578,6 +1578,7 @@ impl Agent {
|
||||
no_tools_called = false;
|
||||
}
|
||||
}
|
||||
#[allow(unused_variables)]
|
||||
Err(ref provider_err @ ProviderError::ContextLengthExceeded(_)) => {
|
||||
#[cfg(feature = "telemetry")]
|
||||
crate::posthog::emit_error(provider_err.telemetry_type(), &provider_err.to_string());
|
||||
|
||||
@@ -2,11 +2,13 @@ use crate::agents::extension::PlatformExtensionContext;
|
||||
use crate::agents::mcp_client::{Error, McpClientTrait};
|
||||
use crate::agents::tool_execution::ToolCallContext;
|
||||
use crate::agents::{AgentEvent, SessionConfig};
|
||||
use crate::config::GooseMode;
|
||||
use crate::config::{Config, ExtensionConfig, GooseMode};
|
||||
use crate::context_mgmt::format_message_for_compacting;
|
||||
use crate::conversation::message::Message;
|
||||
use crate::execution::manager::AgentManager;
|
||||
use crate::providers;
|
||||
use crate::providers::base::Provider;
|
||||
use crate::session::extension_data::EnabledExtensionsState;
|
||||
use crate::session::session_manager::SessionType;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
@@ -139,6 +141,11 @@ impl OrchestratorClient {
|
||||
.ok_or_else(|| "Provider not available".to_string())
|
||||
}
|
||||
|
||||
fn parent_extensions(&self) -> Vec<ExtensionConfig> {
|
||||
let extension_data = self.context.session.as_ref().map(|s| &s.extension_data);
|
||||
EnabledExtensionsState::extensions_or_default(extension_data, Config::global())
|
||||
}
|
||||
|
||||
async fn handle_list_sessions(
|
||||
&self,
|
||||
arguments: Option<JsonObject>,
|
||||
@@ -397,8 +404,15 @@ impl OrchestratorClient {
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create agent: {}", e))?;
|
||||
|
||||
// Inherit the orchestrator's provider and model
|
||||
let provider = self.get_provider().await?;
|
||||
let parent_provider = self.get_provider().await?;
|
||||
let extensions = self.parent_extensions();
|
||||
let provider = providers::create(
|
||||
parent_provider.get_name(),
|
||||
parent_provider.get_model_config(),
|
||||
extensions,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create provider for new agent: {}", e))?;
|
||||
agent
|
||||
.update_provider(provider, &session.id)
|
||||
.await
|
||||
@@ -432,11 +446,20 @@ impl OrchestratorClient {
|
||||
.map_err(|e| format!("Failed to get agent for session '{}': {}", session_id, e))?;
|
||||
|
||||
if agent.provider().await.is_err() {
|
||||
if let Ok(provider) = self.get_provider().await {
|
||||
agent
|
||||
.update_provider(provider, &session_id)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to set provider: {}", e))?;
|
||||
if let Ok(parent_provider) = self.get_provider().await {
|
||||
let extensions = self.parent_extensions();
|
||||
if let Ok(provider) = providers::create(
|
||||
parent_provider.get_name(),
|
||||
parent_provider.get_model_config(),
|
||||
extensions,
|
||||
)
|
||||
.await
|
||||
{
|
||||
agent
|
||||
.update_provider(provider, &session_id)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to set provider: {}", e))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -188,6 +188,10 @@ pub async fn check_if_compaction_needed(
|
||||
threshold_override: Option<f64>,
|
||||
session: &crate::session::Session,
|
||||
) -> Result<bool> {
|
||||
if provider.manages_own_context() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let messages = conversation.messages();
|
||||
let config = Config::global();
|
||||
let threshold = threshold_override.unwrap_or_else(|| {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
use anyhow::Result;
|
||||
use futures::future::BoxFuture;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::acp::{
|
||||
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL,
|
||||
};
|
||||
use crate::config::search_path::SearchPaths;
|
||||
use crate::config::{Config, GooseMode};
|
||||
use crate::model::ModelConfig;
|
||||
use crate::providers::base::{ProviderDef, ProviderMetadata};
|
||||
|
||||
const AMP_ACP_PROVIDER_NAME: &str = "amp-acp";
|
||||
const AMP_ACP_DOC_URL: &str = "https://ampcode.com";
|
||||
const AMP_ACP_BINARY: &str = "amp-acp";
|
||||
|
||||
pub struct AmpAcpProvider;
|
||||
|
||||
impl ProviderDef for AmpAcpProvider {
|
||||
type Provider = AcpProvider;
|
||||
|
||||
fn metadata() -> ProviderMetadata {
|
||||
ProviderMetadata::new(
|
||||
AMP_ACP_PROVIDER_NAME,
|
||||
"Amp",
|
||||
"Use goose with your Amp subscription via the amp-acp adapter.",
|
||||
ACP_CURRENT_MODEL,
|
||||
vec![],
|
||||
AMP_ACP_DOC_URL,
|
||||
vec![],
|
||||
)
|
||||
.with_setup_steps(vec![
|
||||
"Install the Amp CLI: `curl -fsSL https://ampcode.com/install.sh | bash`",
|
||||
"Install the ACP adapter: `npm install -g amp-acp`",
|
||||
"Ensure your Amp CLI is authenticated (run `amp` to verify)",
|
||||
"Set in your goose config file (`~/.config/goose/config.yaml` on macOS/Linux):\n GOOSE_PROVIDER: amp-acp\n GOOSE_MODEL: current",
|
||||
"Restart goose for changes to take effect",
|
||||
])
|
||||
}
|
||||
|
||||
fn from_env(
|
||||
model: ModelConfig,
|
||||
extensions: Vec<crate::config::ExtensionConfig>,
|
||||
) -> BoxFuture<'static, Result<AcpProvider>> {
|
||||
Box::pin(async move {
|
||||
let config = Config::global();
|
||||
let resolved_command = SearchPaths::builder().with_npm().resolve(AMP_ACP_BINARY)?;
|
||||
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
|
||||
|
||||
let mode_mapping = HashMap::from([
|
||||
(GooseMode::Auto, "auto".to_string()),
|
||||
(GooseMode::Approve, "approve".to_string()),
|
||||
(GooseMode::SmartApprove, "smart-approve".to_string()),
|
||||
(GooseMode::Chat, "chat".to_string()),
|
||||
]);
|
||||
|
||||
let provider_config = AcpProviderConfig {
|
||||
command: resolved_command,
|
||||
args: vec![],
|
||||
env: vec![],
|
||||
env_remove: vec![],
|
||||
work_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
mcp_servers: extension_configs_to_mcp_servers(&extensions),
|
||||
session_mode_id: Some(mode_mapping[&goose_mode].clone()),
|
||||
mode_mapping,
|
||||
notification_callback: None,
|
||||
};
|
||||
|
||||
let metadata = Self::metadata();
|
||||
AcpProvider::connect(metadata.name, model, goose_mode, provider_config).await
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,7 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::acp::{
|
||||
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, PermissionMapping,
|
||||
ACP_CURRENT_MODEL,
|
||||
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL,
|
||||
};
|
||||
use crate::config::search_path::SearchPaths;
|
||||
use crate::config::{Config, GooseMode};
|
||||
@@ -51,13 +50,6 @@ impl ProviderDef for ClaudeAcpProvider {
|
||||
.resolve(CLAUDE_ACP_BINARY)?;
|
||||
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
|
||||
|
||||
// claude-agent-acp permission option_ids
|
||||
let permission_mapping = PermissionMapping {
|
||||
allow_option_id: Some("allow".to_string()),
|
||||
reject_option_id: Some("reject".to_string()),
|
||||
rejected_tool_status: sacp::schema::ToolCallStatus::Failed,
|
||||
};
|
||||
|
||||
let mode_mapping = HashMap::from([
|
||||
// Closest to "autonomous": bypassPermissions skips confirmations.
|
||||
(GooseMode::Auto, "bypassPermissions".to_string()),
|
||||
@@ -79,7 +71,6 @@ impl ProviderDef for ClaudeAcpProvider {
|
||||
mcp_servers: extension_configs_to_mcp_servers(&extensions),
|
||||
session_mode_id: Some(mode_mapping[&goose_mode].clone()),
|
||||
mode_mapping,
|
||||
permission_mapping,
|
||||
notification_callback: None,
|
||||
};
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::acp::{
|
||||
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, PermissionMapping,
|
||||
ACP_CURRENT_MODEL,
|
||||
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL,
|
||||
};
|
||||
use crate::config::search_path::SearchPaths;
|
||||
use crate::config::{Config, GooseMode};
|
||||
@@ -74,13 +73,6 @@ impl ProviderDef for CodexAcpProvider {
|
||||
]);
|
||||
}
|
||||
|
||||
// codex-acp permission option_ids
|
||||
let permission_mapping = PermissionMapping {
|
||||
allow_option_id: Some("approved".to_string()),
|
||||
reject_option_id: Some("abort".to_string()),
|
||||
rejected_tool_status: sacp::schema::ToolCallStatus::Failed,
|
||||
};
|
||||
|
||||
// Chat and Approve both map to "read-only".
|
||||
let mode_mapping = HashMap::from([
|
||||
(GooseMode::Auto, "full-access".to_string()),
|
||||
@@ -99,7 +91,6 @@ impl ProviderDef for CodexAcpProvider {
|
||||
// Disabled until https://github.com/zed-industries/codex-acp/issues/179 is fixed.
|
||||
session_mode_id: None,
|
||||
mode_mapping,
|
||||
permission_mapping,
|
||||
notification_callback: None,
|
||||
};
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::acp::{
|
||||
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, PermissionMapping,
|
||||
ACP_CURRENT_MODEL,
|
||||
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL,
|
||||
};
|
||||
use crate::config::search_path::SearchPaths;
|
||||
use crate::config::{Config, GooseMode};
|
||||
@@ -54,10 +53,6 @@ impl ProviderDef for CopilotAcpProvider {
|
||||
.resolve(COPILOT_ACP_BINARY)?;
|
||||
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
|
||||
|
||||
// Copilot uses standard ACP permission option IDs (allow_once,
|
||||
// allow_always, reject_once) so kind-based fallback handles them.
|
||||
let permission_mapping = PermissionMapping::default();
|
||||
|
||||
let mut args = vec!["--acp".to_string()];
|
||||
if model.model_name != ACP_CURRENT_MODEL {
|
||||
args.push("--model".to_string());
|
||||
@@ -82,7 +77,6 @@ impl ProviderDef for CopilotAcpProvider {
|
||||
mcp_servers: extension_configs_to_mcp_servers(&extensions),
|
||||
session_mode_id: Some(mode_mapping[&goose_mode].clone()),
|
||||
mode_mapping,
|
||||
permission_mapping,
|
||||
notification_callback: None,
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use super::local_inference::LocalInferenceProvider;
|
||||
#[cfg(feature = "aws-providers")]
|
||||
use super::sagemaker_tgi::SageMakerTgiProvider;
|
||||
use super::{
|
||||
amp_acp::AmpAcpProvider,
|
||||
anthropic::AnthropicProvider,
|
||||
avian::AvianProvider,
|
||||
azure::AzureProvider,
|
||||
@@ -29,6 +30,7 @@ use super::{
|
||||
ollama::OllamaProvider,
|
||||
openai::OpenAiProvider,
|
||||
openrouter::OpenRouterProvider,
|
||||
pi_acp::PiAcpProvider,
|
||||
provider_registry::ProviderRegistry,
|
||||
snowflake::SnowflakeProvider,
|
||||
tetrate::TetrateProvider,
|
||||
@@ -49,6 +51,7 @@ static REGISTRY: OnceCell<RwLock<ProviderRegistry>> = OnceCell::const_new();
|
||||
|
||||
async fn init_registry() -> RwLock<ProviderRegistry> {
|
||||
let mut registry = ProviderRegistry::new().with_providers(|registry| {
|
||||
registry.register::<AmpAcpProvider>(false);
|
||||
registry.register::<AnthropicProvider>(true);
|
||||
registry.register::<AvianProvider>(false);
|
||||
registry.register::<AzureProvider>(false);
|
||||
@@ -74,6 +77,7 @@ async fn init_registry() -> RwLock<ProviderRegistry> {
|
||||
registry.register::<OllamaProvider>(true);
|
||||
registry.register::<OpenAiProvider>(true);
|
||||
registry.register::<OpenRouterProvider>(true);
|
||||
registry.register::<PiAcpProvider>(false);
|
||||
#[cfg(feature = "aws-providers")]
|
||||
registry.register::<SageMakerTgiProvider>(false);
|
||||
registry.register::<SnowflakeProvider>(false);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod amp_acp;
|
||||
pub mod anthropic;
|
||||
pub mod api_client;
|
||||
pub mod avian;
|
||||
@@ -36,6 +37,7 @@ pub mod ollama;
|
||||
pub mod openai;
|
||||
pub mod openai_compatible;
|
||||
pub mod openrouter;
|
||||
pub mod pi_acp;
|
||||
pub mod provider_registry;
|
||||
pub mod provider_test;
|
||||
mod retry;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
use anyhow::Result;
|
||||
use futures::future::BoxFuture;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::acp::{
|
||||
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL,
|
||||
};
|
||||
use crate::config::search_path::SearchPaths;
|
||||
use crate::config::{Config, GooseMode};
|
||||
use crate::model::ModelConfig;
|
||||
use crate::providers::base::{ProviderDef, ProviderMetadata};
|
||||
|
||||
const PI_ACP_PROVIDER_NAME: &str = "pi-acp";
|
||||
const PI_ACP_DOC_URL: &str = "https://github.com/anthropics/pi";
|
||||
const PI_ACP_BINARY: &str = "pi-acp";
|
||||
|
||||
pub struct PiAcpProvider;
|
||||
|
||||
impl ProviderDef for PiAcpProvider {
|
||||
type Provider = AcpProvider;
|
||||
|
||||
fn metadata() -> ProviderMetadata {
|
||||
ProviderMetadata::new(
|
||||
PI_ACP_PROVIDER_NAME,
|
||||
"Pi",
|
||||
"Use goose with Pi via the pi-acp adapter.",
|
||||
ACP_CURRENT_MODEL,
|
||||
vec![],
|
||||
PI_ACP_DOC_URL,
|
||||
vec![],
|
||||
)
|
||||
.with_setup_steps(vec![
|
||||
"Install the Pi CLI and the pi-acp adapter",
|
||||
"Ensure your Pi CLI is authenticated (run `pi` to verify)",
|
||||
"Set in your goose config file (`~/.config/goose/config.yaml` on macOS/Linux):\n GOOSE_PROVIDER: pi-acp\n GOOSE_MODEL: current",
|
||||
"Restart goose for changes to take effect",
|
||||
])
|
||||
}
|
||||
|
||||
fn from_env(
|
||||
model: ModelConfig,
|
||||
extensions: Vec<crate::config::ExtensionConfig>,
|
||||
) -> BoxFuture<'static, Result<AcpProvider>> {
|
||||
Box::pin(async move {
|
||||
let config = Config::global();
|
||||
let resolved_command = SearchPaths::builder().with_npm().resolve(PI_ACP_BINARY)?;
|
||||
let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto);
|
||||
|
||||
let mode_mapping = HashMap::from([
|
||||
(GooseMode::Auto, "auto".to_string()),
|
||||
(GooseMode::Approve, "approve".to_string()),
|
||||
(GooseMode::SmartApprove, "smart-approve".to_string()),
|
||||
(GooseMode::Chat, "chat".to_string()),
|
||||
]);
|
||||
|
||||
let provider_config = AcpProviderConfig {
|
||||
command: resolved_command,
|
||||
args: vec![],
|
||||
env: vec![],
|
||||
env_remove: vec![],
|
||||
work_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
mcp_servers: extension_configs_to_mcp_servers(&extensions),
|
||||
session_mode_id: Some(mode_mapping[&goose_mode].clone()),
|
||||
mode_mapping,
|
||||
notification_callback: None,
|
||||
};
|
||||
|
||||
let metadata = Self::metadata();
|
||||
AcpProvider::connect(metadata.name, model, goose_mode, provider_config).await
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,10 @@ pub struct ProviderEntry {
|
||||
}
|
||||
|
||||
impl ProviderEntry {
|
||||
pub fn metadata(&self) -> &ProviderMetadata {
|
||||
&self.metadata
|
||||
}
|
||||
|
||||
pub async fn create_with_default_model(
|
||||
&self,
|
||||
extensions: Vec<ExtensionConfig>,
|
||||
|
||||
@@ -839,6 +839,7 @@ async fn execute_job(
|
||||
}
|
||||
drop(jobs_guard);
|
||||
|
||||
#[cfg(feature = "telemetry")]
|
||||
let start_time = std::time::Instant::now();
|
||||
#[cfg(feature = "telemetry")]
|
||||
tokio::spawn(async move {
|
||||
|
||||
@@ -232,6 +232,11 @@ impl<'a> SessionUpdateBuilder<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear_model_config(mut self) -> Self {
|
||||
self.model_config = Some(None);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn goose_mode(mut self, mode: GooseMode) -> Self {
|
||||
self.goose_mode = Some(mode);
|
||||
self
|
||||
|
||||
@@ -22,6 +22,16 @@ ACP providers let you use goose with your existing Claude Code, ChatGPT Plus/Pro
|
||||
|
||||
## Available ACP Providers
|
||||
|
||||
### Amp ACP
|
||||
|
||||
Wraps [amp-acp](https://www.npmjs.com/package/amp-acp), an ACP adapter for [Amp](https://ampcode.com). Uses your existing Amp subscription.
|
||||
|
||||
**Requirements:**
|
||||
- Node.js and npm
|
||||
- Amp CLI installed (`curl -fsSL https://ampcode.com/install.sh | bash`)
|
||||
- ACP adapter installed (`npm install -g amp-acp`)
|
||||
- Authenticated with your Amp account (`amp` CLI working)
|
||||
|
||||
### Claude ACP
|
||||
|
||||
Wraps [claude-agent-acp](https://github.com/zed-industries/claude-agent-acp), an ACP adapter for Anthropic's Claude Code. Uses the same Claude subscription as the deprecated `claude-code` CLI provider.
|
||||
@@ -49,8 +59,44 @@ Uses Google's [Gemini CLI](https://github.com/google-gemini/gemini-cli) directly
|
||||
- Gemini CLI installed (`npm install -g @google/gemini-cli`)
|
||||
- Authenticated with your Google account (run `gemini` once to authenticate via browser)
|
||||
|
||||
### Pi ACP
|
||||
|
||||
Wraps `pi-acp`, an ACP adapter for Pi. Uses your existing Pi installation.
|
||||
|
||||
**Requirements:**
|
||||
- Pi CLI installed
|
||||
- ACP adapter installed (`pi-acp` binary available)
|
||||
- Authenticated with your Pi account (`pi` CLI working)
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### Amp ACP
|
||||
|
||||
1. **Install the Amp CLI**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://ampcode.com/install.sh | bash
|
||||
```
|
||||
|
||||
2. **Install the ACP adapter**
|
||||
|
||||
```bash
|
||||
npm install -g amp-acp
|
||||
```
|
||||
|
||||
3. **Authenticate with Amp**
|
||||
|
||||
Run `amp` and follow the authentication prompts.
|
||||
|
||||
4. **Configure goose**
|
||||
|
||||
Set the provider environment variable:
|
||||
```bash
|
||||
export GOOSE_PROVIDER=amp-acp
|
||||
```
|
||||
|
||||
Or configure through the goose CLI using `goose configure`.
|
||||
|
||||
### Claude ACP
|
||||
|
||||
1. **Install the ACP adapter**
|
||||
@@ -159,6 +205,25 @@ Uses Google's [Gemini CLI](https://github.com/google-gemini/gemini-cli) directly
|
||||
│ default
|
||||
```
|
||||
|
||||
### Pi ACP
|
||||
|
||||
1. **Install the Pi CLI and ACP adapter**
|
||||
|
||||
Install the `pi` CLI and the `pi-acp` ACP adapter following the project's installation instructions.
|
||||
|
||||
2. **Authenticate with Pi**
|
||||
|
||||
Run `pi` and follow the authentication prompts.
|
||||
|
||||
3. **Configure goose**
|
||||
|
||||
Set the provider environment variable:
|
||||
```bash
|
||||
export GOOSE_PROVIDER=pi-acp
|
||||
```
|
||||
|
||||
Or configure through the goose CLI using `goose configure`.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
@@ -191,6 +256,14 @@ GOOSE_PROVIDER=gemini-acp goose run \
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Amp ACP Configuration
|
||||
|
||||
| Environment Variable | Description | Default |
|
||||
|----------------------|-------------------|-----------|
|
||||
| `GOOSE_PROVIDER` | Set to `amp-acp` | None |
|
||||
| `GOOSE_MODEL` | Model to use | `current` |
|
||||
| `GOOSE_MODE` | Permission mode | `auto` |
|
||||
|
||||
### Claude ACP Configuration
|
||||
|
||||
| Environment Variable | Description | Default |
|
||||
@@ -259,13 +332,21 @@ See [codex-acp](https://github.com/zed-industries/codex-acp) for approval policy
|
||||
|
||||
See the [Gemini CLI documentation](https://github.com/google-gemini/gemini-cli) for approval mode details.
|
||||
|
||||
### Pi ACP Configuration
|
||||
|
||||
| Environment Variable | Description | Default |
|
||||
|----------------------|------------------|-----------|
|
||||
| `GOOSE_PROVIDER` | Set to `pi-acp` | None |
|
||||
| `GOOSE_MODEL` | Model to use | `current` |
|
||||
| `GOOSE_MODE` | Permission mode | `auto` |
|
||||
|
||||
## Error Handling
|
||||
|
||||
ACP providers depend on external npm packages, so ensure:
|
||||
ACP providers depend on external binaries, so ensure:
|
||||
|
||||
- The ACP agent binary is installed and in your PATH (`claude-agent-acp`, `codex-acp`, or `gemini`)
|
||||
- The ACP agent binary is installed and in your PATH (`amp-acp`, `claude-agent-acp`, `codex-acp`, `gemini`, `pi-acp`, or `copilot`)
|
||||
- The underlying CLI tool is authenticated and working
|
||||
- Subscription limits are not exceeded
|
||||
- Node.js and npm are installed
|
||||
- Node.js and npm are installed (for npm-distributed adapters)
|
||||
|
||||
If goose can't find the binary, session startup will fail with an error. Run `which claude-agent-acp`, `which codex-acp`, or `which gemini` to verify installation.
|
||||
If goose can't find the binary, session startup will fail with an error. Run `which <binary>` to verify installation.
|
||||
|
||||
@@ -9,6 +9,8 @@ export interface ExtMethodProvider {
|
||||
|
||||
import type {
|
||||
AddExtensionRequest,
|
||||
CheckSecretRequest,
|
||||
CheckSecretResponse,
|
||||
DeleteSessionRequest,
|
||||
ExportSessionRequest,
|
||||
ExportSessionResponse,
|
||||
@@ -20,18 +22,32 @@ import type {
|
||||
GetToolsResponse,
|
||||
ImportSessionRequest,
|
||||
ImportSessionResponse,
|
||||
ListProvidersRequest,
|
||||
ListProvidersResponse,
|
||||
ReadConfigRequest,
|
||||
ReadConfigResponse,
|
||||
ReadResourceRequest,
|
||||
ReadResourceResponse,
|
||||
RemoveConfigRequest,
|
||||
RemoveExtensionRequest,
|
||||
RemoveSecretRequest,
|
||||
UpdateProviderRequest,
|
||||
UpdateProviderResponse,
|
||||
UpdateWorkingDirRequest,
|
||||
UpsertConfigRequest,
|
||||
UpsertSecretRequest,
|
||||
} from './types.gen.js';
|
||||
import {
|
||||
zCheckSecretResponse,
|
||||
zExportSessionResponse,
|
||||
zGetExtensionsResponse,
|
||||
zGetSessionResponse,
|
||||
zGetToolsResponse,
|
||||
zImportSessionResponse,
|
||||
zListProvidersResponse,
|
||||
zReadConfigResponse,
|
||||
zReadResourceResponse,
|
||||
zUpdateProviderResponse,
|
||||
} from './zod.gen.js';
|
||||
|
||||
export class GooseExtClient {
|
||||
@@ -90,4 +106,51 @@ export class GooseExtClient {
|
||||
const raw = await this.conn.extMethod("_goose/config/extensions", params);
|
||||
return zGetExtensionsResponse.parse(raw) as GetExtensionsResponse;
|
||||
}
|
||||
|
||||
async GooseSessionProviderUpdate(
|
||||
params: UpdateProviderRequest,
|
||||
): Promise<UpdateProviderResponse> {
|
||||
const raw = await this.conn.extMethod(
|
||||
"_goose/session/provider/update",
|
||||
params,
|
||||
);
|
||||
return zUpdateProviderResponse.parse(raw) as UpdateProviderResponse;
|
||||
}
|
||||
|
||||
async GooseProvidersList(
|
||||
params: ListProvidersRequest,
|
||||
): Promise<ListProvidersResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/providers/list", params);
|
||||
return zListProvidersResponse.parse(raw) as ListProvidersResponse;
|
||||
}
|
||||
|
||||
async GooseConfigRead(
|
||||
params: ReadConfigRequest,
|
||||
): Promise<ReadConfigResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/config/read", params);
|
||||
return zReadConfigResponse.parse(raw) as ReadConfigResponse;
|
||||
}
|
||||
|
||||
async GooseConfigUpsert(params: UpsertConfigRequest): Promise<void> {
|
||||
await this.conn.extMethod("_goose/config/upsert", params);
|
||||
}
|
||||
|
||||
async GooseConfigRemove(params: RemoveConfigRequest): Promise<void> {
|
||||
await this.conn.extMethod("_goose/config/remove", params);
|
||||
}
|
||||
|
||||
async GooseSecretCheck(
|
||||
params: CheckSecretRequest,
|
||||
): Promise<CheckSecretResponse> {
|
||||
const raw = await this.conn.extMethod("_goose/secret/check", params);
|
||||
return zCheckSecretResponse.parse(raw) as CheckSecretResponse;
|
||||
}
|
||||
|
||||
async GooseSecretUpsert(params: UpsertSecretRequest): Promise<void> {
|
||||
await this.conn.extMethod("_goose/secret/upsert", params);
|
||||
}
|
||||
|
||||
async GooseSecretRemove(params: RemoveSecretRequest): Promise<void> {
|
||||
await this.conn.extMethod("_goose/secret/remove", params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type { AddExtensionRequest, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetSessionRequest, GetSessionResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ReadResourceRequest, ReadResourceResponse, RemoveExtensionRequest, UpdateWorkingDirRequest } from './types.gen.js';
|
||||
export type { AddExtensionRequest, CheckSecretRequest, CheckSecretResponse, DeleteSessionRequest, EmptyResponse, ExportSessionRequest, ExportSessionResponse, ExtRequest, ExtResponse, GetExtensionsRequest, GetExtensionsResponse, GetSessionRequest, GetSessionResponse, GetToolsRequest, GetToolsResponse, ImportSessionRequest, ImportSessionResponse, ListProvidersRequest, ListProvidersResponse, ProviderListEntry, ReadConfigRequest, ReadConfigResponse, ReadResourceRequest, ReadResourceResponse, RemoveConfigRequest, RemoveExtensionRequest, RemoveSecretRequest, UpdateProviderRequest, UpdateProviderResponse, UpdateWorkingDirRequest, UpsertConfigRequest, UpsertSecretRequest } from './types.gen.js';
|
||||
|
||||
export const GOOSE_EXT_METHODS = [
|
||||
{
|
||||
@@ -53,6 +53,46 @@ export const GOOSE_EXT_METHODS = [
|
||||
requestType: "GetExtensionsRequest",
|
||||
responseType: "GetExtensionsResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/session/provider/update",
|
||||
requestType: "UpdateProviderRequest",
|
||||
responseType: "UpdateProviderResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/providers/list",
|
||||
requestType: "ListProvidersRequest",
|
||||
responseType: "ListProvidersResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/config/read",
|
||||
requestType: "ReadConfigRequest",
|
||||
responseType: "ReadConfigResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/config/upsert",
|
||||
requestType: "UpsertConfigRequest",
|
||||
responseType: "EmptyResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/config/remove",
|
||||
requestType: "RemoveConfigRequest",
|
||||
responseType: "EmptyResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/secret/check",
|
||||
requestType: "CheckSecretRequest",
|
||||
responseType: "CheckSecretResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/secret/upsert",
|
||||
requestType: "UpsertSecretRequest",
|
||||
responseType: "EmptyResponse",
|
||||
},
|
||||
{
|
||||
method: "_goose/secret/remove",
|
||||
requestType: "RemoveSecretRequest",
|
||||
responseType: "EmptyResponse",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type GooseExtMethod = (typeof GOOSE_EXT_METHODS)[number];
|
||||
|
||||
@@ -145,17 +145,117 @@ export type GetExtensionsResponse = {
|
||||
warnings: Array<string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Atomically update the provider for a live session.
|
||||
*/
|
||||
export type UpdateProviderRequest = {
|
||||
sessionId: string;
|
||||
provider: string;
|
||||
model?: string | null;
|
||||
contextLimit?: number | null;
|
||||
requestParams?: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider update response.
|
||||
*/
|
||||
export type UpdateProviderResponse = {
|
||||
/**
|
||||
* Refreshed session config options after the provider/model change.
|
||||
*/
|
||||
configOptions: Array<unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* List providers available through goose, including the config-default sentinel.
|
||||
*/
|
||||
export type ListProvidersRequest = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provider list response.
|
||||
*/
|
||||
export type ListProvidersResponse = {
|
||||
providers: Array<ProviderListEntry>;
|
||||
};
|
||||
|
||||
export type ProviderListEntry = {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Read a single non-secret config value.
|
||||
*/
|
||||
export type ReadConfigRequest = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Config read response.
|
||||
*/
|
||||
export type ReadConfigResponse = {
|
||||
value?: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Upsert a single non-secret config value.
|
||||
*/
|
||||
export type UpsertConfigRequest = {
|
||||
key: string;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a single non-secret config value.
|
||||
*/
|
||||
export type RemoveConfigRequest = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether a secret exists. Never returns the actual value.
|
||||
*/
|
||||
export type CheckSecretRequest = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Secret check response.
|
||||
*/
|
||||
export type CheckSecretResponse = {
|
||||
exists: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a secret value (write-only).
|
||||
*/
|
||||
export type UpsertSecretRequest = {
|
||||
key: string;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a secret.
|
||||
*/
|
||||
export type RemoveSecretRequest = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
export type ExtRequest = {
|
||||
id: string;
|
||||
method: string;
|
||||
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | GetSessionRequest | DeleteSessionRequest | ExportSessionRequest | ImportSessionRequest | GetExtensionsRequest | {
|
||||
params?: AddExtensionRequest | RemoveExtensionRequest | GetToolsRequest | ReadResourceRequest | UpdateWorkingDirRequest | GetSessionRequest | DeleteSessionRequest | ExportSessionRequest | ImportSessionRequest | GetExtensionsRequest | UpdateProviderRequest | ListProvidersRequest | ReadConfigRequest | UpsertConfigRequest | RemoveConfigRequest | CheckSecretRequest | UpsertSecretRequest | RemoveSecretRequest | {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type ExtResponse = {
|
||||
id: string;
|
||||
result?: EmptyResponse | GetToolsResponse | ReadResourceResponse | GetSessionResponse | ExportSessionResponse | ImportSessionResponse | GetExtensionsResponse | unknown;
|
||||
result?: EmptyResponse | GetToolsResponse | ReadResourceResponse | GetSessionResponse | ExportSessionResponse | ImportSessionResponse | GetExtensionsResponse | UpdateProviderResponse | ListProvidersResponse | ReadConfigResponse | CheckSecretResponse | unknown;
|
||||
} | {
|
||||
error: {
|
||||
code: number;
|
||||
|
||||
@@ -124,6 +124,108 @@ export const zGetExtensionsResponse = z.object({
|
||||
warnings: z.array(z.string())
|
||||
});
|
||||
|
||||
/**
|
||||
* Atomically update the provider for a live session.
|
||||
*/
|
||||
export const zUpdateProviderRequest = z.object({
|
||||
sessionId: z.string(),
|
||||
provider: z.string(),
|
||||
model: z.union([
|
||||
z.string(),
|
||||
z.null()
|
||||
]).optional(),
|
||||
contextLimit: z.union([
|
||||
z.number().int().gte(0),
|
||||
z.null()
|
||||
]).optional(),
|
||||
requestParams: z.union([
|
||||
z.record(z.unknown()),
|
||||
z.null()
|
||||
]).optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* Provider update response.
|
||||
*/
|
||||
export const zUpdateProviderResponse = z.object({
|
||||
configOptions: z.array(z.unknown())
|
||||
});
|
||||
|
||||
/**
|
||||
* List providers available through goose, including the config-default sentinel.
|
||||
*/
|
||||
export const zListProvidersRequest = z.record(z.unknown());
|
||||
|
||||
export const zProviderListEntry = z.object({
|
||||
id: z.string(),
|
||||
label: z.string()
|
||||
});
|
||||
|
||||
/**
|
||||
* Provider list response.
|
||||
*/
|
||||
export const zListProvidersResponse = z.object({
|
||||
providers: z.array(zProviderListEntry)
|
||||
});
|
||||
|
||||
/**
|
||||
* Read a single non-secret config value.
|
||||
*/
|
||||
export const zReadConfigRequest = z.object({
|
||||
key: z.string()
|
||||
});
|
||||
|
||||
/**
|
||||
* Config read response.
|
||||
*/
|
||||
export const zReadConfigResponse = z.object({
|
||||
value: z.unknown().optional().default(null)
|
||||
});
|
||||
|
||||
/**
|
||||
* Upsert a single non-secret config value.
|
||||
*/
|
||||
export const zUpsertConfigRequest = z.object({
|
||||
key: z.string(),
|
||||
value: z.unknown()
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove a single non-secret config value.
|
||||
*/
|
||||
export const zRemoveConfigRequest = z.object({
|
||||
key: z.string()
|
||||
});
|
||||
|
||||
/**
|
||||
* Check whether a secret exists. Never returns the actual value.
|
||||
*/
|
||||
export const zCheckSecretRequest = z.object({
|
||||
key: z.string()
|
||||
});
|
||||
|
||||
/**
|
||||
* Secret check response.
|
||||
*/
|
||||
export const zCheckSecretResponse = z.object({
|
||||
exists: z.boolean()
|
||||
});
|
||||
|
||||
/**
|
||||
* Set a secret value (write-only).
|
||||
*/
|
||||
export const zUpsertSecretRequest = z.object({
|
||||
key: z.string(),
|
||||
value: z.unknown()
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove a secret.
|
||||
*/
|
||||
export const zRemoveSecretRequest = z.object({
|
||||
key: z.string()
|
||||
});
|
||||
|
||||
export const zExtRequest = z.object({
|
||||
id: z.string(),
|
||||
method: z.string(),
|
||||
@@ -138,7 +240,15 @@ export const zExtRequest = z.object({
|
||||
zDeleteSessionRequest,
|
||||
zExportSessionRequest,
|
||||
zImportSessionRequest,
|
||||
zGetExtensionsRequest
|
||||
zGetExtensionsRequest,
|
||||
zUpdateProviderRequest,
|
||||
zListProvidersRequest,
|
||||
zReadConfigRequest,
|
||||
zUpsertConfigRequest,
|
||||
zRemoveConfigRequest,
|
||||
zCheckSecretRequest,
|
||||
zUpsertSecretRequest,
|
||||
zRemoveSecretRequest
|
||||
]),
|
||||
z.union([
|
||||
z.record(z.unknown()),
|
||||
@@ -158,7 +268,11 @@ export const zExtResponse = z.union([
|
||||
zGetSessionResponse,
|
||||
zExportSessionResponse,
|
||||
zImportSessionResponse,
|
||||
zGetExtensionsResponse
|
||||
zGetExtensionsResponse,
|
||||
zUpdateProviderResponse,
|
||||
zListProvidersResponse,
|
||||
zReadConfigResponse,
|
||||
zCheckSecretResponse
|
||||
]),
|
||||
z.unknown()
|
||||
]).optional()
|
||||
|
||||
Reference in New Issue
Block a user