Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 73 additions & 67 deletions crates/goose-sdk/src/custom_requests.rs

Large diffs are not rendered by default.

286 changes: 143 additions & 143 deletions crates/goose/acp-meta.json

Large diffs are not rendered by default.

885 changes: 457 additions & 428 deletions crates/goose/acp-schema.json

Large diffs are not rendered by default.

37 changes: 22 additions & 15 deletions crates/goose/src/acp/server/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,13 @@ impl GooseAcpAgent {
&self,
req: CreateSourceRequest,
) -> Result<CreateSourceResponse, agent_client_protocol::Error> {
let project_dir = match (&req.project_id, &req.project_dir) {
(Some(pid), _) if !req.global => {
let dirs = crate::sources::project_working_dirs(pid);
Some(dirs.into_iter().next().ok_or_else(|| {
agent_client_protocol::Error::invalid_params().data(format!(
"Project \"{pid}\" has no working directories configured"
))
})?)
}
(_, Some(pd)) => Some(pd.clone()),
_ => None,
};
let (global, project_dir) = resolve_source_scope(&req.target)?;
let source = crate::sources::create_source(
req.source_type,
&req.name,
&req.description,
&req.content,
req.global,
global,
project_dir.as_deref(),
req.properties,
)?;
Expand Down Expand Up @@ -88,8 +77,26 @@ impl GooseAcpAgent {
&self,
req: ImportSourcesRequest,
) -> Result<ImportSourcesResponse, agent_client_protocol::Error> {
let sources =
crate::sources::import_sources(&req.data, req.global, req.project_dir.as_deref())?;
let (global, project_dir) = resolve_source_scope(&req.target)?;
let sources = crate::sources::import_sources(&req.data, global, project_dir.as_deref())?;
Ok(ImportSourcesResponse { sources })
}
}

fn resolve_source_scope(
target: &SourceScope,
) -> Result<(bool, Option<String>), agent_client_protocol::Error> {
match target {
SourceScope::Global => Ok((true, None)),
SourceScope::ProjectDir { project_dir } => Ok((false, Some(project_dir.clone()))),
SourceScope::ProjectId { project_id } => {
let dirs = crate::sources::project_working_dirs(project_id);
let project_dir = dirs.into_iter().next().ok_or_else(|| {
agent_client_protocol::Error::invalid_params().data(format!(
"Project \"{project_id}\" has no working directories configured"
))
})?;
Ok((false, Some(project_dir)))
}
}
}
86 changes: 78 additions & 8 deletions crates/goose/src/bin/generate_acp_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,32 @@ fn main() {
}
}

let unstable_type_names: BTreeSet<String> = type_methods
.iter()
.filter_map(|(name, methods_list)| {
if methods_list.iter().all(|method| is_unstable_method(method)) {
Some(name.clone())
} else {
None
}
})
.collect();

for def in defs.values_mut() {
rewrite_unstable_schema_refs(def, &unstable_type_names);
}

let mut renamed_defs = Map::new();
for (name, def) in defs {
let generated_name = generated_type_name(&name, &unstable_type_names);
let previous = renamed_defs.insert(generated_name.clone(), def);
assert!(
previous.is_none(),
"duplicate schema definition name after unstable suffix: {generated_name}"
);
}
let mut defs = renamed_defs;

// 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.
Expand All @@ -49,7 +75,8 @@ fn main() {
// 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 {
if let Some(def) = defs.get_mut(name) {
let generated_name = generated_type_name(name, &unstable_type_names);
if let Some(def) = defs.get_mut(&generated_name) {
if let Some(obj) = def.as_object_mut() {
obj.insert("x-side".into(), json!("agent"));
if methods_list.len() == 1 {
Expand All @@ -67,18 +94,20 @@ fn main() {

for m in &methods {
if let Some(name) = &m.params_type_name {
let generated_name = generated_type_name(name, &unstable_type_names);
request_variants.push(json!({
"allOf": [{ "$ref": format!("#/$defs/{name}") }],
"allOf": [{ "$ref": format!("#/$defs/{generated_name}") }],
"description": format!("Params for {}", m.method),
"title": name,
"title": generated_name,
}));
}

if let Some(name) = &m.response_type_name {
if seen_response_types.insert(name.clone()) {
let generated_name = generated_type_name(name, &unstable_type_names);
if seen_response_types.insert(generated_name.clone()) {
response_variants.push(json!({
"allOf": [{ "$ref": format!("#/$defs/{name}") }],
"title": name,
"allOf": [{ "$ref": format!("#/$defs/{generated_name}") }],
"title": generated_name,
}));
}
}
Expand Down Expand Up @@ -179,8 +208,14 @@ fn main() {
.map(|m| {
json!({
"method": &m.method,
"requestType": m.params_type_name,
"responseType": m.response_type_name,
"requestType": m
.params_type_name
.as_ref()
.map(|name| generated_type_name(name, &unstable_type_names)),
"responseType": m
.response_type_name
.as_ref()
.map(|name| generated_type_name(name, &unstable_type_names)),
})
})
.collect();
Expand All @@ -193,6 +228,41 @@ fn main() {
println!("{json_str}");
}

fn is_unstable_method(method: &str) -> bool {
method.contains("_goose/unstable")
}

fn generated_type_name(name: &str, unstable_type_names: &BTreeSet<String>) -> String {
if unstable_type_names.contains(name) {
format!("{name}_unstable")
} else {
name.to_string()
}
}

fn rewrite_unstable_schema_refs(value: &mut Value, unstable_type_names: &BTreeSet<String>) {
match value {
Value::Object(map) => {
if let Some(Value::String(reference)) = map.get_mut("$ref") {
if let Some(name) = reference.strip_prefix("#/$defs/") {
if unstable_type_names.contains(name) {
*reference = format!("#/$defs/{name}_unstable");
}
}
}
for v in map.values_mut() {
rewrite_unstable_schema_refs(v, unstable_type_names);
}
}
Value::Array(arr) => {
for v in arr.iter_mut() {
rewrite_unstable_schema_refs(v, unstable_type_names);
}
}
_ => {}
}
}

/// Recursively strip `"format"` from integer-typed schemas.
///
/// schemars emits `"format": "uint64"` / `"int64"` etc. for Rust integer types.
Expand Down
45 changes: 25 additions & 20 deletions crates/goose/tests/acp_custom_provider_methods_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let catalog = send_custom(
conn.cx(),
"_goose/providers/catalog/list",
"_goose/unstable/providers/catalog/list",
serde_json::json!({ "format": "openai" }),
)
.await
Expand All @@ -74,7 +74,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let setup_catalog = send_custom(
conn.cx(),
"_goose/providers/setup/catalog/list",
"_goose/unstable/providers/setup/catalog/list",
serde_json::json!({}),
)
.await
Expand Down Expand Up @@ -137,7 +137,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let template = send_custom(
conn.cx(),
"_goose/providers/catalog/template",
"_goose/unstable/providers/catalog/template",
serde_json::json!({ "providerId": "zai" }),
)
.await
Expand All @@ -156,7 +156,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let configured_status = send_custom(
conn.cx(),
"_goose/providers/config/status",
"_goose/unstable/providers/config/status",
serde_json::json!({ "providerIds": ["xai"] }),
)
.await
Expand All @@ -172,7 +172,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let configured_read = send_custom(
conn.cx(),
"_goose/providers/config/read",
"_goose/unstable/providers/config/read",
serde_json::json!({ "providerId": "xai" }),
)
.await
Expand All @@ -194,7 +194,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let non_oauth_auth = send_custom(
conn.cx(),
"_goose/providers/config/authenticate",
"_goose/unstable/providers/config/authenticate",
serde_json::json!({ "providerId": "xai" }),
)
.await;
Expand All @@ -210,7 +210,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let created = send_custom(
conn.cx(),
"_goose/providers/custom/create",
"_goose/unstable/providers/custom/create",
serde_json::json!({
"engine": "openai_compatible",
"displayName": "Stark ACP Provider",
Expand Down Expand Up @@ -292,7 +292,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let read = send_custom(
conn.cx(),
"_goose/providers/custom/read",
"_goose/unstable/providers/custom/read",
serde_json::json!({ "providerId": provider_id }),
)
.await
Expand Down Expand Up @@ -321,7 +321,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let inventory = send_custom(
conn.cx(),
"_goose/providers/list",
"_goose/unstable/providers/list",
serde_json::json!({ "providerIds": [provider_id] }),
)
.await
Expand All @@ -337,7 +337,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let updated = send_custom(
conn.cx(),
"_goose/providers/custom/update",
"_goose/unstable/providers/custom/update",
serde_json::json!({
"providerId": provider_id,
"engine": "openai",
Expand Down Expand Up @@ -392,7 +392,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let auth_disabled = send_custom(
conn.cx(),
"_goose/providers/custom/update",
"_goose/unstable/providers/custom/update",
serde_json::json!({
"providerId": provider_id,
"engine": "openai_compatible",
Expand Down Expand Up @@ -432,7 +432,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let auth_reenabled_without_key = send_custom(
conn.cx(),
"_goose/providers/custom/update",
"_goose/unstable/providers/custom/update",
serde_json::json!({
"providerId": provider_id,
"engine": "openai_compatible",
Expand Down Expand Up @@ -464,7 +464,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let deleted = send_custom(
conn.cx(),
"_goose/providers/custom/delete",
"_goose/unstable/providers/custom/delete",
serde_json::json!({ "providerId": provider_id }),
)
.await
Expand Down Expand Up @@ -494,7 +494,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let deleted_status = send_custom(
conn.cx(),
"_goose/providers/config/status",
"_goose/unstable/providers/config/status",
serde_json::json!({ "providerIds": [provider_id] }),
)
.await
Expand All @@ -518,7 +518,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {
] {
let read = send_custom(
conn.cx(),
"_goose/providers/custom/read",
"_goose/unstable/providers/custom/read",
serde_json::json!({ "providerId": invalid_id }),
)
.await;
Expand Down Expand Up @@ -570,7 +570,12 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {
payload_obj.insert(key.clone(), value.clone());
}

let result = send_custom(conn.cx(), "_goose/providers/custom/create", payload).await;
let result = send_custom(
conn.cx(),
"_goose/unstable/providers/custom/create",
payload,
)
.await;
assert!(result.is_err(), "{name} should be rejected");
}

Expand All @@ -580,7 +585,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let shared = send_custom(
conn.cx(),
"_goose/providers/custom/create",
"_goose/unstable/providers/custom/create",
serde_json::json!({
"engine": "openai_compatible",
"displayName": "Shared Secret Test",
Expand Down Expand Up @@ -613,7 +618,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

send_custom(
conn.cx(),
"_goose/providers/custom/update",
"_goose/unstable/providers/custom/update",
serde_json::json!({
"providerId": shared_id,
"engine": "openai_compatible",
Expand All @@ -635,7 +640,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

let shared_delete = send_custom(
conn.cx(),
"_goose/providers/custom/create",
"_goose/unstable/providers/custom/create",
serde_json::json!({
"engine": "openai_compatible",
"displayName": "Shared Secret Delete",
Expand Down Expand Up @@ -668,7 +673,7 @@ fn acp_catalog_and_custom_provider_methods_use_core_provider_store() {

send_custom(
conn.cx(),
"_goose/providers/custom/delete",
"_goose/unstable/providers/custom/delete",
serde_json::json!({ "providerId": shared_delete_id }),
)
.await
Expand Down
Loading
Loading