feat(sdk): minimal uniffi setup for cross language sdk (#9593)

This commit is contained in:
Alex Hancock
2026-06-04 09:58:31 -04:00
committed by GitHub
parent dc59e41945
commit 6251e56347
24 changed files with 514 additions and 24 deletions
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "goose-sdk-types"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
description = "Shared types for the Goose SDK"
[dependencies]
agent-client-protocol = { workspace = true, features = ["unstable"] }
agent-client-protocol-schema = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
schemars = { workspace = true, features = ["derive"] }
[package.metadata.cargo-machete]
# Used to provide extras imports for agent-client-protocol
ignored = ["agent-client-protocol-schema"]
+8
View File
@@ -0,0 +1,8 @@
//! Shared types for the Goose SDK.
//!
//! These wire types are used by both the ACP client/server path and the
//! in-process uniffi bindings, keeping a single source of truth for Goose's
//! custom `_goose/*` JSON-RPC methods.
pub mod custom_notifications;
pub mod custom_requests;
+2
View File
@@ -0,0 +1,2 @@
generated
examples/uniffi/*.jar
+18 -4
View File
@@ -6,14 +6,28 @@ rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
description = "Rust SDK for talking to Goose over the Agent Client Protocol (ACP)"
description = "Rust SDK for Goose with optional uniffi bindings for Python/Kotlin"
[lib]
name = "goose_sdk"
crate-type = ["cdylib", "staticlib", "rlib"]
[[bin]]
name = "goose-uniffi-bindgen"
path = "src/bin/uniffi-bindgen.rs"
required-features = ["uniffi"]
[features]
default = []
uniffi = ["dep:uniffi", "dep:thiserror"]
[dependencies]
goose-sdk-types = { path = "../goose-sdk-types" }
agent-client-protocol = { workspace = true, features = ["unstable"] }
agent-client-protocol-schema = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
schemars = { workspace = true, features = ["derive"] }
uniffi = { version = "0.29", features = ["cli"], optional = true }
thiserror = { version = "2", optional = true }
[dev-dependencies]
tokio = { workspace = true }
+16
View File
@@ -0,0 +1,16 @@
# goose-sdk
The bindings layer for Goose. It houses the shared types used for both ACP and
SDK access, and exposes a cross-language version of the Goose API.
With `--features uniffi` the crate compiles to native bindings for Python and
Kotlin (namespace `aaif_goose` / `aaif.goose`). The published surface is
currently a `ping` -> `pong` stub in `src/bindings.rs` — the scaffold for the
real implementation.
```bash
just python # build bindings + run examples/uniffi/ping.py
just kotlin # build bindings + run examples/uniffi/Ping.kt
```
Both print `pong: aaif.io`.
+9
View File
@@ -0,0 +1,9 @@
package aaif.example
import aaif.goose.Client
fun main() {
val client = Client()
val pong = client.ping("aaif.io")
println(pong.message)
}
+21
View File
@@ -0,0 +1,21 @@
"""Minimal Goose SDK demo: ping the SDK and print the pong."""
from __future__ import annotations
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent.parent / "generated"))
from aaif_goose import Client # noqa: E402
def main() -> None:
client = Client()
pong = client.ping("aaif.io")
print(pong.message)
if __name__ == "__main__":
main()
+38
View File
@@ -0,0 +1,38 @@
set shell := ["bash", "-cu"]
set working-directory := '../..'
lib_ext := if os() == "macos" { "dylib" } else if os() == "windows" { "dll" } else { "so" }
lib_dir := "./target/debug"
lib_path := lib_dir / "libgoose_sdk." + lib_ext
bindgen := "./target/debug/goose-uniffi-bindgen"
config := "./crates/goose-sdk/uniffi.toml"
gen_dir := "./crates/goose-sdk/generated"
examples_dir := "./crates/goose-sdk/examples/uniffi"
default:
@just --list --justfile {{justfile()}}
_build:
cargo build -p goose-sdk --features uniffi -q
_generate lang: _build
{{bindgen}} generate --library {{lib_path}} --config {{config}} --language {{lang}} --no-format --out-dir {{gen_dir}} 2>/dev/null
cp {{lib_path}} {{gen_dir}}/
touch {{gen_dir}}/__init__.py
python: (_generate "python")
DYLD_LIBRARY_PATH={{lib_dir}} LD_LIBRARY_PATH={{lib_dir}} \
python3 {{examples_dir}}/ping.py
kotlin: (_generate "kotlin")
@if [ ! -f {{examples_dir}}/jna.jar ]; then \
curl -sSL -o {{examples_dir}}/jna.jar \
https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar; \
fi
kotlinc -cp {{examples_dir}}/jna.jar -nowarn \
{{gen_dir}}/aaif/goose/aaif_goose.kt \
{{examples_dir}}/Ping.kt \
-include-runtime -d {{examples_dir}}/ping.jar 2>/dev/null
java -Djna.library.path={{lib_dir}} \
--enable-native-access=ALL-UNNAMED \
-cp {{examples_dir}}/ping.jar:{{examples_dir}}/jna.jar aaif.example.PingKt
@@ -0,0 +1,3 @@
fn main() {
uniffi::uniffi_bindgen_main()
}
+69
View File
@@ -0,0 +1,69 @@
//! In-process uniffi bindings for the Goose SDK.
//!
//! This is the published API surface exposed to Python and Kotlin. Right now it
//! is a minimal `ping` -> `pong` round-trip that proves the uniffi
//! infrastructure end to end without depending on the `goose` core crate.
//!
//! To build the real SDK, add `goose` (and whatever else you need) as
//! dependencies and replace the [`Client`] methods below with the actual
//! agent surface.
use std::sync::Arc;
/// Errors surfaced across the uniffi boundary.
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum GooseError {
#[error("{0}")]
Generic(String),
}
/// A reply to a [`Client::ping`] call.
#[derive(Debug, Clone, uniffi::Record)]
pub struct Pong {
/// Echo of the message that was pinged.
pub message: String,
}
/// The top-level entry point for the Goose SDK.
///
/// This is the object that consuming languages instantiate. Today it only knows
/// how to answer a ping; extend it with the real agent API.
#[derive(uniffi::Object)]
pub struct Client {}
#[uniffi::export]
impl Client {
#[uniffi::constructor]
pub fn new() -> Arc<Self> {
Arc::new(Self {})
}
/// Round-trip a message through the SDK. Returns a [`Pong`] echoing the
/// supplied `message`, prefixed with `pong: `.
pub fn ping(&self, message: String) -> Result<Pong, GooseError> {
if message.is_empty() {
return Err(GooseError::Generic("message must not be empty".into()));
}
Ok(Pong {
message: format!("pong: {message}"),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ping_returns_pong() {
let client = Client::new();
let pong = client.ping("aaif.io".into()).expect("ping should succeed");
assert_eq!(pong.message, "pong: aaif.io");
}
#[test]
fn empty_ping_errors() {
let client = Client::new();
assert!(client.ping(String::new()).is_err());
}
}
+21 -2
View File
@@ -1,2 +1,21 @@
pub mod custom_notifications;
pub mod custom_requests;
//! Goose SDK.
//!
//! With default features this crate re-exports the shared SDK wire types from
//! `goose-sdk-types` so you can build an Agent Client Protocol (ACP) client
//! that talks to `goose acp` over stdio.
//!
//! With `--features uniffi` the crate additionally compiles as a
//! `cdylib`/`staticlib` and exposes a small in-process API to Python and Kotlin
//! via [uniffi-rs](https://github.com/mozilla/uniffi-rs).
//!
//! The published uniffi surface is intentionally a single `ping` -> `pong`
//! round-trip. It exists as a working scaffold for adding the real Goose SDK
//! API: replace [`bindings`] with the actual implementation.
pub use goose_sdk_types::{custom_notifications, custom_requests};
#[cfg(feature = "uniffi")]
uniffi::setup_scaffolding!("aaif_goose");
#[cfg(feature = "uniffi")]
pub mod bindings;
+2
View File
@@ -0,0 +1,2 @@
[bindings.kotlin]
package_name = "aaif.goose"
+1 -1
View File
@@ -122,7 +122,7 @@ strum = { workspace = true }
once_cell = { workspace = true }
etcetera = { workspace = true }
fs-err = { version = "3.1", default-features = false }
goose-sdk = { path = "../goose-sdk", default-features = false }
goose-sdk-types = { path = "../goose-sdk-types" }
rand = { workspace = true }
utoipa = { workspace = true, features = ["chrono"] }
tokio-cron-scheduler = { version = "0.15", default-features = false }
+1 -2
View File
@@ -10,8 +10,7 @@ pub(crate) mod tools;
pub mod transport;
pub use common::{map_permission_response, PermissionDecision};
pub use goose_sdk::custom_notifications;
pub use goose_sdk::custom_requests;
pub use goose_sdk_types::{custom_notifications, custom_requests};
pub use provider::{
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL,
};
@@ -16,7 +16,7 @@ use crate::sources::parse_frontmatter;
use crate::utils::safe_truncate;
use anyhow::Result;
use async_trait::async_trait;
use goose_sdk::custom_requests::{SourceEntry, SourceType};
use goose_sdk_types::custom_requests::{SourceEntry, SourceType};
use rmcp::model::{
CallToolResult, Content, Implementation, InitializeResult, JsonObject, ListToolsResult, Meta,
ServerCapabilities, ServerNotification, Tool,
+1 -1
View File
@@ -7,7 +7,7 @@
use crate::sources::parse_frontmatter;
use anyhow::{anyhow, bail, Context, Result};
use goose_sdk::custom_requests::{SourceEntry, SourceType};
use goose_sdk_types::custom_requests::{SourceEntry, SourceType};
use serde::Deserialize;
use std::collections::{BTreeMap, HashMap};
use std::fs;
+1 -2
View File
@@ -2,8 +2,7 @@
compile_error!("Features `rustls-tls` and `native-tls` are mutually exclusive");
pub mod acp;
pub use goose_sdk::custom_notifications;
pub use goose_sdk::custom_requests;
pub use goose_sdk_types::{custom_notifications, custom_requests};
pub mod action_required_manager;
pub mod agents;
pub mod builtin_extension;
+1 -1
View File
@@ -4,7 +4,7 @@ use crate::agents::extension::PlatformExtensionContext;
use crate::agents::mcp_client::{Error, McpClientTrait};
use crate::agents::ToolCallContext;
use async_trait::async_trait;
use goose_sdk::custom_requests::{SourceEntry, SourceType};
use goose_sdk_types::custom_requests::{SourceEntry, SourceType};
use rmcp::model::{
CallToolResult, Content, Implementation, InitializeResult, JsonObject, ListToolsResult,
ServerCapabilities, ServerNotification, Tool,
+1 -1
View File
@@ -14,7 +14,7 @@ use crate::sources::parse_frontmatter;
use agent_client_protocol::Error;
use anyhow::Result;
use arguments::apply_skill_arguments;
use goose_sdk::custom_requests::{SourceEntry, SourceType};
use goose_sdk_types::custom_requests::{SourceEntry, SourceType};
use serde::Deserialize;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
@@ -1,6 +1,6 @@
use std::path::Path;
use goose_sdk::custom_requests::{SourceEntry, SourceType};
use goose_sdk_types::custom_requests::{SourceEntry, SourceType};
use super::types::{SlashCommandEntry, SlashCommandSource};
use super::util::normalize_command_name;
@@ -82,7 +82,7 @@ pub(super) fn commands_from_sources(sources: Vec<SourceEntry>) -> Vec<SlashComma
#[cfg(test)]
mod tests {
use super::*;
use goose_sdk::custom_requests::SourceType;
use goose_sdk_types::custom_requests::SourceType;
use std::collections::HashMap;
use tempfile::TempDir;
+1 -1
View File
@@ -11,7 +11,7 @@ use crate::skills::{
use crate::source_roots::SourceRoot;
use agent_client_protocol::Error;
use fs_err as fs;
use goose_sdk::custom_requests::{SourceEntry, SourceType};
use goose_sdk_types::custom_requests::{SourceEntry, SourceType};
use serde::Deserialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};