mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
Add SDK PoC: uniffi bindings for goose agent core
Exposes a minimal Agent API (configure + reply loop) over uniffi, producing Python / Kotlin bindings from one cdylib. Surface: - Agent::new() / configure(ProviderSpec) / reply(prompt, EventSink) - EventSink callback interface for streaming AgentEvents
This commit is contained in:
Generated
+303
-14
@@ -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,7 +4693,7 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"env-lock",
|
||||
"etcetera 0.11.0",
|
||||
"fs-err",
|
||||
"fs-err 3.3.0",
|
||||
"fs2",
|
||||
"futures",
|
||||
"goose-acp-macros",
|
||||
@@ -4854,6 +4961,21 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "goose-uniffi"
|
||||
version = "1.34.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"futures",
|
||||
"goose",
|
||||
"rmcp",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.13.0"
|
||||
@@ -5710,7 +5832,7 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6602,7 +6724,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 +8735,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8672,7 +8794,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 +8986,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 +9099,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 +9661,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 +10806,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 +11349,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 +12018,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 +12563,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 +12643,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 +13128,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 +13148,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]]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/*",
|
||||
"sdk",
|
||||
# Mainly for cargo-machete to not error out during inspection.
|
||||
"vendor/v8",
|
||||
]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
generated/
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "goose-uniffi"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Multi-language bindings (Python, Kotlin) for the Goose agent core via UniFFI"
|
||||
|
||||
[lib]
|
||||
name = "goose_uniffi"
|
||||
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "goose-uniffi-bindgen"
|
||||
path = "src/bin/uniffi-bindgen.rs"
|
||||
|
||||
[dependencies]
|
||||
goose = { path = "../crates/goose" }
|
||||
uniffi = { version = "0.29", features = ["tokio", "cli"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync"] }
|
||||
futures = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = "2"
|
||||
async-trait = { workspace = true }
|
||||
rmcp = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
# goose-uniffi
|
||||
|
||||
PoC multi-language SDK for the Goose Rust agent core, using [UniFFI](https://mozilla.github.io/uniffi-rs/).
|
||||
|
||||
This crate compiles the `goose` Rust agent into one dynamic library plus generated bindings for **Python** and **Kotlin**. The same `libgoose_uniffi` powers every language, so adding a feature in Rust ships to all of them.
|
||||
|
||||
> **TypeScript:** UniFFI core does not ship a TS backend, and the React-Native-only `uniffi-bindgen-react-native` doesn't fit a plain-Node use case. For TS clients today, use the existing ACP-over-stdio path in [`crates/goose-sdk`](../crates/goose-sdk) / [`ui/sdk`](../ui/sdk) instead.
|
||||
|
||||
This sits next to two other client paths:
|
||||
|
||||
| Path | Transport | Used by |
|
||||
|---|---|---|
|
||||
| v1 | HTTP (`goose-server`) | Electron desktop |
|
||||
| v2 | ACP over stdio/HTTP/WS (`crates/goose/src/acp`) | `ui/text` TUI, `ui/sdk` TS client, `crates/goose-sdk` Rust client |
|
||||
| **this PoC** | **In-process FFI** (uniffi) | Any host language embedding goose |
|
||||
|
||||
## API surface
|
||||
|
||||
Deliberately tiny:
|
||||
|
||||
```rust
|
||||
Agent::new()
|
||||
Agent::configure(ProviderSpec, Vec<ExtensionSpec>) -> session_id
|
||||
Agent::reply(prompt, EventSink) // streaming via callback
|
||||
Agent::reply_collect(prompt) -> Vec<AgentEvent> // one-shot
|
||||
```
|
||||
|
||||
`EventSink` is a foreign callback interface — the host implements it, Rust invokes it for every assistant chunk, tool request, tool response, thinking, plus `on_done` / `on_error`.
|
||||
|
||||
## Quick start
|
||||
|
||||
Each example sends the prompt `"ping apple.com"` to a configured agent and prints the streamed response.
|
||||
|
||||
```bash
|
||||
cd sdk
|
||||
just # list recipes
|
||||
just python # build, generate bindings, run ping_apple.py
|
||||
just kotlin # build, generate bindings, fetch jna, compile, run PingApple.kt
|
||||
```
|
||||
|
||||
Prereqs: a Rust toolchain, `python3`, `kotlinc` (`brew install kotlin`) + a JDK, and one `goose configure`-d provider on your machine. Override at runtime with `GOOSE_PROVIDER=openai GOOSE_MODEL=gpt-4o`.
|
||||
|
||||
## Code examples
|
||||
|
||||
All three speak the same shape: build an `Agent`, `configure` it with a provider, implement an `EventSink`, call `reply`.
|
||||
|
||||
`AgentEvent` is a tagged union — `AssistantText { text }`, `Thinking { text }`, `ToolRequest { id, name, arguments }`, `ToolResponse { id, output, isError }`.
|
||||
|
||||
### Python — [`examples/ping_apple.py`](examples/ping_apple.py)
|
||||
|
||||
```python
|
||||
from goose_uniffi import Agent, ProviderSpec, ExtensionSpec, EventSink, AgentEvent
|
||||
|
||||
class Printer(EventSink):
|
||||
def on_event(self, event):
|
||||
if isinstance(event, AgentEvent.ASSISTANT_TEXT):
|
||||
print(event.text, end="", flush=True)
|
||||
elif isinstance(event, AgentEvent.TOOL_REQUEST):
|
||||
print(f"→ {event.name} {event.arguments}")
|
||||
def on_error(self, error): print(f"error: {error}")
|
||||
def on_done(self): pass
|
||||
|
||||
agent = Agent()
|
||||
agent.configure(
|
||||
ProviderSpec(name="anthropic", model="claude-sonnet-4-5"),
|
||||
[ExtensionSpec.BUILTIN(name="developer")],
|
||||
)
|
||||
agent.reply("ping apple.com", Printer())
|
||||
```
|
||||
|
||||
Run with `just python`.
|
||||
|
||||
### Kotlin — [`examples/PingApple.kt`](examples/PingApple.kt)
|
||||
|
||||
```kotlin
|
||||
import uniffi.goose_uniffi.*
|
||||
|
||||
class Printer : EventSink {
|
||||
override fun onEvent(event: AgentEvent) = when (event) {
|
||||
is AgentEvent.AssistantText -> print(event.text)
|
||||
is AgentEvent.ToolRequest -> println("→ ${event.name} ${event.arguments}")
|
||||
else -> {}
|
||||
}
|
||||
override fun onError(error: String) = System.err.println("error: $error")
|
||||
override fun onDone() {}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val agent = Agent()
|
||||
agent.configure(
|
||||
ProviderSpec(name = "anthropic", model = "claude-sonnet-4-5"),
|
||||
listOf(ExtensionSpec.Builtin(name = "developer")),
|
||||
)
|
||||
agent.reply("ping apple.com", Printer())
|
||||
}
|
||||
```
|
||||
|
||||
Run with `just kotlin`.
|
||||
|
||||
## Out of scope (for now)
|
||||
|
||||
Extension/MCP wiring, permission/elicitation callbacks, session resume, recipes, hooks, mode switching, mid-stream cancellation. All present in the underlying `goose` crate — just not bound yet.
|
||||
@@ -0,0 +1,83 @@
|
||||
package examples
|
||||
|
||||
import uniffi.goose_uniffi.Agent
|
||||
import uniffi.goose_uniffi.AgentEvent
|
||||
import uniffi.goose_uniffi.EventSink
|
||||
import uniffi.goose_uniffi.ExtensionSpec
|
||||
import uniffi.goose_uniffi.ProviderSpec
|
||||
|
||||
/** Minimal goose SDK demo: ask the agent to ping apple.com. */
|
||||
|
||||
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 apple.com".paint(Style.DIM) + "\n")
|
||||
agent.reply("ping apple.com", Printer())
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
# Examples
|
||||
|
||||
All three examples do the same thing: build an `Agent`, configure it with a provider, and send the prompt `"ping apple.com"`. The agent will call the `developer__shell` tool (or equivalent) and stream the output back.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
# 1. From the repo root, build the cdylib and bindgen binary:
|
||||
cargo build -p goose-uniffi
|
||||
|
||||
# 2. Configure a goose provider (only needs to be done once on your machine):
|
||||
goose configure
|
||||
|
||||
# 3. Generate bindings for whichever languages you want to use:
|
||||
LIB=./target/debug/libgoose_uniffi.dylib # .so on Linux, .dll on Windows
|
||||
./target/debug/goose-uniffi-bindgen generate --library $LIB --language python --out-dir ./sdk/generated
|
||||
./target/debug/goose-uniffi-bindgen generate --library $LIB --language kotlin --out-dir ./sdk/generated
|
||||
```
|
||||
|
||||
All examples assume `libgoose_uniffi.{dylib,so}` is on the dynamic-library load path. The snippets below set that automatically.
|
||||
|
||||
---
|
||||
|
||||
## Python
|
||||
|
||||
**File:** `ping_apple.py`
|
||||
|
||||
```bash
|
||||
DYLD_LIBRARY_PATH=./target/debug \
|
||||
LD_LIBRARY_PATH=./target/debug \
|
||||
python3 sdk/examples/ping_apple.py
|
||||
```
|
||||
|
||||
Requires Python 3.8+. No pip packages needed — `ctypes` ships with the stdlib.
|
||||
|
||||
---
|
||||
|
||||
## Kotlin
|
||||
|
||||
**File:** `PingApple.kt`
|
||||
|
||||
Compile and run via `kotlinc` + `java`:
|
||||
|
||||
```bash
|
||||
# Compile (jna is needed by the uniffi-generated kotlin runtime)
|
||||
JNA_JAR=$(find ~/.gradle /opt/homebrew /usr/local -name 'jna-5.*.jar' 2>/dev/null | head -1)
|
||||
# If you don't have jna locally, grab it:
|
||||
# curl -L -o jna.jar https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar
|
||||
# JNA_JAR=./jna.jar
|
||||
|
||||
kotlinc -cp "$JNA_JAR" \
|
||||
sdk/generated/uniffi/goose_uniffi/goose_uniffi.kt \
|
||||
sdk/examples/PingApple.kt \
|
||||
-include-runtime -d ping_apple.jar
|
||||
|
||||
DYLD_LIBRARY_PATH=./target/debug \
|
||||
LD_LIBRARY_PATH=./target/debug \
|
||||
java -cp "ping_apple.jar:$JNA_JAR" examples.PingAppleKt
|
||||
```
|
||||
|
||||
Requires `kotlinc` (`brew install kotlin`) and a JDK.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- The first run creates a session under `~/.local/share/goose/sessions/`. Subsequent runs make new sessions; this PoC doesn't expose resume.
|
||||
- Provider/model come from `~/.config/goose/config.yaml` by default. Override per-run with `GOOSE_PROVIDER=openai GOOSE_MODEL=gpt-4o just python`.
|
||||
- Each example loads the `developer` builtin extension so the agent can run shell commands like `ping`.
|
||||
|
||||
## TypeScript
|
||||
|
||||
Not supported by this SDK. UniFFI core has no TypeScript backend, and the React-Native-only `uniffi-bindgen-react-native` doesn't fit a plain-Node use case. For TS, use the existing ACP path in [`../../crates/goose-sdk`](../../crates/goose-sdk) and [`../../ui/sdk`](../../ui/sdk) instead.
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Minimal goose SDK demo: ask the agent to ping apple.com."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE.parent / "generated"))
|
||||
|
||||
from goose_uniffi import Agent, AgentEvent, EventSink, ExtensionSpec, ProviderSpec # noqa: E402
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Style:
|
||||
dim: str = "\033[2m"
|
||||
cyan: str = "\033[36m"
|
||||
green: str = "\033[32m"
|
||||
red: str = "\033[31m"
|
||||
reset: str = "\033[0m"
|
||||
|
||||
def paint(self, color: str, text: str) -> str:
|
||||
return f"{color}{text}{self.reset}"
|
||||
|
||||
|
||||
S = Style()
|
||||
|
||||
|
||||
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):
|
||||
"""Pretty-prints agent events to the terminal."""
|
||||
|
||||
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"{S.paint(S.cyan, '→ ' + event.name)} {S.paint(S.dim, args)}", flush=True)
|
||||
|
||||
elif isinstance(event, AgentEvent.TOOL_RESPONSE):
|
||||
color = S.red if event.is_error else S.green
|
||||
marker = "✗" if event.is_error else "✓"
|
||||
print(f"{S.paint(color, marker)} {S.paint(S.dim, _preview(event.output))}\n", flush=True)
|
||||
|
||||
def on_error(self, error: str) -> None:
|
||||
print(f"\n{S.paint(S.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(S.paint(S.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(S.paint(S.dim, "> ping apple.com") + "\n", file=sys.stderr)
|
||||
agent.reply("ping apple.com", Printer())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,66 @@
|
||||
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_uniffi." + lib_ext
|
||||
bindgen := "./target/debug/goose-uniffi-bindgen"
|
||||
gen_dir := "./sdk/generated"
|
||||
|
||||
# ANSI helpers
|
||||
bold := '\033[1m'
|
||||
dim := '\033[2m'
|
||||
cyan := '\033[36m'
|
||||
reset := '\033[0m'
|
||||
|
||||
default:
|
||||
@just --list --justfile {{justfile()}}
|
||||
|
||||
# ─── shared steps ────────────────────────────────────────────────
|
||||
|
||||
_build:
|
||||
@printf "{{dim}}┌─ building goose-uniffi (rust → cdylib){{reset}}\n"
|
||||
@printf "{{dim}}│ cargo build -p goose-uniffi{{reset}}\n"
|
||||
@cargo build -p goose-uniffi -q
|
||||
@printf "{{dim}}└─ {{reset}}{{bold}}libgoose_uniffi.{{lib_ext}}{{reset}} {{dim}}ready{{reset}}\n\n"
|
||||
|
||||
_generate lang: _build
|
||||
@printf "{{dim}}┌─ generating {{lang}} bindings via uniffi-bindgen{{reset}}\n"
|
||||
@printf "{{dim}}│ goose-uniffi-bindgen generate --language {{lang}}{{reset}}\n"
|
||||
@{{bindgen}} generate --library {{lib_path}} --language {{lang}} --no-format --out-dir {{gen_dir}} 2>/dev/null
|
||||
@printf "{{dim}}└─ bindings written to {{gen_dir}}{{reset}}\n\n"
|
||||
|
||||
_header lang:
|
||||
@printf "\n{{bold}}{{cyan}}━━━ goose-uniffi · {{lang}} demo ━━━{{reset}}\n"
|
||||
@printf "{{dim}}prompt: \"ping apple.com\" · extension: developer (builtin){{reset}}\n\n"
|
||||
|
||||
# ─── language demos ──────────────────────────────────────────────
|
||||
|
||||
python: (_header "python") (_generate "python")
|
||||
@printf "{{dim}}┌─ running python example{{reset}}\n"
|
||||
@printf "{{dim}}│ python3 sdk/examples/ping_apple.py{{reset}}\n"
|
||||
@printf "{{dim}}└──────────────────────────────────────{{reset}}\n\n"
|
||||
@DYLD_LIBRARY_PATH={{lib_dir}} LD_LIBRARY_PATH={{lib_dir}} \
|
||||
python3 sdk/examples/ping_apple.py
|
||||
|
||||
kotlin: (_header "kotlin") (_generate "kotlin")
|
||||
@if [ ! -f sdk/examples/jna.jar ]; then \
|
||||
printf "{{dim}}┌─ downloading jna.jar{{reset}}\n"; \
|
||||
curl -sSL -o sdk/examples/jna.jar \
|
||||
https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar; \
|
||||
printf "{{dim}}└─ jna.jar ready{{reset}}\n\n"; \
|
||||
fi
|
||||
@printf "{{dim}}┌─ compiling kotlin{{reset}}\n"
|
||||
@printf "{{dim}}│ kotlinc PingApple.kt + generated bindings{{reset}}\n"
|
||||
@kotlinc -cp sdk/examples/jna.jar -nowarn \
|
||||
{{gen_dir}}/uniffi/goose_uniffi/goose_uniffi.kt \
|
||||
sdk/examples/PingApple.kt \
|
||||
-include-runtime -d sdk/examples/ping_apple.jar 2>/dev/null
|
||||
@printf "{{dim}}└─ ping_apple.jar built{{reset}}\n\n"
|
||||
@printf "{{dim}}┌─ running kotlin example{{reset}}\n"
|
||||
@printf "{{dim}}│ java -cp ping_apple.jar examples.PingAppleKt{{reset}}\n"
|
||||
@printf "{{dim}}└──────────────────────────────────────{{reset}}\n\n"
|
||||
@java -Djna.library.path={{lib_dir}} \
|
||||
--enable-native-access=ALL-UNNAMED \
|
||||
-cp sdk/examples/ping_apple.jar:sdk/examples/jna.jar examples.PingAppleKt
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
uniffi::uniffi_bindgen_main()
|
||||
}
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
use std::collections::HashMap;
|
||||
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;
|
||||
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
static RUNTIME: OnceLock<Runtime> = 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);
|
||||
|
||||
#[derive(uniffi::Record, Clone)]
|
||||
pub struct ProviderSpec {
|
||||
pub name: Option<String>,
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum, Clone)]
|
||||
pub enum ExtensionSpec {
|
||||
Builtin {
|
||||
name: String,
|
||||
},
|
||||
Stdio {
|
||||
name: String,
|
||||
cmd: String,
|
||||
args: Vec<String>,
|
||||
envs: HashMap<String, String>,
|
||||
},
|
||||
StreamableHttp {
|
||||
name: String,
|
||||
uri: String,
|
||||
headers: HashMap<String, String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ExtensionSpec {
|
||||
fn into_config(self) -> ExtensionConfig {
|
||||
match self {
|
||||
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(HashMap::new()),
|
||||
env_keys: vec![],
|
||||
headers,
|
||||
timeout: Some(DEFAULT_EXTENSION_TIMEOUT),
|
||||
socket: None,
|
||||
bundled: None,
|
||||
available_tools: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum, Clone, Debug)]
|
||||
pub enum AgentEvent {
|
||||
AssistantText {
|
||||
text: String,
|
||||
},
|
||||
Thinking {
|
||||
text: String,
|
||||
},
|
||||
ToolRequest {
|
||||
id: String,
|
||||
name: String,
|
||||
arguments: String,
|
||||
},
|
||||
ToolResponse {
|
||||
id: String,
|
||||
output: String,
|
||||
is_error: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[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<CoreAgent>,
|
||||
session_id: tokio::sync::Mutex<Option<String>>,
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl Agent {
|
||||
#[uniffi::constructor]
|
||||
pub fn new() -> Arc<Self> {
|
||||
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<ExtensionSpec>,
|
||||
) -> Result<String, GooseError> {
|
||||
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<ExtensionConfig> =
|
||||
extensions.into_iter().map(|e| e.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_collect(&self, prompt: String) -> Result<Vec<AgentEvent>, 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 mut stream = self
|
||||
.inner
|
||||
.reply(Message::user().with_text(&prompt), session_config, None)
|
||||
.await?;
|
||||
let mut out = Vec::new();
|
||||
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) {
|
||||
out.push(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reply(&self, prompt: String, sink: Box<dyn EventSink>) -> 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<AgentEvent> {
|
||||
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) => ("<error>".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::<Vec<_>>()
|
||||
.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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user