diff --git a/Cargo.lock b/Cargo.lock index c0e823760504..c8fa0bc0022d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,7 +187,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -198,7 +198,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -278,6 +278,48 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "askama" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash 2.1.1", + "serde", + "serde_derive", + "syn 2.0.117", +] + +[[package]] +name = "askama_parser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow 0.7.15", +] + [[package]] name = "asn1-rs" version = "0.7.1" @@ -338,6 +380,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-compression" version = "0.4.41" @@ -961,7 +1016,7 @@ dependencies = [ "arc-swap", "bytes", "either", - "fs-err", + "fs-err 3.3.0", "http 1.4.0", "http-body 1.0.1", "hyper", @@ -1044,6 +1099,15 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bat" version = "0.26.1" @@ -1838,6 +1902,29 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "castaway" version = "0.2.4" @@ -3387,7 +3474,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3702,7 +3789,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4021,6 +4108,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + [[package]] name = "fs-err" version = "3.3.0" @@ -4554,6 +4650,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + [[package]] name = "goose" version = "1.34.0" @@ -4586,12 +4693,12 @@ dependencies = [ "encoding_rs", "env-lock", "etcetera 0.11.0", - "fs-err", + "fs-err 3.3.0", "fs2", "futures", "goose-acp-macros", "goose-mcp", - "goose-sdk", + "goose-sdk-types", "goose-test-support", "http 1.4.0", "http-body-util", @@ -4780,11 +4887,31 @@ version = "1.34.0" dependencies = [ "agent-client-protocol", "agent-client-protocol-schema", + "anyhow", + "async-trait", + "futures", + "goose", + "goose-sdk-types", + "rmcp", "schemars 1.2.1", "serde", "serde_json", + "thiserror 2.0.18", "tokio", "tokio-util", + "uniffi", +] + +[[package]] +name = "goose-sdk-types" +version = "1.34.0" +dependencies = [ + "agent-client-protocol", + "agent-client-protocol-schema", + "schemars 1.2.1", + "serde", + "serde_json", + "uniffi", ] [[package]] @@ -5710,7 +5837,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6602,7 +6729,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8613,7 +8740,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8672,7 +8799,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8864,6 +8991,26 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "scrypt" version = "0.11.0" @@ -8957,6 +9104,10 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "seq-macro" @@ -9515,7 +9666,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -10660,7 +10811,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -11203,6 +11354,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.9.12+spec-1.1.0" @@ -11863,6 +12023,128 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "uniffi" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err 2.11.0", + "glob", + "goblin", + "heck", + "indexmap 2.14.0", + "once_cell", + "serde", + "tempfile", + "textwrap", + "toml 0.5.11", + "uniffi_internal_macros", + "uniffi_meta", + "uniffi_pipeline", + "uniffi_udl", +] + +[[package]] +name = "uniffi_core" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98" +dependencies = [ + "anyhow", + "async-compat", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "uniffi_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152" +dependencies = [ + "camino", + "fs-err 2.11.0", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "toml 0.5.11", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff" +dependencies = [ + "anyhow", + "siphasher 0.3.11", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + [[package]] name = "unit-prefix" version = "0.5.2" @@ -12286,6 +12568,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "weezl" version = "0.1.12" @@ -12357,7 +12648,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -12842,6 +13133,9 @@ name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] [[package]] name = "winnow" @@ -12859,7 +13153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" dependencies = [ "cfg-if", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/crates/goose-sdk-types/Cargo.toml b/crates/goose-sdk-types/Cargo.toml new file mode 100644 index 000000000000..53bd2b43661d --- /dev/null +++ b/crates/goose-sdk-types/Cargo.toml @@ -0,0 +1,24 @@ +[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" + +[features] +default = [] +uniffi = ["dep:uniffi"] + +[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"] } +uniffi = { version = "0.29", optional = true } + +[package.metadata.cargo-machete] +ignored = ["agent-client-protocol-schema"] diff --git a/crates/goose-sdk/src/custom_requests.rs b/crates/goose-sdk-types/src/custom_requests.rs similarity index 97% rename from crates/goose-sdk/src/custom_requests.rs rename to crates/goose-sdk-types/src/custom_requests.rs index 4a8b52accf99..e7e51f027604 100644 --- a/crates/goose-sdk/src/custom_requests.rs +++ b/crates/goose-sdk-types/src/custom_requests.rs @@ -1283,3 +1283,53 @@ pub struct DictationModelSelectRequest { pub provider: String, pub model_id: String, } + +#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[serde(rename_all = "camelCase")] +pub struct ProviderSpec { + pub name: Option, + pub model: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ExtensionSpec { + Builtin { + name: String, + }, + Stdio { + name: String, + cmd: String, + args: Vec, + envs: HashMap, + }, + StreamableHttp { + name: String, + uri: String, + headers: HashMap, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AgentEvent { + AssistantText { + text: String, + }, + Thinking { + text: String, + }, + ToolRequest { + id: String, + name: String, + arguments: String, + }, + ToolResponse { + id: String, + output: String, + is_error: bool, + }, +} diff --git a/crates/goose-sdk-types/src/lib.rs b/crates/goose-sdk-types/src/lib.rs new file mode 100644 index 000000000000..2ab7b502e66a --- /dev/null +++ b/crates/goose-sdk-types/src/lib.rs @@ -0,0 +1,8 @@ +//! Shared types for the Goose SDK. + +#[cfg(feature = "uniffi")] +uniffi::setup_scaffolding!(); + +pub mod custom_requests; + +pub use custom_requests::{AgentEvent, ExtensionSpec, ProviderSpec}; diff --git a/crates/goose-sdk-types/uniffi.toml b/crates/goose-sdk-types/uniffi.toml new file mode 100644 index 000000000000..2a789711290f --- /dev/null +++ b/crates/goose-sdk-types/uniffi.toml @@ -0,0 +1,4 @@ +[bindings.kotlin] +package_name = "io.aaif.goose.sdk_types" + +[bindings.python] diff --git a/crates/goose-sdk/.gitignore b/crates/goose-sdk/.gitignore new file mode 100644 index 000000000000..86d4c2dd380e --- /dev/null +++ b/crates/goose-sdk/.gitignore @@ -0,0 +1 @@ +generated diff --git a/crates/goose-sdk/Cargo.toml b/crates/goose-sdk/Cargo.toml index 5cdfc6bb072c..cdcdf049fdcf 100644 --- a/crates/goose-sdk/Cargo.toml +++ b/crates/goose-sdk/Cargo.toml @@ -6,19 +6,51 @@ 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:goose", + "dep:tokio", + "dep:futures", + "dep:anyhow", + "dep:thiserror", + "dep:async-trait", + "dep:rmcp", + "goose-sdk-types/uniffi", +] [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"] } +goose = { path = "../goose", optional = true } +uniffi = { version = "0.29", features = ["tokio", "cli"], optional = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync"], optional = true } +futures = { workspace = true, optional = true } +anyhow = { workspace = true, optional = true } +thiserror = { version = "2", optional = true } +async-trait = { workspace = true, optional = true } +rmcp = { workspace = true, optional = true } + [dev-dependencies] tokio = { workspace = true } tokio-util = { workspace = true, features = ["compat", "rt"] } [package.metadata.cargo-machete] -# Used to provide extras imports for agent-client-protocol ignored = ["agent-client-protocol-schema"] diff --git a/crates/goose-sdk/README.md b/crates/goose-sdk/README.md new file mode 100644 index 000000000000..124f2f4d3d1e --- /dev/null +++ b/crates/goose-sdk/README.md @@ -0,0 +1,101 @@ +# goose-sdk + +The Goose SDK exposes Goose's agent functionality outside of the main `goose` binary + +## 1. ACP client/server (default) + +With default features, this crate is a thin Rust library re-exporting the shared types so you can build an Agent Client Protocol client that talks to `goose acp` (or any ACP-compatible Goose server) over stdio. + +See `examples/acp_client.rs`: + +```bash +cargo run -p goose-sdk --example acp_client -- "What is 2 + 2?" +``` + +This path has no dependency on the `goose` core crate — it speaks to Goose as an external process via ACP + Goose's custom `_goose/*` JSON-RPC methods. + +## 2. uniffi bindings (Python / Kotlin) + +With `--features uniffi`, the crate compiles as a `cdylib`/`staticlib` that embeds the `goose` core in-process and exposes an `Agent` object to Python and Kotlin via [uniffi-rs](https://github.com/mozilla/uniffi-rs). + +Build the library, generate bindings, and run the example pings: + +```bash +just python # generates Python bindings + runs examples/uniffi/ping_aaif.py +just kotlin # generates Kotlin bindings + runs examples/uniffi/PingAaif.kt +``` + +Generated bindings land in `generated/`. The shared types from `goose-sdk-types` appear as native records in both languages. + +## Packaging + +Build a distributable artifact for the current platform. Both artifacts bundle the native `libgoose_sdk` for the host platform/arch. + +### Python wheel + +```bash +just python-wheel +pip install crates/goose-sdk/packaging/python/dist/goose_sdk-*.whl +``` + +```python +from goose_sdk import Agent, EventSink +from goose_sdk.goose_sdk_types import ProviderSpec, ExtensionSpec, AgentEvent + +class Printer(EventSink): + def on_event(self, event): + if isinstance(event, AgentEvent.ASSISTANT_TEXT): + print(event.text, end="", flush=True) + def on_error(self, error): print("error:", error) + def on_done(self): print() + +agent = Agent() +agent.configure( + ProviderSpec(name="openai", model="gpt-4o"), + [ExtensionSpec.BUILTIN(name="developer")], +) +agent.reply("ping aaif.io", Printer()) +``` + +### Kotlin/JVM JAR + +```bash +just kotlin-jar +# → crates/goose-sdk/packaging/kotlin/dist/goose-sdk-0.1.0--.jar +``` + +Consumers must also have `net.java.dev.jna:jna:5.14.0` and `org.jetbrains.kotlin:kotlin-stdlib` on the classpath. Call `NativeLoader.ensureLoaded()` once before touching any uniffi-generated type — it extracts the bundled native library and points JNA at it. + +```kotlin +import io.aaif.goose.sdk.{Agent, EventSink, NativeLoader} +import io.aaif.goose.sdk_types.{AgentEvent, ExtensionSpec, ProviderSpec} + +fun main() { + NativeLoader.ensureLoaded() + val agent = Agent() + agent.configure( + ProviderSpec(name = "openai", model = "gpt-4o"), + listOf(ExtensionSpec.Builtin(name = "developer")), + ) + agent.reply("ping aaif.io", object : EventSink { + override fun onEvent(event: AgentEvent) { + if (event is AgentEvent.AssistantText) print(event.text) + } + override fun onError(error: String) = System.err.println("error: $error") + override fun onDone() = println() + }) +} +``` + +Provider credentials for both are read from the same global Goose config (env vars, OS keyring, `~/.config/goose/config.yaml`) used by the `goose` CLI. + +## When to use which + +- **ACP** — use this when you need the full Goose feature surface (sessions, sources, providers, dictation, onboarding, etc.) or process isolation, and you're happy to spawn `goose acp` as a subprocess and speak JSON-RPC over stdio from any language. +- **uniffi** — use this when you want the Goose agent embedded directly inside a Python or Kotlin host process with native types, lower latency, and no subprocess, and the current minimal `Agent` surface (`configure` + `reply`) is enough for your use case. + +## Shared types: `goose-sdk-types` + +The `goose-sdk-types` crate holds the wire types used by both consumers above — request/response structs for Goose's custom JSON-RPC ACP methods (`AddExtensionRequest`, `GooseToolCallRequest`, provider/session/sources/dictation requests, etc.) and the streaming `AgentEvent`, `ExtensionSpec`, and `ProviderSpec` records. + +Keeping these types in a small, dependency-light crate lets the ACP path serialize/deserialize them as JSON-RPC and the uniffi path expose them as native records in Python/Kotlin — from one source of truth. diff --git a/crates/goose-sdk/examples/uniffi/PingAaif.kt b/crates/goose-sdk/examples/uniffi/PingAaif.kt new file mode 100644 index 000000000000..3a3939c8c67f --- /dev/null +++ b/crates/goose-sdk/examples/uniffi/PingAaif.kt @@ -0,0 +1,81 @@ +package examples + +import io.aaif.goose.sdk.Agent +import io.aaif.goose.sdk.EventSink +import io.aaif.goose.sdk_types.AgentEvent +import io.aaif.goose.sdk_types.ExtensionSpec +import io.aaif.goose.sdk_types.ProviderSpec + +private object Style { + const val DIM = "\u001B[2m" + const val CYAN = "\u001B[36m" + const val GREEN = "\u001B[32m" + const val RED = "\u001B[31m" + const val RESET = "\u001B[0m" +} + +private fun String.paint(color: String) = "$color$this${Style.RESET}" + +private fun String.preview(maxLines: Int = 3, maxWidth: Int = 100): String = + lineSequence() + .filter { it.isNotBlank() } + .map { it.take(maxWidth) } + .take(maxLines) + .joinToString("\n ") + +private class Printer : EventSink { + private var midText = false + + override fun onEvent(event: AgentEvent) { + when (event) { + is AgentEvent.AssistantText -> { + print(event.text) + midText = true + } + is AgentEvent.ToolRequest -> { + endTextLine() + val args = event.arguments.replace("\n", " ").take(120) + println("→ ${event.name}".paint(Style.CYAN) + " " + args.paint(Style.DIM)) + } + is AgentEvent.ToolResponse -> { + endTextLine() + val color = if (event.isError) Style.RED else Style.GREEN + val marker = if (event.isError) "✗" else "✓" + println(marker.paint(color) + " " + event.output.preview().paint(Style.DIM)) + println() + } + is AgentEvent.Thinking -> Unit + } + System.out.flush() + } + + override fun onError(error: String) { + System.err.println("\n${"error:".paint(Style.RED)} $error") + } + + override fun onDone() = endTextLine() + + private fun endTextLine() { + if (midText) { + println() + midText = false + } + } +} + +fun main() { + System.err.println("configuring agent…".paint(Style.DIM)) + + val agent = Agent().apply { + configure( + ProviderSpec( + name = System.getenv("GOOSE_PROVIDER"), + model = System.getenv("GOOSE_MODEL"), + ), + listOf(ExtensionSpec.Builtin(name = "developer")), + ) + } + + System.err.println("> ping aaif.io".paint(Style.DIM) + "\n") + agent.reply("ping aaif.io", Printer()) +} diff --git a/crates/goose-sdk/examples/uniffi/ping_aaif.py b/crates/goose-sdk/examples/uniffi/ping_aaif.py new file mode 100644 index 000000000000..9f60f9d2f700 --- /dev/null +++ b/crates/goose-sdk/examples/uniffi/ping_aaif.py @@ -0,0 +1,81 @@ +"""Minimal goose SDK demo: ask the agent to ping aaif.io.""" + +from __future__ import annotations + +import os +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE.parent.parent)) + +from generated.goose_sdk import Agent, EventSink # noqa: E402 +from generated.goose_sdk_types import AgentEvent, ExtensionSpec, ProviderSpec # noqa: E402 + +DIM = "\033[2m" +CYAN = "\033[36m" +GREEN = "\033[32m" +RED = "\033[31m" +RESET = "\033[0m" + + +def paint(color: str, text: str) -> str: + return f"{color}{text}{RESET}" + + +def preview(output: str, max_lines: int = 3, max_width: int = 100) -> str: + lines = (line[:max_width] for line in output.splitlines() if line.strip()) + return "\n ".join(list(lines)[:max_lines]) + + +class Printer(EventSink): + def __init__(self) -> None: + self._mid_text = False + + def on_event(self, event: AgentEvent) -> None: + if isinstance(event, AgentEvent.ASSISTANT_TEXT): + print(event.text, end="", flush=True) + self._mid_text = True + return + + self._end_text_line() + + if isinstance(event, AgentEvent.TOOL_REQUEST): + args = event.arguments.replace("\n", " ")[:120] + print(f"{paint(CYAN, '→ ' + event.name)} {paint(DIM, args)}", flush=True) + + elif isinstance(event, AgentEvent.TOOL_RESPONSE): + color = RED if event.is_error else GREEN + marker = "✗" if event.is_error else "✓" + print(f"{paint(color, marker)} {paint(DIM, preview(event.output))}\n", flush=True) + + def on_error(self, error: str) -> None: + print(f"\n{paint(RED, 'error:')} {error}", file=sys.stderr) + + def on_done(self) -> None: + self._end_text_line() + + def _end_text_line(self) -> None: + if self._mid_text: + print() + self._mid_text = False + + +def main() -> None: + print(paint(DIM, "configuring agent…"), file=sys.stderr) + + agent = Agent() + agent.configure( + ProviderSpec( + name=os.environ.get("GOOSE_PROVIDER"), + model=os.environ.get("GOOSE_MODEL"), + ), + [ExtensionSpec.BUILTIN(name="developer")], + ) + + print(paint(DIM, "> ping aaif.io") + "\n", file=sys.stderr) + agent.reply("ping aaif.io", Printer()) + + +if __name__ == "__main__": + main() diff --git a/crates/goose-sdk/justfile b/crates/goose-sdk/justfile new file mode 100644 index 000000000000..a38b4534cace --- /dev/null +++ b/crates/goose-sdk/justfile @@ -0,0 +1,46 @@ +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" +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}} --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_aaif.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}}/io/aaif/goose/sdk_types/goose_sdk_types.kt \ + {{gen_dir}}/io/aaif/goose/sdk/goose_sdk.kt \ + {{examples_dir}}/PingAaif.kt \ + -include-runtime -d {{examples_dir}}/ping_aaif.jar 2>/dev/null + java -Djna.library.path={{lib_dir}} \ + --enable-native-access=ALL-UNNAMED \ + -cp {{examples_dir}}/ping_aaif.jar:{{examples_dir}}/jna.jar examples.PingAaifKt + +# Build a platform-specific Python wheel under packaging/python/dist/ +python-wheel: + python3 ./crates/goose-sdk/packaging/python/make_wheel.py + +# Build a platform-specific Kotlin/JVM JAR under packaging/kotlin/dist/ +kotlin-jar: + python3 ./crates/goose-sdk/packaging/kotlin/make_jar.py diff --git a/crates/goose-sdk/packaging/kotlin/.gitignore b/crates/goose-sdk/packaging/kotlin/.gitignore new file mode 100644 index 000000000000..e11dfecddf41 --- /dev/null +++ b/crates/goose-sdk/packaging/kotlin/.gitignore @@ -0,0 +1,2 @@ +.build/ +dist/ diff --git a/crates/goose-sdk/packaging/kotlin/make_jar.py b/crates/goose-sdk/packaging/kotlin/make_jar.py new file mode 100644 index 000000000000..309f2db7aab8 --- /dev/null +++ b/crates/goose-sdk/packaging/kotlin/make_jar.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Assemble the goose-sdk Kotlin/JVM JAR (debug profile). + +Builds the cargo dylib + uniffi bindings, downloads JNA if missing, compiles +the Kotlin sources, and bundles classes + the native library into a JAR with +a minimal POM. Output: packaging/kotlin/dist/goose-sdk---.jar + +Consumers also need net.java.dev.jna:jna and kotlin-stdlib on the classpath. +""" + +from __future__ import annotations + +import platform +import shutil +import subprocess +import sys +import urllib.request +from pathlib import Path + +HERE = Path(__file__).resolve().parent +REPO_ROOT = HERE.parents[3] +PROFILE_DIR = REPO_ROOT / "target" / "debug" + +GROUP, ARTIFACT, VERSION = "io.aaif.goose", "goose-sdk", "0.1.0" +JNA_VERSION = "5.14.0" +JNA_URL = f"https://repo1.maven.org/maven2/net/java/dev/jna/jna/{JNA_VERSION}/jna-{JNA_VERSION}.jar" + +NATIVE_LOADER_KT = r""" +package io.aaif.goose.sdk + +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +/** Extracts the bundled native lib from the JAR and points JNA at it. */ +object NativeLoader { + private var loaded = false + + @Synchronized fun ensureLoaded() { + if (loaded) return + val (osDir, libName) = platform() + val res = "/native/$osDir/$libName" + val stream = NativeLoader::class.java.getResourceAsStream(res) + ?: throw UnsatisfiedLinkError("goose-sdk: no bundled native lib at $res") + val tmp = Files.createTempDirectory("goose-sdk-").toFile().apply { deleteOnExit() } + val out = File(tmp, libName).apply { deleteOnExit() } + stream.use { Files.copy(it, out.toPath(), StandardCopyOption.REPLACE_EXISTING) } + val prev = System.getProperty("jna.library.path") + System.setProperty("jna.library.path", + if (prev.isNullOrEmpty()) tmp.absolutePath + else "${tmp.absolutePath}${File.pathSeparator}$prev") + loaded = true + } + + private fun platform(): Pair { + val os = System.getProperty("os.name").lowercase() + val arch = when (System.getProperty("os.arch").lowercase()) { + "amd64", "x86_64" -> "x86_64" + "aarch64", "arm64" -> "aarch64" + else -> System.getProperty("os.arch").lowercase() + } + return when { + "mac" in os || "darwin" in os -> "darwin-$arch" to "libgoose_sdk.dylib" + "win" in os -> "windows-$arch" to "goose_sdk.dll" + else -> "linux-$arch" to "libgoose_sdk.so" + } + } +} +""" + +POM_XML = f""" + + 4.0.0 + {GROUP} + {ARTIFACT} + {VERSION} + + net.java.dev.jnajna{JNA_VERSION} + org.jetbrains.kotlinkotlin-stdlib1.9.0 + + +""" + + +def lib_info() -> tuple[str, str]: + system = platform.system() + arch = {"arm64": "aarch64", "aarch64": "aarch64", + "x86_64": "x86_64", "amd64": "x86_64"}.get( + platform.machine().lower(), platform.machine().lower()) + if system == "Darwin": return f"darwin-{arch}", "libgoose_sdk.dylib" + if system == "Windows": return f"windows-{arch}", "goose_sdk.dll" + return f"linux-{arch}", "libgoose_sdk.so" + + +def run(cmd: list[str], **kw) -> None: + print("$", *cmd, flush=True) + subprocess.run(cmd, check=True, **kw) + + +def main() -> int: + run(["cargo", "build", "-p", "goose-sdk", "--features", "uniffi"], cwd=REPO_ROOT) + + os_arch, lib_name = lib_info() + lib = PROFILE_DIR / lib_name + bindgen = PROFILE_DIR / "goose-uniffi-bindgen" + for p in (lib, bindgen): + if not p.exists(): + sys.exit(f"missing: {p}") + + build = HERE / ".build" + shutil.rmtree(build, ignore_errors=True) + bindings, classes, staging = build / "bindings", build / "classes", build / "staging" + for d in (bindings, classes, staging): + d.mkdir(parents=True) + + run([str(bindgen), "generate", "--library", str(lib), "--language", "kotlin", + "--no-format", "--out-dir", str(bindings)]) + + jna_jar = build / f"jna-{JNA_VERSION}.jar" + if not jna_jar.exists(): + print("$ download", JNA_URL, flush=True) + urllib.request.urlretrieve(JNA_URL, jna_jar) + + loader_kt = build / "NativeLoader.kt" + loader_kt.write_text(NATIVE_LOADER_KT) + sources = [ + str(bindings / "io" / "aaif" / "goose" / "sdk_types" / "goose_sdk_types.kt"), + str(bindings / "io" / "aaif" / "goose" / "sdk" / "goose_sdk.kt"), + str(loader_kt), + ] + run(["kotlinc", "-cp", str(jna_jar), "-nowarn", "-d", str(classes), *sources]) + + for sub in classes.iterdir(): + dest = staging / sub.name + (shutil.copytree if sub.is_dir() else shutil.copy)(sub, dest) + + native = staging / "native" / os_arch + native.mkdir(parents=True) + shutil.copy(lib, native / lib_name) + + pom_dir = staging / "META-INF" / "maven" / GROUP / ARTIFACT + pom_dir.mkdir(parents=True) + (pom_dir / "pom.xml").write_text(POM_XML) + + dist = HERE / "dist" + shutil.rmtree(dist, ignore_errors=True) + dist.mkdir() + jar = dist / f"{ARTIFACT}-{VERSION}-{os_arch}.jar" + run(["jar", "--create", "--file", str(jar), "-C", str(staging), "."]) + + print(f"\nBuilt JAR: {jar}") + print(f"Runtime deps: net.java.dev.jna:jna:{JNA_VERSION}, kotlin-stdlib") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/crates/goose-sdk/packaging/python/.gitignore b/crates/goose-sdk/packaging/python/.gitignore new file mode 100644 index 000000000000..34e3aad2c2c7 --- /dev/null +++ b/crates/goose-sdk/packaging/python/.gitignore @@ -0,0 +1,5 @@ +.build/ +build/ +dist/ +src/ +*.egg-info/ diff --git a/crates/goose-sdk/packaging/python/make_wheel.py b/crates/goose-sdk/packaging/python/make_wheel.py new file mode 100644 index 000000000000..c519173c4e9f --- /dev/null +++ b/crates/goose-sdk/packaging/python/make_wheel.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Assemble the goose-sdk Python wheel (debug profile). + +Builds the cargo dylib + uniffi bindings, drops them into src/goose_sdk/, then +runs `python -m build --wheel` inside a throwaway venv (system Python is often +PEP 668 externally-managed). Output: packaging/python/dist/*.whl +""" + +from __future__ import annotations + +import platform +import shutil +import subprocess +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +REPO_ROOT = HERE.parents[3] +PROFILE_DIR = REPO_ROOT / "target" / "debug" +PKG_SRC = HERE / "src" / "goose_sdk" + +LIB_NAME = {"Darwin": "libgoose_sdk.dylib", "Windows": "goose_sdk.dll"}.get( + platform.system(), "libgoose_sdk.so" +) + +# Platform tag for the wheel; setuptools defaults to a pure-python tag, but we +# embed a native library, so force a platform-specific one. +def _plat_tag() -> str: + system, machine = platform.system(), platform.machine().lower() + arm = machine in ("arm64", "aarch64") + if system == "Darwin": + return f"macosx_{'11_0_arm64' if arm else '10_12_x86_64'}" + if system == "Linux": + return f"linux_{'aarch64' if arm else 'x86_64'}" + return "win_amd64" + + +def run(cmd: list[str], **kw) -> None: + print("$", *cmd, flush=True) + subprocess.run(cmd, check=True, **kw) + + +def main() -> int: + run(["cargo", "build", "-p", "goose-sdk", "--features", "uniffi"], cwd=REPO_ROOT) + + lib = PROFILE_DIR / LIB_NAME + bindgen = PROFILE_DIR / "goose-uniffi-bindgen" + for p in (lib, bindgen): + if not p.exists(): + sys.exit(f"missing: {p}") + + shutil.rmtree(PKG_SRC, ignore_errors=True) + PKG_SRC.mkdir(parents=True) + + run([str(bindgen), "generate", "--library", str(lib), "--language", "python", + "--no-format", "--out-dir", str(PKG_SRC)]) + shutil.copy(lib, PKG_SRC / lib.name) + (PKG_SRC / "__init__.py").write_text( + "from .goose_sdk import Agent, EventSink # noqa: F401\n" + "from . import goose_sdk_types # noqa: F401\n" + ) + + shutil.rmtree(HERE / "dist", ignore_errors=True) + + venv = HERE / ".build" / "venv" + venv_py = venv / ("Scripts" if platform.system() == "Windows" else "bin") / "python" + if not venv.exists(): + run([sys.executable, "-m", "venv", str(venv)]) + run([str(venv_py), "-m", "pip", "install", "-q", "build", "setuptools", "wheel"]) + + run([str(venv_py), "-m", "build", "--wheel", "--no-isolation", + "-C--build-option=--plat-name", f"-C--build-option={_plat_tag()}"], cwd=HERE) + + print("\nBuilt:", *sorted((HERE / "dist").glob("*.whl")), sep="\n ") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/crates/goose-sdk/packaging/python/pyproject.toml b/crates/goose-sdk/packaging/python/pyproject.toml new file mode 100644 index 000000000000..23a16f7080ac --- /dev/null +++ b/crates/goose-sdk/packaging/python/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "goose-sdk" +version = "0.1.0" +description = "Python bindings for the Goose agent (in-process via uniffi)" +readme = "README.md" +requires-python = ">=3.8" +license = { text = "Apache-2.0" } + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +goose_sdk = ["*.dylib", "*.so", "*.dll"] diff --git a/crates/goose-sdk/src/bin/uniffi-bindgen.rs b/crates/goose-sdk/src/bin/uniffi-bindgen.rs new file mode 100644 index 000000000000..f6cff6cf1d99 --- /dev/null +++ b/crates/goose-sdk/src/bin/uniffi-bindgen.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::uniffi_bindgen_main() +} diff --git a/crates/goose-sdk/src/bindings.rs b/crates/goose-sdk/src/bindings.rs new file mode 100644 index 000000000000..90ba93d12342 --- /dev/null +++ b/crates/goose-sdk/src/bindings.rs @@ -0,0 +1,264 @@ +//! In-process uniffi bindings for the Goose agent. + +use std::sync::Arc; +use std::sync::OnceLock; + +use futures::StreamExt; +use goose::agents::extension::{Envs, ExtensionConfig}; +use goose::agents::types::SessionConfig as CoreSessionConfig; +use goose::agents::{Agent as CoreAgent, AgentEvent as CoreAgentEvent}; +use goose::config::{Config, GooseMode, DEFAULT_EXTENSION_TIMEOUT}; +use goose::conversation::message::{Message, MessageContent}; +use goose::model::ModelConfig; +use goose::providers; +use goose::session::session_manager::SessionType; +use tokio::runtime::Runtime; + +pub use goose_sdk_types::{AgentEvent, ExtensionSpec, ProviderSpec}; + +static RUNTIME: OnceLock = OnceLock::new(); + +fn rt() -> &'static Runtime { + RUNTIME.get_or_init(|| Runtime::new().expect("failed to build tokio runtime")) +} + +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum GooseError { + #[error("{0}")] + Generic(String), +} + +macro_rules! err_from { + ($($t:ty),* $(,)?) => {$( + impl From<$t> for GooseError { + fn from(e: $t) -> Self { GooseError::Generic(e.to_string()) } + } + )*}; +} +err_from!(anyhow::Error, goose::model::ConfigError, std::io::Error); + +fn extension_spec_into_config(spec: ExtensionSpec) -> ExtensionConfig { + match spec { + ExtensionSpec::Builtin { name } => ExtensionConfig::Builtin { + name, + description: String::new(), + display_name: None, + timeout: Some(DEFAULT_EXTENSION_TIMEOUT), + bundled: Some(true), + available_tools: vec![], + }, + ExtensionSpec::Stdio { + name, + cmd, + args, + envs, + } => ExtensionConfig::Stdio { + name, + description: String::new(), + cmd, + args, + envs: Envs::new(envs), + env_keys: vec![], + timeout: Some(DEFAULT_EXTENSION_TIMEOUT), + bundled: None, + available_tools: vec![], + }, + ExtensionSpec::StreamableHttp { name, uri, headers } => ExtensionConfig::StreamableHttp { + name, + description: String::new(), + uri, + envs: Envs::new(std::collections::HashMap::new()), + env_keys: vec![], + headers, + timeout: Some(DEFAULT_EXTENSION_TIMEOUT), + socket: None, + bundled: None, + available_tools: vec![], + }, + } +} + +#[uniffi::export(callback_interface)] +pub trait EventSink: Send + Sync { + fn on_event(&self, event: AgentEvent); + fn on_error(&self, error: String); + fn on_done(&self); +} + +#[derive(uniffi::Object)] +pub struct Agent { + inner: Arc, + session_id: tokio::sync::Mutex>, +} + +#[uniffi::export] +impl Agent { + #[uniffi::constructor] + pub fn new() -> Arc { + let inner = rt().block_on(async { Arc::new(CoreAgent::new()) }); + Arc::new(Self { + inner, + session_id: tokio::sync::Mutex::new(None), + }) + } + + pub fn configure( + &self, + provider: ProviderSpec, + extensions: Vec, + ) -> Result { + rt().block_on(async { + let cfg = Config::global(); + let provider_name = provider + .name + .or_else(|| cfg.get_goose_provider().ok()) + .ok_or_else(|| { + GooseError::Generic( + "no provider: set ProviderSpec.name or run `goose configure`".into(), + ) + })?; + let model_name = provider + .model + .or_else(|| cfg.get_goose_model().ok()) + .ok_or_else(|| { + GooseError::Generic( + "no model: set ProviderSpec.model or run `goose configure`".into(), + ) + })?; + + let cwd = std::env::current_dir()?; + let session = self + .inner + .config + .session_manager + .create_session( + cwd, + "uniffi-sdk".to_string(), + SessionType::User, + GooseMode::default(), + ) + .await?; + + let ext_configs: Vec = extensions + .into_iter() + .map(extension_spec_into_config) + .collect(); + + let model_config = ModelConfig::new(&model_name)?.with_canonical_limits(&provider_name); + let prov = providers::create(&provider_name, model_config, ext_configs.clone()).await?; + self.inner.update_provider(prov, &session.id).await?; + + if !ext_configs.is_empty() { + let results = self + .inner + .add_extensions_bulk(ext_configs, &session.id) + .await?; + for r in &results { + if !r.success { + return Err(GooseError::Generic(format!( + "extension {} failed to load: {}", + r.name, + r.error.clone().unwrap_or_default() + ))); + } + } + } + + *self.session_id.lock().await = Some(session.id.clone()); + Ok(session.id) + }) + } + + pub fn reply(&self, prompt: String, sink: Box) -> Result<(), GooseError> { + rt().block_on(async { + let session_id = self + .session_id + .lock() + .await + .clone() + .ok_or_else(|| GooseError::Generic("call configure() first".into()))?; + + let session_config = CoreSessionConfig { + id: session_id, + schedule_id: None, + max_turns: None, + retry_config: None, + }; + + let user_message = Message::user().with_text(&prompt); + + let mut stream = self.inner.reply(user_message, session_config, None).await?; + + while let Some(item) = stream.next().await { + match item { + Ok(CoreAgentEvent::Message(msg)) => { + for content in &msg.content { + if let Some(ev) = content_to_event(content) { + sink.on_event(ev); + } + } + } + Ok(_) => {} + Err(e) => { + sink.on_error(e.to_string()); + return Err(e.into()); + } + } + } + sink.on_done(); + Ok(()) + }) + } +} + +fn content_to_event(content: &MessageContent) -> Option { + use rmcp::model::RawContent; + match content { + MessageContent::Text(t) => Some(AgentEvent::AssistantText { + text: t.text.clone(), + }), + MessageContent::Thinking(t) => Some(AgentEvent::Thinking { + text: t.thinking.clone(), + }), + MessageContent::ToolRequest(req) => { + let (name, arguments) = match &req.tool_call { + Ok(call) => ( + call.name.to_string(), + call.arguments + .as_ref() + .map(|a| serde_json::to_string(a).unwrap_or_default()) + .unwrap_or_default(), + ), + Err(e) => ("".into(), e.to_string()), + }; + Some(AgentEvent::ToolRequest { + id: req.id.clone(), + name, + arguments, + }) + } + MessageContent::ToolResponse(resp) => { + let (output, is_error) = match &resp.tool_result { + Ok(result) => { + let text = result + .content + .iter() + .filter_map(|c| match &c.raw { + RawContent::Text(t) => Some(t.text.clone()), + _ => None, + }) + .collect::>() + .join("\n"); + (text, result.is_error.unwrap_or(false)) + } + Err(e) => (e.to_string(), true), + }; + Some(AgentEvent::ToolResponse { + id: resp.id.clone(), + output, + is_error, + }) + } + _ => None, + } +} diff --git a/crates/goose-sdk/src/lib.rs b/crates/goose-sdk/src/lib.rs index 6c1c1bf50c05..3038c6f7d081 100644 --- a/crates/goose-sdk/src/lib.rs +++ b/crates/goose-sdk/src/lib.rs @@ -1 +1,10 @@ -pub mod custom_requests; +//! Goose SDK. + +pub use goose_sdk_types::custom_requests; +pub use goose_sdk_types::{AgentEvent, ExtensionSpec, ProviderSpec}; + +#[cfg(feature = "uniffi")] +uniffi::setup_scaffolding!(); + +#[cfg(feature = "uniffi")] +pub mod bindings; diff --git a/crates/goose-sdk/uniffi.toml b/crates/goose-sdk/uniffi.toml new file mode 100644 index 000000000000..3fc391a5413b --- /dev/null +++ b/crates/goose-sdk/uniffi.toml @@ -0,0 +1,4 @@ +[bindings.kotlin] +package_name = "io.aaif.goose.sdk" + +[bindings.python] diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 4b211a0ed380..ea2ecce4bcd8 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -126,7 +126,7 @@ strum = { workspace = true } once_cell = { workspace = true } etcetera = { workspace = true } fs-err = "3" -goose-sdk = { path = "../goose-sdk" } +goose-sdk-types = { path = "../goose-sdk-types" } rand = { workspace = true } utoipa = { workspace = true, features = ["chrono"] } tokio-cron-scheduler = "0.15.1" diff --git a/crates/goose/src/acp/mod.rs b/crates/goose/src/acp/mod.rs index 594c14cac10e..e8d4062d2f8c 100644 --- a/crates/goose/src/acp/mod.rs +++ b/crates/goose/src/acp/mod.rs @@ -9,7 +9,7 @@ pub(crate) mod tools; pub mod transport; pub use common::{map_permission_response, PermissionDecision}; -pub use goose_sdk::custom_requests; +pub use goose_sdk_types::custom_requests; pub use provider::{ extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL, }; diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index 1beeb79f8bae..6cd86e3017b2 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -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, diff --git a/crates/goose/src/checks/mod.rs b/crates/goose/src/checks/mod.rs index 172ca72b5106..8f0ac45853d5 100644 --- a/crates/goose/src/checks/mod.rs +++ b/crates/goose/src/checks/mod.rs @@ -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; diff --git a/crates/goose/src/lib.rs b/crates/goose/src/lib.rs index 02696312c942..af7de5442cc4 100644 --- a/crates/goose/src/lib.rs +++ b/crates/goose/src/lib.rs @@ -5,7 +5,7 @@ compile_error!("At least one of `rustls-tls` or `native-tls` features must be en compile_error!("Features `rustls-tls` and `native-tls` are mutually exclusive"); pub mod acp; -pub use goose_sdk::custom_requests; +pub use goose_sdk_types::custom_requests; pub mod action_required_manager; pub mod agents; pub mod builtin_extension; diff --git a/crates/goose/src/skills/client.rs b/crates/goose/src/skills/client.rs index e26f37dac581..d23bf5c02234 100644 --- a/crates/goose/src/skills/client.rs +++ b/crates/goose/src/skills/client.rs @@ -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, diff --git a/crates/goose/src/skills/mod.rs b/crates/goose/src/skills/mod.rs index bc73e86803b8..1645b0d03316 100644 --- a/crates/goose/src/skills/mod.rs +++ b/crates/goose/src/skills/mod.rs @@ -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}; diff --git a/crates/goose/src/slash_commands/skill_slash_command.rs b/crates/goose/src/slash_commands/skill_slash_command.rs index 095cb244ee92..f85554dcb239 100644 --- a/crates/goose/src/slash_commands/skill_slash_command.rs +++ b/crates/goose/src/slash_commands/skill_slash_command.rs @@ -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) -> Vec