diff --git a/.gitignore b/.gitignore index bb1fcc215d..a4ac78943f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.jar run_cli.sh tokenizer_files/ .DS_Store diff --git a/Cargo.lock b/Cargo.lock index b70c2a5503..349943abf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,6 +201,48 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[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.99", +] + +[[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", +] + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -211,6 +253,19 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-compat" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bab94bde396a3f7b4962e396fdad640e241ed797d4d8d77fc8c237d14c58fc0" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-compression" version = "0.4.20" @@ -843,6 +898,15 @@ dependencies = [ "vsimd", ] +[[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.24.0" @@ -1086,6 +1150,38 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d2c12f985c78475a6b8d629afd0c360260ef34cfef52efccdcfd31972f81c2e" +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[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.12", +] + [[package]] name = "cast" version = "0.3.0" @@ -2100,6 +2196,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + [[package]] name = "fs2" version = "0.4.3" @@ -2302,6 +2407,17 @@ dependencies = [ "regex-syntax 0.8.5", ] +[[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 = "google-apis-common" version = "7.0.0" @@ -2544,6 +2660,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tracing", + "uniffi", "url", ] @@ -4413,6 +4530,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.7.0" @@ -5279,6 +5402,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.99", +] + [[package]] name = "sct" version = "0.7.1" @@ -5342,6 +5485,9 @@ name = "semver" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] [[package]] name = "serde" @@ -5579,6 +5725,12 @@ dependencies = [ "time", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -5631,6 +5783,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "std_prelude" version = "0.2.12" @@ -6464,6 +6622,128 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "uniffi" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd1d240101ba3b9d7532ae86d9cb64d9a7ff63e13a2b7b9e94a32a601d8233" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap 4.5.31", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0525f06d749ea80d8049dc0bb038bb87941e3d909eefa76b6f0a5589b59ac5" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck 0.5.0", + "indexmap 2.7.1", + "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fa8eb4d825b4ed095cb13483cba6927c3002b9eb603cef9b7688758cc3772e" +dependencies = [ + "anyhow", + "async-compat", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83b547d69d699e52f2129fde4b57ae0d00b5216e59ed5b56097c95c86ba06095" +dependencies = [ + "anyhow", + "indexmap 2.7.1", + "proc-macro2", + "quote", + "syn 2.0.99", +] + +[[package]] +name = "uniffi_macros" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f1de72edc8cb9201c7d650e3678840d143e4499004571aac49e6cb1b17da43" +dependencies = [ + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 2.0.99", + "toml 0.5.11", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acc9204632f6a555b2cba7c8852c5523bc1aa5f3eff605c64af5054ea28b72e" +dependencies = [ + "anyhow", + "siphasher", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b5336a9a925b358183837d31541d12590b7fcec373256d3770de02dff24c69" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.7.1", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f95e73373d85f04736bc51997d3e6855721144ec4384cae9ca8513c80615e129" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -6757,6 +7037,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", +] + [[package]] name = "weezl" version = "0.1.8" diff --git a/bindings/kotlin/example/Usage.kt b/bindings/kotlin/example/Usage.kt new file mode 100644 index 0000000000..0f789ee01a --- /dev/null +++ b/bindings/kotlin/example/Usage.kt @@ -0,0 +1,130 @@ +import kotlinx.coroutines.runBlocking +import uniffi.goose_llm.* + +fun main() = runBlocking { + val now = System.currentTimeMillis() / 1000 + val msgs = listOf( + // 1) User sends a plain-text prompt + Message( + role = Role.USER, + created = now, + content = listOf( + MessageContent.Text( + TextContent("What is 7 x 6?") + ) + ) + ), + + // 2) Assistant makes a tool request (ToolReq) to calculate 7×6 + Message( + role = Role.ASSISTANT, + created = now + 2, + content = listOf( + MessageContent.ToolReq( + ToolRequest( + id = "calc1", + toolCall = """ + { + "status": "success", + "value": { + "name": "calculator_extension__toolname", + "arguments": { + "operation": "multiply", + "numbers": [7, 6] + }, + "needsApproval": false + } + } + """.trimIndent() + ) + ) + ) + ), + + // 3) User (on behalf of the tool) responds with the tool result (ToolResp) + Message( + role = Role.USER, + created = now + 3, + content = listOf( + MessageContent.ToolResp( + ToolResponse( + id = "calc1", + toolResult = """ + { + "status": "success", + "value": [ + {"type": "text", "text": "42"} + ] + } + """.trimIndent() + ) + ) + ) + ), + ) + + printMessages(msgs) + println("---\n") + + val sessionName = generateSessionName(msgs) + println("Session Name: $sessionName") + + val tooltip = generateTooltip(msgs) + println("Tooltip: $tooltip") + + // Completion + val provider = "databricks" + val modelName = "goose-gpt-4-1" + val modelConfig = ModelConfig( + modelName, + 100000u, // UInt + 0.1f, // Float + 200 // Int + ) + + val calculatorTool = createToolConfig( + name = "calculator", + description = "Perform basic arithmetic operations", + inputSchema = """ + { + "type": "object", + "required": ["operation", "numbers"], + "properties": { + "operation": { + "type": "string", + "enum": ["add", "subtract", "multiply", "divide"], + "description": "The arithmetic operation to perform" + }, + "numbers": { + "type": "array", + "items": { "type": "number" }, + "description": "List of numbers to operate on in order" + } + } + } + """.trimIndent(), + approvalMode = ToolApprovalMode.AUTO + ) + + val calculator_extension = ExtensionConfig( + name = "calculator_extension", + instructions = "This extension provides a calculator tool.", + tools = listOf(calculatorTool) + ) + + val extensions = listOf(calculator_extension) + val systemPreamble = "You are a helpful assistant." + + + val req = CompletionRequest( + provider, + modelConfig, + systemPreamble, + msgs, + extensions + ) + + val response = completion(req) + println("\nCompletion Response:") + println(response.message) +} \ No newline at end of file diff --git a/bindings/kotlin/uniffi/goose_llm/goose_llm.kt b/bindings/kotlin/uniffi/goose_llm/goose_llm.kt new file mode 100644 index 0000000000..76e8197a2a --- /dev/null +++ b/bindings/kotlin/uniffi/goose_llm/goose_llm.kt @@ -0,0 +1,2933 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +@file:Suppress("NAME_SHADOWING") + +package uniffi.goose_llm + +// Common helper code. +// +// Ideally this would live in a separate .kt file where it can be unittested etc +// in isolation, and perhaps even published as a re-useable package. +// +// However, it's important that the details of how this helper code works (e.g. the +// way that different builtin types are passed across the FFI) exactly match what's +// expected by the Rust code on the other side of the interface. In practice right +// now that means coming from the exact some version of `uniffi` that was used to +// compile the Rust component. The easiest way to ensure this is to bundle the Kotlin +// helpers directly inline like we're doing here. + +import com.sun.jna.Callback +import com.sun.jna.Library +import com.sun.jna.Native +import com.sun.jna.Pointer +import com.sun.jna.Structure +import com.sun.jna.ptr.* +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.CharBuffer +import java.nio.charset.CodingErrorAction +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong +import kotlin.coroutines.resume + +// This is a helper for safely working with byte buffers returned from the Rust code. +// A rust-owned buffer is represented by its capacity, its current length, and a +// pointer to the underlying data. + +/** + * @suppress + */ +@Structure.FieldOrder("capacity", "len", "data") +open class RustBuffer : Structure() { + // Note: `capacity` and `len` are actually `ULong` values, but JVM only supports signed values. + // When dealing with these fields, make sure to call `toULong()`. + @JvmField var capacity: Long = 0 + + @JvmField var len: Long = 0 + + @JvmField var data: Pointer? = null + + class ByValue : + RustBuffer(), + Structure.ByValue + + class ByReference : + RustBuffer(), + Structure.ByReference + + internal fun setValue(other: RustBuffer) { + capacity = other.capacity + len = other.len + data = other.data + } + + companion object { + internal fun alloc(size: ULong = 0UL) = + uniffiRustCall { status -> + // Note: need to convert the size to a `Long` value to make this work with JVM. + UniffiLib.INSTANCE.ffi_goose_llm_rustbuffer_alloc(size.toLong(), status) + }.also { + if (it.data == null) { + throw RuntimeException("RustBuffer.alloc() returned null data pointer (size=$size)") + } + } + + internal fun create( + capacity: ULong, + len: ULong, + data: Pointer?, + ): RustBuffer.ByValue { + var buf = RustBuffer.ByValue() + buf.capacity = capacity.toLong() + buf.len = len.toLong() + buf.data = data + return buf + } + + internal fun free(buf: RustBuffer.ByValue) = + uniffiRustCall { status -> + UniffiLib.INSTANCE.ffi_goose_llm_rustbuffer_free(buf, status) + } + } + + @Suppress("TooGenericExceptionThrown") + fun asByteBuffer() = + this.data?.getByteBuffer(0, this.len.toLong())?.also { + it.order(ByteOrder.BIG_ENDIAN) + } +} + +/** + * The equivalent of the `*mut RustBuffer` type. + * Required for callbacks taking in an out pointer. + * + * Size is the sum of all values in the struct. + * + * @suppress + */ +class RustBufferByReference : ByReference(16) { + /** + * Set the pointed-to `RustBuffer` to the given value. + */ + fun setValue(value: RustBuffer.ByValue) { + // NOTE: The offsets are as they are in the C-like struct. + val pointer = getPointer() + pointer.setLong(0, value.capacity) + pointer.setLong(8, value.len) + pointer.setPointer(16, value.data) + } + + /** + * Get a `RustBuffer.ByValue` from this reference. + */ + fun getValue(): RustBuffer.ByValue { + val pointer = getPointer() + val value = RustBuffer.ByValue() + value.writeField("capacity", pointer.getLong(0)) + value.writeField("len", pointer.getLong(8)) + value.writeField("data", pointer.getLong(16)) + + return value + } +} + +// This is a helper for safely passing byte references into the rust code. +// It's not actually used at the moment, because there aren't many things that you +// can take a direct pointer to in the JVM, and if we're going to copy something +// then we might as well copy it into a `RustBuffer`. But it's here for API +// completeness. + +@Structure.FieldOrder("len", "data") +internal open class ForeignBytes : Structure() { + @JvmField var len: Int = 0 + + @JvmField var data: Pointer? = null + + class ByValue : + ForeignBytes(), + Structure.ByValue +} + +/** + * The FfiConverter interface handles converter types to and from the FFI + * + * All implementing objects should be public to support external types. When a + * type is external we need to import it's FfiConverter. + * + * @suppress + */ +public interface FfiConverter { + // Convert an FFI type to a Kotlin type + fun lift(value: FfiType): KotlinType + + // Convert an Kotlin type to an FFI type + fun lower(value: KotlinType): FfiType + + // Read a Kotlin type from a `ByteBuffer` + fun read(buf: ByteBuffer): KotlinType + + // Calculate bytes to allocate when creating a `RustBuffer` + // + // This must return at least as many bytes as the write() function will + // write. It can return more bytes than needed, for example when writing + // Strings we can't know the exact bytes needed until we the UTF-8 + // encoding, so we pessimistically allocate the largest size possible (3 + // bytes per codepoint). Allocating extra bytes is not really a big deal + // because the `RustBuffer` is short-lived. + fun allocationSize(value: KotlinType): ULong + + // Write a Kotlin type to a `ByteBuffer` + fun write( + value: KotlinType, + buf: ByteBuffer, + ) + + // Lower a value into a `RustBuffer` + // + // This method lowers a value into a `RustBuffer` rather than the normal + // FfiType. It's used by the callback interface code. Callback interface + // returns are always serialized into a `RustBuffer` regardless of their + // normal FFI type. + fun lowerIntoRustBuffer(value: KotlinType): RustBuffer.ByValue { + val rbuf = RustBuffer.alloc(allocationSize(value)) + try { + val bbuf = + rbuf.data!!.getByteBuffer(0, rbuf.capacity).also { + it.order(ByteOrder.BIG_ENDIAN) + } + write(value, bbuf) + rbuf.writeField("len", bbuf.position().toLong()) + return rbuf + } catch (e: Throwable) { + RustBuffer.free(rbuf) + throw e + } + } + + // Lift a value from a `RustBuffer`. + // + // This here mostly because of the symmetry with `lowerIntoRustBuffer()`. + // It's currently only used by the `FfiConverterRustBuffer` class below. + fun liftFromRustBuffer(rbuf: RustBuffer.ByValue): KotlinType { + val byteBuf = rbuf.asByteBuffer()!! + try { + val item = read(byteBuf) + if (byteBuf.hasRemaining()) { + throw RuntimeException("junk remaining in buffer after lifting, something is very wrong!!") + } + return item + } finally { + RustBuffer.free(rbuf) + } + } +} + +/** + * FfiConverter that uses `RustBuffer` as the FfiType + * + * @suppress + */ +public interface FfiConverterRustBuffer : FfiConverter { + override fun lift(value: RustBuffer.ByValue) = liftFromRustBuffer(value) + + override fun lower(value: KotlinType) = lowerIntoRustBuffer(value) +} +// A handful of classes and functions to support the generated data structures. +// This would be a good candidate for isolating in its own ffi-support lib. + +internal const val UNIFFI_CALL_SUCCESS = 0.toByte() +internal const val UNIFFI_CALL_ERROR = 1.toByte() +internal const val UNIFFI_CALL_UNEXPECTED_ERROR = 2.toByte() + +@Structure.FieldOrder("code", "error_buf") +internal open class UniffiRustCallStatus : Structure() { + @JvmField var code: Byte = 0 + + @JvmField var error_buf: RustBuffer.ByValue = RustBuffer.ByValue() + + class ByValue : + UniffiRustCallStatus(), + Structure.ByValue + + fun isSuccess(): Boolean = code == UNIFFI_CALL_SUCCESS + + fun isError(): Boolean = code == UNIFFI_CALL_ERROR + + fun isPanic(): Boolean = code == UNIFFI_CALL_UNEXPECTED_ERROR + + companion object { + fun create( + code: Byte, + errorBuf: RustBuffer.ByValue, + ): UniffiRustCallStatus.ByValue { + val callStatus = UniffiRustCallStatus.ByValue() + callStatus.code = code + callStatus.error_buf = errorBuf + return callStatus + } + } +} + +class InternalException( + message: String, +) : kotlin.Exception(message) + +/** + * Each top-level error class has a companion object that can lift the error from the call status's rust buffer + * + * @suppress + */ +interface UniffiRustCallStatusErrorHandler { + fun lift(error_buf: RustBuffer.ByValue): E +} + +// Helpers for calling Rust +// In practice we usually need to be synchronized to call this safely, so it doesn't +// synchronize itself + +// Call a rust function that returns a Result<>. Pass in the Error class companion that corresponds to the Err +private inline fun uniffiRustCallWithError( + errorHandler: UniffiRustCallStatusErrorHandler, + callback: (UniffiRustCallStatus) -> U, +): U { + var status = UniffiRustCallStatus() + val return_value = callback(status) + uniffiCheckCallStatus(errorHandler, status) + return return_value +} + +// Check UniffiRustCallStatus and throw an error if the call wasn't successful +private fun uniffiCheckCallStatus( + errorHandler: UniffiRustCallStatusErrorHandler, + status: UniffiRustCallStatus, +) { + if (status.isSuccess()) { + return + } else if (status.isError()) { + throw errorHandler.lift(status.error_buf) + } else if (status.isPanic()) { + // when the rust code sees a panic, it tries to construct a rustbuffer + // with the message. but if that code panics, then it just sends back + // an empty buffer. + if (status.error_buf.len > 0) { + throw InternalException(FfiConverterString.lift(status.error_buf)) + } else { + throw InternalException("Rust panic") + } + } else { + throw InternalException("Unknown rust call status: $status.code") + } +} + +/** + * UniffiRustCallStatusErrorHandler implementation for times when we don't expect a CALL_ERROR + * + * @suppress + */ +object UniffiNullRustCallStatusErrorHandler : UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): InternalException { + RustBuffer.free(error_buf) + return InternalException("Unexpected CALL_ERROR") + } +} + +// Call a rust function that returns a plain value +private inline fun uniffiRustCall(callback: (UniffiRustCallStatus) -> U): U = + uniffiRustCallWithError(UniffiNullRustCallStatusErrorHandler, callback) + +internal inline fun uniffiTraitInterfaceCall( + callStatus: UniffiRustCallStatus, + makeCall: () -> T, + writeReturn: (T) -> Unit, +) { + try { + writeReturn(makeCall()) + } catch (e: kotlin.Exception) { + callStatus.code = UNIFFI_CALL_UNEXPECTED_ERROR + callStatus.error_buf = FfiConverterString.lower(e.toString()) + } +} + +internal inline fun uniffiTraitInterfaceCallWithError( + callStatus: UniffiRustCallStatus, + makeCall: () -> T, + writeReturn: (T) -> Unit, + lowerError: (E) -> RustBuffer.ByValue, +) { + try { + writeReturn(makeCall()) + } catch (e: kotlin.Exception) { + if (e is E) { + callStatus.code = UNIFFI_CALL_ERROR + callStatus.error_buf = lowerError(e) + } else { + callStatus.code = UNIFFI_CALL_UNEXPECTED_ERROR + callStatus.error_buf = FfiConverterString.lower(e.toString()) + } + } +} + +// Map handles to objects +// +// This is used pass an opaque 64-bit handle representing a foreign object to the Rust code. +internal class UniffiHandleMap { + private val map = ConcurrentHashMap() + private val counter = + java.util.concurrent.atomic + .AtomicLong(0) + + val size: Int + get() = map.size + + // Insert a new object into the handle map and get a handle for it + fun insert(obj: T): Long { + val handle = counter.getAndAdd(1) + map.put(handle, obj) + return handle + } + + // Get an object from the handle map + fun get(handle: Long): T = map.get(handle) ?: throw InternalException("UniffiHandleMap.get: Invalid handle") + + // Remove an entry from the handlemap and get the Kotlin object back + fun remove(handle: Long): T = map.remove(handle) ?: throw InternalException("UniffiHandleMap: Invalid handle") +} + +// Contains loading, initialization code, +// and the FFI Function declarations in a com.sun.jna.Library. +@Synchronized +private fun findLibraryName(componentName: String): String { + val libOverride = System.getProperty("uniffi.component.$componentName.libraryOverride") + if (libOverride != null) { + return libOverride + } + return "goose_llm" +} + +private inline fun loadIndirect(componentName: String): Lib = + Native.load(findLibraryName(componentName), Lib::class.java) + +// Define FFI callback types +internal interface UniffiRustFutureContinuationCallback : com.sun.jna.Callback { + fun callback( + `data`: Long, + `pollResult`: Byte, + ) +} + +internal interface UniffiForeignFutureFree : com.sun.jna.Callback { + fun callback(`handle`: Long) +} + +internal interface UniffiCallbackInterfaceFree : com.sun.jna.Callback { + fun callback(`handle`: Long) +} + +@Structure.FieldOrder("handle", "free") +internal open class UniffiForeignFuture( + @JvmField internal var `handle`: Long = 0.toLong(), + @JvmField internal var `free`: UniffiForeignFutureFree? = null, +) : Structure() { + class UniffiByValue( + `handle`: Long = 0.toLong(), + `free`: UniffiForeignFutureFree? = null, + ) : UniffiForeignFuture(`handle`, `free`), + Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFuture) { + `handle` = other.`handle` + `free` = other.`free` + } +} + +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU8( + @JvmField internal var `returnValue`: Byte = 0.toByte(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Byte = 0.toByte(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ) : UniffiForeignFutureStructU8(`returnValue`, `callStatus`), + Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU8) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } +} + +internal interface UniffiForeignFutureCompleteU8 : com.sun.jna.Callback { + fun callback( + `callbackData`: Long, + `result`: UniffiForeignFutureStructU8.UniffiByValue, + ) +} + +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI8( + @JvmField internal var `returnValue`: Byte = 0.toByte(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Byte = 0.toByte(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ) : UniffiForeignFutureStructI8(`returnValue`, `callStatus`), + Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI8) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } +} + +internal interface UniffiForeignFutureCompleteI8 : com.sun.jna.Callback { + fun callback( + `callbackData`: Long, + `result`: UniffiForeignFutureStructI8.UniffiByValue, + ) +} + +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU16( + @JvmField internal var `returnValue`: Short = 0.toShort(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Short = 0.toShort(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ) : UniffiForeignFutureStructU16(`returnValue`, `callStatus`), + Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU16) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } +} + +internal interface UniffiForeignFutureCompleteU16 : com.sun.jna.Callback { + fun callback( + `callbackData`: Long, + `result`: UniffiForeignFutureStructU16.UniffiByValue, + ) +} + +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI16( + @JvmField internal var `returnValue`: Short = 0.toShort(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Short = 0.toShort(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ) : UniffiForeignFutureStructI16(`returnValue`, `callStatus`), + Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI16) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } +} + +internal interface UniffiForeignFutureCompleteI16 : com.sun.jna.Callback { + fun callback( + `callbackData`: Long, + `result`: UniffiForeignFutureStructI16.UniffiByValue, + ) +} + +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU32( + @JvmField internal var `returnValue`: Int = 0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Int = 0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ) : UniffiForeignFutureStructU32(`returnValue`, `callStatus`), + Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } +} + +internal interface UniffiForeignFutureCompleteU32 : com.sun.jna.Callback { + fun callback( + `callbackData`: Long, + `result`: UniffiForeignFutureStructU32.UniffiByValue, + ) +} + +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI32( + @JvmField internal var `returnValue`: Int = 0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Int = 0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ) : UniffiForeignFutureStructI32(`returnValue`, `callStatus`), + Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } +} + +internal interface UniffiForeignFutureCompleteI32 : com.sun.jna.Callback { + fun callback( + `callbackData`: Long, + `result`: UniffiForeignFutureStructI32.UniffiByValue, + ) +} + +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU64( + @JvmField internal var `returnValue`: Long = 0.toLong(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Long = 0.toLong(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ) : UniffiForeignFutureStructU64(`returnValue`, `callStatus`), + Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } +} + +internal interface UniffiForeignFutureCompleteU64 : com.sun.jna.Callback { + fun callback( + `callbackData`: Long, + `result`: UniffiForeignFutureStructU64.UniffiByValue, + ) +} + +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI64( + @JvmField internal var `returnValue`: Long = 0.toLong(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Long = 0.toLong(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ) : UniffiForeignFutureStructI64(`returnValue`, `callStatus`), + Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } +} + +internal interface UniffiForeignFutureCompleteI64 : com.sun.jna.Callback { + fun callback( + `callbackData`: Long, + `result`: UniffiForeignFutureStructI64.UniffiByValue, + ) +} + +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructF32( + @JvmField internal var `returnValue`: Float = 0.0f, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Float = 0.0f, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ) : UniffiForeignFutureStructF32(`returnValue`, `callStatus`), + Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructF32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } +} + +internal interface UniffiForeignFutureCompleteF32 : com.sun.jna.Callback { + fun callback( + `callbackData`: Long, + `result`: UniffiForeignFutureStructF32.UniffiByValue, + ) +} + +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructF64( + @JvmField internal var `returnValue`: Double = 0.0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Double = 0.0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ) : UniffiForeignFutureStructF64(`returnValue`, `callStatus`), + Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructF64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } +} + +internal interface UniffiForeignFutureCompleteF64 : com.sun.jna.Callback { + fun callback( + `callbackData`: Long, + `result`: UniffiForeignFutureStructF64.UniffiByValue, + ) +} + +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructPointer( + @JvmField internal var `returnValue`: Pointer = Pointer.NULL, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Pointer = Pointer.NULL, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ) : UniffiForeignFutureStructPointer(`returnValue`, `callStatus`), + Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructPointer) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } +} + +internal interface UniffiForeignFutureCompletePointer : com.sun.jna.Callback { + fun callback( + `callbackData`: Long, + `result`: UniffiForeignFutureStructPointer.UniffiByValue, + ) +} + +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructRustBuffer( + @JvmField internal var `returnValue`: RustBuffer.ByValue = RustBuffer.ByValue(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: RustBuffer.ByValue = RustBuffer.ByValue(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ) : UniffiForeignFutureStructRustBuffer(`returnValue`, `callStatus`), + Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructRustBuffer) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } +} + +internal interface UniffiForeignFutureCompleteRustBuffer : com.sun.jna.Callback { + fun callback( + `callbackData`: Long, + `result`: UniffiForeignFutureStructRustBuffer.UniffiByValue, + ) +} + +@Structure.FieldOrder("callStatus") +internal open class UniffiForeignFutureStructVoid( + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ) : UniffiForeignFutureStructVoid(`callStatus`), + Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructVoid) { + `callStatus` = other.`callStatus` + } +} + +internal interface UniffiForeignFutureCompleteVoid : com.sun.jna.Callback { + fun callback( + `callbackData`: Long, + `result`: UniffiForeignFutureStructVoid.UniffiByValue, + ) +} + +// For large crates we prevent `MethodTooLargeException` (see #2340) +// N.B. the name of the extension is very misleading, since it is +// rather `InterfaceTooLargeException`, caused by too many methods +// in the interface for large crates. +// +// By splitting the otherwise huge interface into two parts +// * UniffiLib +// * IntegrityCheckingUniffiLib (this) +// we allow for ~2x as many methods in the UniffiLib interface. +// +// The `ffi_uniffi_contract_version` method and all checksum methods are put +// into `IntegrityCheckingUniffiLib` and these methods are called only once, +// when the library is loaded. +internal interface IntegrityCheckingUniffiLib : Library { + // Integrity check functions only + fun uniffi_goose_llm_checksum_func_completion(): Short + + fun uniffi_goose_llm_checksum_func_create_tool_config(): Short + + fun uniffi_goose_llm_checksum_func_generate_session_name(): Short + + fun uniffi_goose_llm_checksum_func_generate_tooltip(): Short + + fun uniffi_goose_llm_checksum_func_print_messages(): Short + + fun ffi_goose_llm_uniffi_contract_version(): Int +} + +// A JNA Library to expose the extern-C FFI definitions. +// This is an implementation detail which will be called internally by the public API. +internal interface UniffiLib : Library { + companion object { + internal val INSTANCE: UniffiLib by lazy { + val componentName = "goose_llm" + // For large crates we prevent `MethodTooLargeException` (see #2340) + // N.B. the name of the extension is very misleading, since it is + // rather `InterfaceTooLargeException`, caused by too many methods + // in the interface for large crates. + // + // By splitting the otherwise huge interface into two parts + // * UniffiLib (this) + // * IntegrityCheckingUniffiLib + // And all checksum methods are put into `IntegrityCheckingUniffiLib` + // we allow for ~2x as many methods in the UniffiLib interface. + // + // Thus we first load the library with `loadIndirect` as `IntegrityCheckingUniffiLib` + // so that we can (optionally!) call `uniffiCheckApiChecksums`... + loadIndirect(componentName) + .also { lib: IntegrityCheckingUniffiLib -> + uniffiCheckContractApiVersion(lib) + uniffiCheckApiChecksums(lib) + } + // ... and then we load the library as `UniffiLib` + // N.B. we cannot use `loadIndirect` once and then try to cast it to `UniffiLib` + // => results in `java.lang.ClassCastException: com.sun.proxy.$Proxy cannot be cast to ...` + // error. So we must call `loadIndirect` twice. For crates large enough + // to trigger this issue, the performance impact is negligible, running on + // a macOS M1 machine the `loadIndirect` call takes ~50ms. + val lib = loadIndirect(componentName) + // No need to check the contract version and checksums, since + // we already did that with `IntegrityCheckingUniffiLib` above. + // Loading of library with integrity check done. + lib + } + } + + // FFI functions + fun uniffi_goose_llm_fn_func_completion(`req`: RustBuffer.ByValue): Long + + fun uniffi_goose_llm_fn_func_create_tool_config( + `name`: RustBuffer.ByValue, + `description`: RustBuffer.ByValue, + `inputSchema`: RustBuffer.ByValue, + `approvalMode`: RustBuffer.ByValue, + uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + + fun uniffi_goose_llm_fn_func_generate_session_name(`messages`: RustBuffer.ByValue): Long + + fun uniffi_goose_llm_fn_func_generate_tooltip(`messages`: RustBuffer.ByValue): Long + + fun uniffi_goose_llm_fn_func_print_messages( + `messages`: RustBuffer.ByValue, + uniffi_out_err: UniffiRustCallStatus, + ): Unit + + fun ffi_goose_llm_rustbuffer_alloc( + `size`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + + fun ffi_goose_llm_rustbuffer_from_bytes( + `bytes`: ForeignBytes.ByValue, + uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + + fun ffi_goose_llm_rustbuffer_free( + `buf`: RustBuffer.ByValue, + uniffi_out_err: UniffiRustCallStatus, + ): Unit + + fun ffi_goose_llm_rustbuffer_reserve( + `buf`: RustBuffer.ByValue, + `additional`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + + fun ffi_goose_llm_rust_future_poll_u8( + `handle`: Long, + `callback`: UniffiRustFutureContinuationCallback, + `callbackData`: Long, + ): Unit + + fun ffi_goose_llm_rust_future_cancel_u8(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_free_u8(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_complete_u8( + `handle`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): Byte + + fun ffi_goose_llm_rust_future_poll_i8( + `handle`: Long, + `callback`: UniffiRustFutureContinuationCallback, + `callbackData`: Long, + ): Unit + + fun ffi_goose_llm_rust_future_cancel_i8(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_free_i8(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_complete_i8( + `handle`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): Byte + + fun ffi_goose_llm_rust_future_poll_u16( + `handle`: Long, + `callback`: UniffiRustFutureContinuationCallback, + `callbackData`: Long, + ): Unit + + fun ffi_goose_llm_rust_future_cancel_u16(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_free_u16(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_complete_u16( + `handle`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): Short + + fun ffi_goose_llm_rust_future_poll_i16( + `handle`: Long, + `callback`: UniffiRustFutureContinuationCallback, + `callbackData`: Long, + ): Unit + + fun ffi_goose_llm_rust_future_cancel_i16(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_free_i16(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_complete_i16( + `handle`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): Short + + fun ffi_goose_llm_rust_future_poll_u32( + `handle`: Long, + `callback`: UniffiRustFutureContinuationCallback, + `callbackData`: Long, + ): Unit + + fun ffi_goose_llm_rust_future_cancel_u32(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_free_u32(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_complete_u32( + `handle`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): Int + + fun ffi_goose_llm_rust_future_poll_i32( + `handle`: Long, + `callback`: UniffiRustFutureContinuationCallback, + `callbackData`: Long, + ): Unit + + fun ffi_goose_llm_rust_future_cancel_i32(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_free_i32(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_complete_i32( + `handle`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): Int + + fun ffi_goose_llm_rust_future_poll_u64( + `handle`: Long, + `callback`: UniffiRustFutureContinuationCallback, + `callbackData`: Long, + ): Unit + + fun ffi_goose_llm_rust_future_cancel_u64(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_free_u64(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_complete_u64( + `handle`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): Long + + fun ffi_goose_llm_rust_future_poll_i64( + `handle`: Long, + `callback`: UniffiRustFutureContinuationCallback, + `callbackData`: Long, + ): Unit + + fun ffi_goose_llm_rust_future_cancel_i64(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_free_i64(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_complete_i64( + `handle`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): Long + + fun ffi_goose_llm_rust_future_poll_f32( + `handle`: Long, + `callback`: UniffiRustFutureContinuationCallback, + `callbackData`: Long, + ): Unit + + fun ffi_goose_llm_rust_future_cancel_f32(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_free_f32(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_complete_f32( + `handle`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): Float + + fun ffi_goose_llm_rust_future_poll_f64( + `handle`: Long, + `callback`: UniffiRustFutureContinuationCallback, + `callbackData`: Long, + ): Unit + + fun ffi_goose_llm_rust_future_cancel_f64(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_free_f64(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_complete_f64( + `handle`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): Double + + fun ffi_goose_llm_rust_future_poll_pointer( + `handle`: Long, + `callback`: UniffiRustFutureContinuationCallback, + `callbackData`: Long, + ): Unit + + fun ffi_goose_llm_rust_future_cancel_pointer(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_free_pointer(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_complete_pointer( + `handle`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): Pointer + + fun ffi_goose_llm_rust_future_poll_rust_buffer( + `handle`: Long, + `callback`: UniffiRustFutureContinuationCallback, + `callbackData`: Long, + ): Unit + + fun ffi_goose_llm_rust_future_cancel_rust_buffer(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_free_rust_buffer(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_complete_rust_buffer( + `handle`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + + fun ffi_goose_llm_rust_future_poll_void( + `handle`: Long, + `callback`: UniffiRustFutureContinuationCallback, + `callbackData`: Long, + ): Unit + + fun ffi_goose_llm_rust_future_cancel_void(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_free_void(`handle`: Long): Unit + + fun ffi_goose_llm_rust_future_complete_void( + `handle`: Long, + uniffi_out_err: UniffiRustCallStatus, + ): Unit +} + +private fun uniffiCheckContractApiVersion(lib: IntegrityCheckingUniffiLib) { + // Get the bindings contract version from our ComponentInterface + val bindings_contract_version = 29 + // Get the scaffolding contract version by calling the into the dylib + val scaffolding_contract_version = lib.ffi_goose_llm_uniffi_contract_version() + if (bindings_contract_version != scaffolding_contract_version) { + throw RuntimeException("UniFFI contract version mismatch: try cleaning and rebuilding your project") + } +} + +@Suppress("UNUSED_PARAMETER") +private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) { + if (lib.uniffi_goose_llm_checksum_func_completion() != 55281.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_goose_llm_checksum_func_create_tool_config() != 22809.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_goose_llm_checksum_func_generate_session_name() != 61290.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_goose_llm_checksum_func_generate_tooltip() != 7529.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_goose_llm_checksum_func_print_messages() != 30278.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } +} + +/** + * @suppress + */ +public fun uniffiEnsureInitialized() { + UniffiLib.INSTANCE +} + +// Async support +// Async return type handlers + +internal const val UNIFFI_RUST_FUTURE_POLL_READY = 0.toByte() +internal const val UNIFFI_RUST_FUTURE_POLL_MAYBE_READY = 1.toByte() + +internal val uniffiContinuationHandleMap = UniffiHandleMap>() + +// FFI type for Rust future continuations +internal object uniffiRustFutureContinuationCallbackImpl : UniffiRustFutureContinuationCallback { + override fun callback( + data: Long, + pollResult: Byte, + ) { + uniffiContinuationHandleMap.remove(data).resume(pollResult) + } +} + +internal suspend fun uniffiRustCallAsync( + rustFuture: Long, + pollFunc: (Long, UniffiRustFutureContinuationCallback, Long) -> Unit, + completeFunc: (Long, UniffiRustCallStatus) -> F, + freeFunc: (Long) -> Unit, + liftFunc: (F) -> T, + errorHandler: UniffiRustCallStatusErrorHandler, +): T { + try { + do { + val pollResult = + suspendCancellableCoroutine { continuation -> + pollFunc( + rustFuture, + uniffiRustFutureContinuationCallbackImpl, + uniffiContinuationHandleMap.insert(continuation), + ) + } + } while (pollResult != UNIFFI_RUST_FUTURE_POLL_READY) + + return liftFunc( + uniffiRustCallWithError(errorHandler, { status -> completeFunc(rustFuture, status) }), + ) + } finally { + freeFunc(rustFuture) + } +} + +// Public interface members begin here. + +// Interface implemented by anything that can contain an object reference. +// +// Such types expose a `destroy()` method that must be called to cleanly +// dispose of the contained objects. Failure to call this method may result +// in memory leaks. +// +// The easiest way to ensure this method is called is to use the `.use` +// helper method to execute a block and destroy the object at the end. +interface Disposable { + fun destroy() + + companion object { + fun destroy(vararg args: Any?) { + for (arg in args) { + when (arg) { + is Disposable -> arg.destroy() + is ArrayList<*> -> { + for (idx in arg.indices) { + val element = arg[idx] + if (element is Disposable) { + element.destroy() + } + } + } + is Map<*, *> -> { + for (element in arg.values) { + if (element is Disposable) { + element.destroy() + } + } + } + is Iterable<*> -> { + for (element in arg) { + if (element is Disposable) { + element.destroy() + } + } + } + } + } + } + } +} + +/** + * @suppress + */ +inline fun T.use(block: (T) -> R) = + try { + block(this) + } finally { + try { + // N.B. our implementation is on the nullable type `Disposable?`. + this?.destroy() + } catch (e: Throwable) { + // swallow + } + } + +/** + * Used to instantiate an interface without an actual pointer, for fakes in tests, mostly. + * + * @suppress + * */ +object NoPointer + +/** + * @suppress + */ +public object FfiConverterUInt : FfiConverter { + override fun lift(value: Int): UInt = value.toUInt() + + override fun read(buf: ByteBuffer): UInt = lift(buf.getInt()) + + override fun lower(value: UInt): Int = value.toInt() + + override fun allocationSize(value: UInt) = 4UL + + override fun write( + value: UInt, + buf: ByteBuffer, + ) { + buf.putInt(value.toInt()) + } +} + +/** + * @suppress + */ +public object FfiConverterInt : FfiConverter { + override fun lift(value: Int): Int = value + + override fun read(buf: ByteBuffer): Int = buf.getInt() + + override fun lower(value: Int): Int = value + + override fun allocationSize(value: Int) = 4UL + + override fun write( + value: Int, + buf: ByteBuffer, + ) { + buf.putInt(value) + } +} + +/** + * @suppress + */ +public object FfiConverterLong : FfiConverter { + override fun lift(value: Long): Long = value + + override fun read(buf: ByteBuffer): Long = buf.getLong() + + override fun lower(value: Long): Long = value + + override fun allocationSize(value: Long) = 8UL + + override fun write( + value: Long, + buf: ByteBuffer, + ) { + buf.putLong(value) + } +} + +/** + * @suppress + */ +public object FfiConverterFloat : FfiConverter { + override fun lift(value: Float): Float = value + + override fun read(buf: ByteBuffer): Float = buf.getFloat() + + override fun lower(value: Float): Float = value + + override fun allocationSize(value: Float) = 4UL + + override fun write( + value: Float, + buf: ByteBuffer, + ) { + buf.putFloat(value) + } +} + +/** + * @suppress + */ +public object FfiConverterDouble : FfiConverter { + override fun lift(value: Double): Double = value + + override fun read(buf: ByteBuffer): Double = buf.getDouble() + + override fun lower(value: Double): Double = value + + override fun allocationSize(value: Double) = 8UL + + override fun write( + value: Double, + buf: ByteBuffer, + ) { + buf.putDouble(value) + } +} + +/** + * @suppress + */ +public object FfiConverterString : FfiConverter { + // Note: we don't inherit from FfiConverterRustBuffer, because we use a + // special encoding when lowering/lifting. We can use `RustBuffer.len` to + // store our length and avoid writing it out to the buffer. + override fun lift(value: RustBuffer.ByValue): String { + try { + val byteArr = ByteArray(value.len.toInt()) + value.asByteBuffer()!!.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } finally { + RustBuffer.free(value) + } + } + + override fun read(buf: ByteBuffer): String { + val len = buf.getInt() + val byteArr = ByteArray(len) + buf.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } + + fun toUtf8(value: String): ByteBuffer { + // Make sure we don't have invalid UTF-16, check for lone surrogates. + return Charsets.UTF_8.newEncoder().run { + onMalformedInput(CodingErrorAction.REPORT) + encode(CharBuffer.wrap(value)) + } + } + + override fun lower(value: String): RustBuffer.ByValue { + val byteBuf = toUtf8(value) + // Ideally we'd pass these bytes to `ffi_bytebuffer_from_bytes`, but doing so would require us + // to copy them into a JNA `Memory`. So we might as well directly copy them into a `RustBuffer`. + val rbuf = RustBuffer.alloc(byteBuf.limit().toULong()) + rbuf.asByteBuffer()!!.put(byteBuf) + return rbuf + } + + // We aren't sure exactly how many bytes our string will be once it's UTF-8 + // encoded. Allocate 3 bytes per UTF-16 code unit which will always be + // enough. + override fun allocationSize(value: String): ULong { + val sizeForLength = 4UL + val sizeForString = value.length.toULong() * 3UL + return sizeForLength + sizeForString + } + + override fun write( + value: String, + buf: ByteBuffer, + ) { + val byteBuf = toUtf8(value) + buf.putInt(byteBuf.limit()) + buf.put(byteBuf) + } +} + +data class CompletionRequest( + var `providerName`: kotlin.String, + var `modelConfig`: ModelConfig, + var `systemPreamble`: kotlin.String, + var `messages`: List, + var `extensions`: List, +) { + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeCompletionRequest : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): CompletionRequest = + CompletionRequest( + FfiConverterString.read(buf), + FfiConverterTypeModelConfig.read(buf), + FfiConverterString.read(buf), + FfiConverterSequenceTypeMessage.read(buf), + FfiConverterSequenceTypeExtensionConfig.read(buf), + ) + + override fun allocationSize(value: CompletionRequest) = + ( + FfiConverterString.allocationSize(value.`providerName`) + + FfiConverterTypeModelConfig.allocationSize(value.`modelConfig`) + + FfiConverterString.allocationSize(value.`systemPreamble`) + + FfiConverterSequenceTypeMessage.allocationSize(value.`messages`) + + FfiConverterSequenceTypeExtensionConfig.allocationSize(value.`extensions`) + ) + + override fun write( + value: CompletionRequest, + buf: ByteBuffer, + ) { + FfiConverterString.write(value.`providerName`, buf) + FfiConverterTypeModelConfig.write(value.`modelConfig`, buf) + FfiConverterString.write(value.`systemPreamble`, buf) + FfiConverterSequenceTypeMessage.write(value.`messages`, buf) + FfiConverterSequenceTypeExtensionConfig.write(value.`extensions`, buf) + } +} + +data class CompletionResponse( + var `message`: Message, + var `model`: kotlin.String, + var `usage`: Usage, + var `runtimeMetrics`: RuntimeMetrics, +) { + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeCompletionResponse : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): CompletionResponse = + CompletionResponse( + FfiConverterTypeMessage.read(buf), + FfiConverterString.read(buf), + FfiConverterTypeUsage.read(buf), + FfiConverterTypeRuntimeMetrics.read(buf), + ) + + override fun allocationSize(value: CompletionResponse) = + ( + FfiConverterTypeMessage.allocationSize(value.`message`) + + FfiConverterString.allocationSize(value.`model`) + + FfiConverterTypeUsage.allocationSize(value.`usage`) + + FfiConverterTypeRuntimeMetrics.allocationSize(value.`runtimeMetrics`) + ) + + override fun write( + value: CompletionResponse, + buf: ByteBuffer, + ) { + FfiConverterTypeMessage.write(value.`message`, buf) + FfiConverterString.write(value.`model`, buf) + FfiConverterTypeUsage.write(value.`usage`, buf) + FfiConverterTypeRuntimeMetrics.write(value.`runtimeMetrics`, buf) + } +} + +data class ExtensionConfig( + var `name`: kotlin.String, + var `instructions`: kotlin.String?, + var `tools`: List, +) { + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeExtensionConfig : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ExtensionConfig = + ExtensionConfig( + FfiConverterString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterSequenceTypeToolConfig.read(buf), + ) + + override fun allocationSize(value: ExtensionConfig) = + ( + FfiConverterString.allocationSize(value.`name`) + + FfiConverterOptionalString.allocationSize(value.`instructions`) + + FfiConverterSequenceTypeToolConfig.allocationSize(value.`tools`) + ) + + override fun write( + value: ExtensionConfig, + buf: ByteBuffer, + ) { + FfiConverterString.write(value.`name`, buf) + FfiConverterOptionalString.write(value.`instructions`, buf) + FfiConverterSequenceTypeToolConfig.write(value.`tools`, buf) + } +} + +data class ImageContent( + var `data`: kotlin.String, + var `mimeType`: kotlin.String, +) { + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeImageContent : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ImageContent = + ImageContent( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + ) + + override fun allocationSize(value: ImageContent) = + ( + FfiConverterString.allocationSize(value.`data`) + + FfiConverterString.allocationSize(value.`mimeType`) + ) + + override fun write( + value: ImageContent, + buf: ByteBuffer, + ) { + FfiConverterString.write(value.`data`, buf) + FfiConverterString.write(value.`mimeType`, buf) + } +} + +/** + * A message to or from an LLM + */ +data class Message( + var `role`: Role, + var `created`: kotlin.Long, + var `content`: Contents, +) { + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeMessage : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): Message = + Message( + FfiConverterTypeRole.read(buf), + FfiConverterLong.read(buf), + FfiConverterTypeContents.read(buf), + ) + + override fun allocationSize(value: Message) = + ( + FfiConverterTypeRole.allocationSize(value.`role`) + + FfiConverterLong.allocationSize(value.`created`) + + FfiConverterTypeContents.allocationSize(value.`content`) + ) + + override fun write( + value: Message, + buf: ByteBuffer, + ) { + FfiConverterTypeRole.write(value.`role`, buf) + FfiConverterLong.write(value.`created`, buf) + FfiConverterTypeContents.write(value.`content`, buf) + } +} + +/** + * Configuration for model-specific settings and limits + */ +data class ModelConfig( + /** + * The name of the model to use + */ + var `modelName`: kotlin.String, + /** + * Optional explicit context limit that overrides any defaults + */ + var `contextLimit`: kotlin.UInt?, + /** + * Optional temperature setting (0.0 - 1.0) + */ + var `temperature`: kotlin.Float?, + /** + * Optional maximum tokens to generate + */ + var `maxTokens`: kotlin.Int?, +) { + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeModelConfig : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ModelConfig = + ModelConfig( + FfiConverterString.read(buf), + FfiConverterOptionalUInt.read(buf), + FfiConverterOptionalFloat.read(buf), + FfiConverterOptionalInt.read(buf), + ) + + override fun allocationSize(value: ModelConfig) = + ( + FfiConverterString.allocationSize(value.`modelName`) + + FfiConverterOptionalUInt.allocationSize(value.`contextLimit`) + + FfiConverterOptionalFloat.allocationSize(value.`temperature`) + + FfiConverterOptionalInt.allocationSize(value.`maxTokens`) + ) + + override fun write( + value: ModelConfig, + buf: ByteBuffer, + ) { + FfiConverterString.write(value.`modelName`, buf) + FfiConverterOptionalUInt.write(value.`contextLimit`, buf) + FfiConverterOptionalFloat.write(value.`temperature`, buf) + FfiConverterOptionalInt.write(value.`maxTokens`, buf) + } +} + +data class ProviderCompleteResponse( + var `message`: Message, + var `model`: kotlin.String, + var `usage`: Usage, +) { + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeProviderCompleteResponse : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ProviderCompleteResponse = + ProviderCompleteResponse( + FfiConverterTypeMessage.read(buf), + FfiConverterString.read(buf), + FfiConverterTypeUsage.read(buf), + ) + + override fun allocationSize(value: ProviderCompleteResponse) = + ( + FfiConverterTypeMessage.allocationSize(value.`message`) + + FfiConverterString.allocationSize(value.`model`) + + FfiConverterTypeUsage.allocationSize(value.`usage`) + ) + + override fun write( + value: ProviderCompleteResponse, + buf: ByteBuffer, + ) { + FfiConverterTypeMessage.write(value.`message`, buf) + FfiConverterString.write(value.`model`, buf) + FfiConverterTypeUsage.write(value.`usage`, buf) + } +} + +data class RedactedThinkingContent( + var `data`: kotlin.String, +) { + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeRedactedThinkingContent : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): RedactedThinkingContent = + RedactedThinkingContent( + FfiConverterString.read(buf), + ) + + override fun allocationSize(value: RedactedThinkingContent) = + ( + FfiConverterString.allocationSize(value.`data`) + ) + + override fun write( + value: RedactedThinkingContent, + buf: ByteBuffer, + ) { + FfiConverterString.write(value.`data`, buf) + } +} + +data class RuntimeMetrics( + var `totalTimeSec`: kotlin.Float, + var `totalTimeSecProvider`: kotlin.Float, + var `tokensPerSecond`: kotlin.Double?, +) { + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeRuntimeMetrics : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): RuntimeMetrics = + RuntimeMetrics( + FfiConverterFloat.read(buf), + FfiConverterFloat.read(buf), + FfiConverterOptionalDouble.read(buf), + ) + + override fun allocationSize(value: RuntimeMetrics) = + ( + FfiConverterFloat.allocationSize(value.`totalTimeSec`) + + FfiConverterFloat.allocationSize(value.`totalTimeSecProvider`) + + FfiConverterOptionalDouble.allocationSize(value.`tokensPerSecond`) + ) + + override fun write( + value: RuntimeMetrics, + buf: ByteBuffer, + ) { + FfiConverterFloat.write(value.`totalTimeSec`, buf) + FfiConverterFloat.write(value.`totalTimeSecProvider`, buf) + FfiConverterOptionalDouble.write(value.`tokensPerSecond`, buf) + } +} + +data class TextContent( + var `text`: kotlin.String, +) { + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeTextContent : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): TextContent = + TextContent( + FfiConverterString.read(buf), + ) + + override fun allocationSize(value: TextContent) = + ( + FfiConverterString.allocationSize(value.`text`) + ) + + override fun write( + value: TextContent, + buf: ByteBuffer, + ) { + FfiConverterString.write(value.`text`, buf) + } +} + +data class ThinkingContent( + var `thinking`: kotlin.String, + var `signature`: kotlin.String, +) { + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeThinkingContent : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ThinkingContent = + ThinkingContent( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + ) + + override fun allocationSize(value: ThinkingContent) = + ( + FfiConverterString.allocationSize(value.`thinking`) + + FfiConverterString.allocationSize(value.`signature`) + ) + + override fun write( + value: ThinkingContent, + buf: ByteBuffer, + ) { + FfiConverterString.write(value.`thinking`, buf) + FfiConverterString.write(value.`signature`, buf) + } +} + +data class ToolRequest( + var `id`: kotlin.String, + var `toolCall`: ToolRequestToolCall, +) { + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeToolRequest : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ToolRequest = + ToolRequest( + FfiConverterString.read(buf), + FfiConverterTypeToolRequestToolCall.read(buf), + ) + + override fun allocationSize(value: ToolRequest) = + ( + FfiConverterString.allocationSize(value.`id`) + + FfiConverterTypeToolRequestToolCall.allocationSize(value.`toolCall`) + ) + + override fun write( + value: ToolRequest, + buf: ByteBuffer, + ) { + FfiConverterString.write(value.`id`, buf) + FfiConverterTypeToolRequestToolCall.write(value.`toolCall`, buf) + } +} + +data class ToolResponse( + var `id`: kotlin.String, + var `toolResult`: ToolResponseToolResult, +) { + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeToolResponse : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ToolResponse = + ToolResponse( + FfiConverterString.read(buf), + FfiConverterTypeToolResponseToolResult.read(buf), + ) + + override fun allocationSize(value: ToolResponse) = + ( + FfiConverterString.allocationSize(value.`id`) + + FfiConverterTypeToolResponseToolResult.allocationSize(value.`toolResult`) + ) + + override fun write( + value: ToolResponse, + buf: ByteBuffer, + ) { + FfiConverterString.write(value.`id`, buf) + FfiConverterTypeToolResponseToolResult.write(value.`toolResult`, buf) + } +} + +data class Usage( + var `inputTokens`: kotlin.Int?, + var `outputTokens`: kotlin.Int?, + var `totalTokens`: kotlin.Int?, +) { + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeUsage : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): Usage = + Usage( + FfiConverterOptionalInt.read(buf), + FfiConverterOptionalInt.read(buf), + FfiConverterOptionalInt.read(buf), + ) + + override fun allocationSize(value: Usage) = + ( + FfiConverterOptionalInt.allocationSize(value.`inputTokens`) + + FfiConverterOptionalInt.allocationSize(value.`outputTokens`) + + FfiConverterOptionalInt.allocationSize(value.`totalTokens`) + ) + + override fun write( + value: Usage, + buf: ByteBuffer, + ) { + FfiConverterOptionalInt.write(value.`inputTokens`, buf) + FfiConverterOptionalInt.write(value.`outputTokens`, buf) + FfiConverterOptionalInt.write(value.`totalTokens`, buf) + } +} + +sealed class CompletionException( + message: String, +) : kotlin.Exception(message) { + class UnknownProvider( + message: String, + ) : CompletionException(message) + + class Provider( + message: String, + ) : CompletionException(message) + + class Template( + message: String, + ) : CompletionException(message) + + class Json( + message: String, + ) : CompletionException(message) + + class ToolNotFound( + message: String, + ) : CompletionException(message) + + companion object ErrorHandler : UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): CompletionException = FfiConverterTypeCompletionError.lift(error_buf) + } +} + +/** + * @suppress + */ +public object FfiConverterTypeCompletionError : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): CompletionException = + when (buf.getInt()) { + 1 -> CompletionException.UnknownProvider(FfiConverterString.read(buf)) + 2 -> CompletionException.Provider(FfiConverterString.read(buf)) + 3 -> CompletionException.Template(FfiConverterString.read(buf)) + 4 -> CompletionException.Json(FfiConverterString.read(buf)) + 5 -> CompletionException.ToolNotFound(FfiConverterString.read(buf)) + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + + override fun allocationSize(value: CompletionException): ULong = 4UL + + override fun write( + value: CompletionException, + buf: ByteBuffer, + ) { + when (value) { + is CompletionException.UnknownProvider -> { + buf.putInt(1) + Unit + } + is CompletionException.Provider -> { + buf.putInt(2) + Unit + } + is CompletionException.Template -> { + buf.putInt(3) + Unit + } + is CompletionException.Json -> { + buf.putInt(4) + Unit + } + is CompletionException.ToolNotFound -> { + buf.putInt(5) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } +} + +sealed class Content { + data class Text( + val v1: TextContent, + ) : Content() { + companion object + } + + data class Image( + val v1: ImageContent, + ) : Content() { + companion object + } + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeContent : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): Content = + when (buf.getInt()) { + 1 -> + Content.Text( + FfiConverterTypeTextContent.read(buf), + ) + 2 -> + Content.Image( + FfiConverterTypeImageContent.read(buf), + ) + else -> throw RuntimeException("invalid enum value, something is very wrong!!") + } + + override fun allocationSize(value: Content) = + when (value) { + is Content.Text -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeTextContent.allocationSize(value.v1) + ) + } + is Content.Image -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeImageContent.allocationSize(value.v1) + ) + } + } + + override fun write( + value: Content, + buf: ByteBuffer, + ) { + when (value) { + is Content.Text -> { + buf.putInt(1) + FfiConverterTypeTextContent.write(value.v1, buf) + Unit + } + is Content.Image -> { + buf.putInt(2) + FfiConverterTypeImageContent.write(value.v1, buf) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } +} + +/** + * Content passed inside a message, which can be both simple content and tool content + */ +sealed class MessageContent { + data class Text( + val v1: TextContent, + ) : MessageContent() { + companion object + } + + data class Image( + val v1: ImageContent, + ) : MessageContent() { + companion object + } + + data class ToolReq( + val v1: ToolRequest, + ) : MessageContent() { + companion object + } + + data class ToolResp( + val v1: ToolResponse, + ) : MessageContent() { + companion object + } + + data class Thinking( + val v1: ThinkingContent, + ) : MessageContent() { + companion object + } + + data class RedactedThinking( + val v1: RedactedThinkingContent, + ) : MessageContent() { + companion object + } + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeMessageContent : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): MessageContent = + when (buf.getInt()) { + 1 -> + MessageContent.Text( + FfiConverterTypeTextContent.read(buf), + ) + 2 -> + MessageContent.Image( + FfiConverterTypeImageContent.read(buf), + ) + 3 -> + MessageContent.ToolReq( + FfiConverterTypeToolRequest.read(buf), + ) + 4 -> + MessageContent.ToolResp( + FfiConverterTypeToolResponse.read(buf), + ) + 5 -> + MessageContent.Thinking( + FfiConverterTypeThinkingContent.read(buf), + ) + 6 -> + MessageContent.RedactedThinking( + FfiConverterTypeRedactedThinkingContent.read(buf), + ) + else -> throw RuntimeException("invalid enum value, something is very wrong!!") + } + + override fun allocationSize(value: MessageContent) = + when (value) { + is MessageContent.Text -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeTextContent.allocationSize(value.v1) + ) + } + is MessageContent.Image -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeImageContent.allocationSize(value.v1) + ) + } + is MessageContent.ToolReq -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeToolRequest.allocationSize(value.v1) + ) + } + is MessageContent.ToolResp -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeToolResponse.allocationSize(value.v1) + ) + } + is MessageContent.Thinking -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeThinkingContent.allocationSize(value.v1) + ) + } + is MessageContent.RedactedThinking -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeRedactedThinkingContent.allocationSize(value.v1) + ) + } + } + + override fun write( + value: MessageContent, + buf: ByteBuffer, + ) { + when (value) { + is MessageContent.Text -> { + buf.putInt(1) + FfiConverterTypeTextContent.write(value.v1, buf) + Unit + } + is MessageContent.Image -> { + buf.putInt(2) + FfiConverterTypeImageContent.write(value.v1, buf) + Unit + } + is MessageContent.ToolReq -> { + buf.putInt(3) + FfiConverterTypeToolRequest.write(value.v1, buf) + Unit + } + is MessageContent.ToolResp -> { + buf.putInt(4) + FfiConverterTypeToolResponse.write(value.v1, buf) + Unit + } + is MessageContent.Thinking -> { + buf.putInt(5) + FfiConverterTypeThinkingContent.write(value.v1, buf) + Unit + } + is MessageContent.RedactedThinking -> { + buf.putInt(6) + FfiConverterTypeRedactedThinkingContent.write(value.v1, buf) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } +} + +sealed class ProviderException : kotlin.Exception() { + class Authentication( + val v1: kotlin.String, + ) : ProviderException() { + override val message + get() = "v1=${ v1 }" + } + + class ContextLengthExceeded( + val v1: kotlin.String, + ) : ProviderException() { + override val message + get() = "v1=${ v1 }" + } + + class RateLimitExceeded( + val v1: kotlin.String, + ) : ProviderException() { + override val message + get() = "v1=${ v1 }" + } + + class ServerException( + val v1: kotlin.String, + ) : ProviderException() { + override val message + get() = "v1=${ v1 }" + } + + class RequestFailed( + val v1: kotlin.String, + ) : ProviderException() { + override val message + get() = "v1=${ v1 }" + } + + class ExecutionException( + val v1: kotlin.String, + ) : ProviderException() { + override val message + get() = "v1=${ v1 }" + } + + class UsageException( + val v1: kotlin.String, + ) : ProviderException() { + override val message + get() = "v1=${ v1 }" + } + + class ResponseParseException( + val v1: kotlin.String, + ) : ProviderException() { + override val message + get() = "v1=${ v1 }" + } + + companion object ErrorHandler : UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): ProviderException = FfiConverterTypeProviderError.lift(error_buf) + } +} + +/** + * @suppress + */ +public object FfiConverterTypeProviderError : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ProviderException = + when (buf.getInt()) { + 1 -> + ProviderException.Authentication( + FfiConverterString.read(buf), + ) + 2 -> + ProviderException.ContextLengthExceeded( + FfiConverterString.read(buf), + ) + 3 -> + ProviderException.RateLimitExceeded( + FfiConverterString.read(buf), + ) + 4 -> + ProviderException.ServerException( + FfiConverterString.read(buf), + ) + 5 -> + ProviderException.RequestFailed( + FfiConverterString.read(buf), + ) + 6 -> + ProviderException.ExecutionException( + FfiConverterString.read(buf), + ) + 7 -> + ProviderException.UsageException( + FfiConverterString.read(buf), + ) + 8 -> + ProviderException.ResponseParseException( + FfiConverterString.read(buf), + ) + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + + override fun allocationSize(value: ProviderException): ULong = + when (value) { + is ProviderException.Authentication -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is ProviderException.ContextLengthExceeded -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is ProviderException.RateLimitExceeded -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is ProviderException.ServerException -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is ProviderException.RequestFailed -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is ProviderException.ExecutionException -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is ProviderException.UsageException -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is ProviderException.ResponseParseException -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + } + + override fun write( + value: ProviderException, + buf: ByteBuffer, + ) { + when (value) { + is ProviderException.Authentication -> { + buf.putInt(1) + FfiConverterString.write(value.v1, buf) + Unit + } + is ProviderException.ContextLengthExceeded -> { + buf.putInt(2) + FfiConverterString.write(value.v1, buf) + Unit + } + is ProviderException.RateLimitExceeded -> { + buf.putInt(3) + FfiConverterString.write(value.v1, buf) + Unit + } + is ProviderException.ServerException -> { + buf.putInt(4) + FfiConverterString.write(value.v1, buf) + Unit + } + is ProviderException.RequestFailed -> { + buf.putInt(5) + FfiConverterString.write(value.v1, buf) + Unit + } + is ProviderException.ExecutionException -> { + buf.putInt(6) + FfiConverterString.write(value.v1, buf) + Unit + } + is ProviderException.UsageException -> { + buf.putInt(7) + FfiConverterString.write(value.v1, buf) + Unit + } + is ProviderException.ResponseParseException -> { + buf.putInt(8) + FfiConverterString.write(value.v1, buf) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } +} + +enum class Role { + USER, + ASSISTANT, + ; + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeRole : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer) = + try { + Role.values()[buf.getInt() - 1] + } catch (e: IndexOutOfBoundsException) { + throw RuntimeException("invalid enum value, something is very wrong!!", e) + } + + override fun allocationSize(value: Role) = 4UL + + override fun write( + value: Role, + buf: ByteBuffer, + ) { + buf.putInt(value.ordinal + 1) + } +} + +enum class ToolApprovalMode { + AUTO, + MANUAL, + SMART, + ; + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeToolApprovalMode : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer) = + try { + ToolApprovalMode.values()[buf.getInt() - 1] + } catch (e: IndexOutOfBoundsException) { + throw RuntimeException("invalid enum value, something is very wrong!!", e) + } + + override fun allocationSize(value: ToolApprovalMode) = 4UL + + override fun write( + value: ToolApprovalMode, + buf: ByteBuffer, + ) { + buf.putInt(value.ordinal + 1) + } +} + +sealed class ToolException : kotlin.Exception() { + class InvalidParameters( + val v1: kotlin.String, + ) : ToolException() { + override val message + get() = "v1=${ v1 }" + } + + class ExecutionException( + val v1: kotlin.String, + ) : ToolException() { + override val message + get() = "v1=${ v1 }" + } + + class SchemaException( + val v1: kotlin.String, + ) : ToolException() { + override val message + get() = "v1=${ v1 }" + } + + class NotFound( + val v1: kotlin.String, + ) : ToolException() { + override val message + get() = "v1=${ v1 }" + } + + companion object ErrorHandler : UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): ToolException = FfiConverterTypeToolError.lift(error_buf) + } +} + +/** + * @suppress + */ +public object FfiConverterTypeToolError : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ToolException = + when (buf.getInt()) { + 1 -> + ToolException.InvalidParameters( + FfiConverterString.read(buf), + ) + 2 -> + ToolException.ExecutionException( + FfiConverterString.read(buf), + ) + 3 -> + ToolException.SchemaException( + FfiConverterString.read(buf), + ) + 4 -> + ToolException.NotFound( + FfiConverterString.read(buf), + ) + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + + override fun allocationSize(value: ToolException): ULong = + when (value) { + is ToolException.InvalidParameters -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is ToolException.ExecutionException -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is ToolException.SchemaException -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + is ToolException.NotFound -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterString.allocationSize(value.v1) + ) + } + + override fun write( + value: ToolException, + buf: ByteBuffer, + ) { + when (value) { + is ToolException.InvalidParameters -> { + buf.putInt(1) + FfiConverterString.write(value.v1, buf) + Unit + } + is ToolException.ExecutionException -> { + buf.putInt(2) + FfiConverterString.write(value.v1, buf) + Unit + } + is ToolException.SchemaException -> { + buf.putInt(3) + FfiConverterString.write(value.v1, buf) + Unit + } + is ToolException.NotFound -> { + buf.putInt(4) + FfiConverterString.write(value.v1, buf) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } +} + +/** + * @suppress + */ +public object FfiConverterOptionalUInt : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.UInt? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterUInt.read(buf) + } + + override fun allocationSize(value: kotlin.UInt?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterUInt.allocationSize(value) + } + } + + override fun write( + value: kotlin.UInt?, + buf: ByteBuffer, + ) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterUInt.write(value, buf) + } + } +} + +/** + * @suppress + */ +public object FfiConverterOptionalInt : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.Int? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterInt.read(buf) + } + + override fun allocationSize(value: kotlin.Int?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterInt.allocationSize(value) + } + } + + override fun write( + value: kotlin.Int?, + buf: ByteBuffer, + ) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterInt.write(value, buf) + } + } +} + +/** + * @suppress + */ +public object FfiConverterOptionalFloat : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.Float? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterFloat.read(buf) + } + + override fun allocationSize(value: kotlin.Float?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterFloat.allocationSize(value) + } + } + + override fun write( + value: kotlin.Float?, + buf: ByteBuffer, + ) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterFloat.write(value, buf) + } + } +} + +/** + * @suppress + */ +public object FfiConverterOptionalDouble : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.Double? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterDouble.read(buf) + } + + override fun allocationSize(value: kotlin.Double?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterDouble.allocationSize(value) + } + } + + override fun write( + value: kotlin.Double?, + buf: ByteBuffer, + ) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterDouble.write(value, buf) + } + } +} + +/** + * @suppress + */ +public object FfiConverterOptionalString : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.String? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterString.read(buf) + } + + override fun allocationSize(value: kotlin.String?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterString.allocationSize(value) + } + } + + override fun write( + value: kotlin.String?, + buf: ByteBuffer, + ) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterString.write(value, buf) + } + } +} + +/** + * @suppress + */ +public object FfiConverterSequenceTypeExtensionConfig : FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeExtensionConfig.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterTypeExtensionConfig.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write( + value: List, + buf: ByteBuffer, + ) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeExtensionConfig.write(it, buf) + } + } +} + +/** + * @suppress + */ +public object FfiConverterSequenceTypeMessage : FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeMessage.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterTypeMessage.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write( + value: List, + buf: ByteBuffer, + ) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeMessage.write(it, buf) + } + } +} + +/** + * @suppress + */ +public object FfiConverterSequenceTypeMessageContent : FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeMessageContent.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterTypeMessageContent.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write( + value: List, + buf: ByteBuffer, + ) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeMessageContent.write(it, buf) + } + } +} + +/** + * @suppress + */ +public object FfiConverterSequenceTypeToolConfig : FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeToolConfig.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterTypeToolConfig.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write( + value: List, + buf: ByteBuffer, + ) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeToolConfig.write(it, buf) + } + } +} + +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias Contents = List +public typealias FfiConverterTypeContents = FfiConverterSequenceTypeMessageContent + +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias JsonValueFfi = kotlin.String +public typealias FfiConverterTypeJsonValueFfi = FfiConverterString + +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias ToolConfig = kotlin.String +public typealias FfiConverterTypeToolConfig = FfiConverterString + +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias ToolRequestToolCall = kotlin.String +public typealias FfiConverterTypeToolRequestToolCall = FfiConverterString + +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias ToolResponseToolResult = kotlin.String +public typealias FfiConverterTypeToolResponseToolResult = FfiConverterString + +/** + * Public API for the Goose LLM completion function + */ +@Throws(CompletionException::class) +@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") +suspend fun `completion`(`req`: CompletionRequest): CompletionResponse = + uniffiRustCallAsync( + UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_completion(FfiConverterTypeCompletionRequest.lower(`req`)), + { future, callback, continuation -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_poll_rust_buffer(future, callback, continuation) }, + { future, continuation -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_complete_rust_buffer(future, continuation) }, + { future -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_free_rust_buffer(future) }, + // lift function + { FfiConverterTypeCompletionResponse.lift(it) }, + // Error FFI converter + CompletionException.ErrorHandler, + ) + +fun `createToolConfig`( + `name`: kotlin.String, + `description`: kotlin.String, + `inputSchema`: JsonValueFfi, + `approvalMode`: ToolApprovalMode, +): ToolConfig = + FfiConverterTypeToolConfig.lift( + uniffiRustCall { _status -> + UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_create_tool_config( + FfiConverterString.lower(`name`), + FfiConverterString.lower(`description`), + FfiConverterTypeJsonValueFfi.lower(`inputSchema`), + FfiConverterTypeToolApprovalMode.lower(`approvalMode`), + _status, + ) + }, + ) + +/** + * Generates a short (≤4 words) session name + */ +@Throws(ProviderException::class) +@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") +suspend fun `generateSessionName`(`messages`: List): kotlin.String = + uniffiRustCallAsync( + UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_generate_session_name(FfiConverterSequenceTypeMessage.lower(`messages`)), + { future, callback, continuation -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_poll_rust_buffer(future, callback, continuation) }, + { future, continuation -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_complete_rust_buffer(future, continuation) }, + { future -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_free_rust_buffer(future) }, + // lift function + { FfiConverterString.lift(it) }, + // Error FFI converter + ProviderException.ErrorHandler, + ) + +/** + * Generates a tooltip summarizing the last two messages in the session, + * including any tool calls or results. + */ +@Throws(ProviderException::class) +@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") +suspend fun `generateTooltip`(`messages`: List): kotlin.String = + uniffiRustCallAsync( + UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_generate_tooltip(FfiConverterSequenceTypeMessage.lower(`messages`)), + { future, callback, continuation -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_poll_rust_buffer(future, callback, continuation) }, + { future, continuation -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_complete_rust_buffer(future, continuation) }, + { future -> UniffiLib.INSTANCE.ffi_goose_llm_rust_future_free_rust_buffer(future) }, + // lift function + { FfiConverterString.lift(it) }, + // Error FFI converter + ProviderException.ErrorHandler, + ) + +fun `printMessages`(`messages`: List) = + uniffiRustCall { _status -> + UniffiLib.INSTANCE.uniffi_goose_llm_fn_func_print_messages( + FfiConverterSequenceTypeMessage.lower(`messages`), + _status, + ) + } diff --git a/crates/goose-llm/Cargo.toml b/crates/goose-llm/Cargo.toml index 6bb99c42fc..2323d29e30 100644 --- a/crates/goose-llm/Cargo.toml +++ b/crates/goose-llm/Cargo.toml @@ -7,8 +7,11 @@ license.workspace = true repository.workspace = true description.workspace = true +[lib] +crate-type = ["lib", "cdylib"] +name = "goose_llm" + [dependencies] -tokio = { version = "1.43", features = ["full"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" anyhow = "1.0" @@ -36,6 +39,9 @@ regex = "1.11.1" tracing = "0.1" smallvec = { version = "1.13", features = ["serde"] } indoc = "1.0" +# https://github.com/mozilla/uniffi-rs/blob/c7f6caa3d1bf20f934346cefd8e82b5093f0dc6f/fixtures/futures/Cargo.toml#L22 +uniffi = { version = "0.29", features = ["tokio", "cli", "scaffolding-ffi-buffer-fns"] } +tokio = { version = "1.43", features = ["time", "sync"] } [dev-dependencies] criterion = "0.5" @@ -43,7 +49,12 @@ tempfile = "3.15.0" dotenv = "0.15" lazy_static = "1.5" ctor = "0.2.7" +tokio = { version = "1.43", features = ["full"] } +[[bin]] +# https://mozilla.github.io/uniffi-rs/latest/tutorial/foreign_language_bindings.html +name = "uniffi-bindgen" +path = "uniffi-bindgen.rs" [[example]] name = "simple" diff --git a/crates/goose-llm/README.md b/crates/goose-llm/README.md index 09e971d3d4..dcf205c88a 100644 --- a/crates/goose-llm/README.md +++ b/crates/goose-llm/README.md @@ -1,4 +1,4 @@ -### goose-llm +## goose-llm This crate is meant to be used for foreign function interface (FFI). It's meant to be stateless and contain logic related to providers and prompts: @@ -12,3 +12,59 @@ Run: cargo run -p goose-llm --example simple ``` + +## Kotlin bindings + +Structure: +``` +. +└── crates + └── goose-llm/... +└── target + └── debug/libgoose_llm.dylib +├── bindings +│ └── kotlin +│ ├── example +│ │ └── Usage.kt ← your demo app +│ └── uniffi +│ └── goose_llm +│ └── goose_llm.kt ← auto-generated bindings +``` + +Create Kotlin bindings: +``` +# run from project root directory +cargo build -p goose-llm + +cargo run --features=uniffi/cli --bin uniffi-bindgen generate --library ./target/debug/libgoose_llm.dylib --language kotlin --out-dir bindings/kotlin +``` + +Download jars in `bindings/kotlin/libs` directory (only need to do this once): +``` +pushd bindings/kotlin/libs/ +curl -O https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.9.0/kotlin-stdlib-1.9.0.jar +curl -O https://repo1.maven.org/maven2/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.7.3/kotlinx-coroutines-core-jvm-1.7.3.jar +curl -O https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.13.0/jna-5.13.0.jar +popd +``` + + +Compile & Run usage example from Kotlin -> Rust: +``` +pushd bindings/kotlin/ + +kotlinc \ + example/Usage.kt \ + uniffi/goose_llm/goose_llm.kt \ + -classpath "libs/kotlin-stdlib-1.9.0.jar:libs/kotlinx-coroutines-core-jvm-1.7.3.jar:libs/jna-5.13.0.jar" \ + -include-runtime \ + -d example.jar + +java \ + -Djna.library.path=$HOME/Development/goose/target/debug \ + -classpath "example.jar:libs/kotlin-stdlib-1.9.0.jar:libs/kotlinx-coroutines-core-jvm-1.7.3.jar:libs/jna-5.13.0.jar" \ + UsageKt + +popd +``` + diff --git a/crates/goose-llm/examples/simple.rs b/crates/goose-llm/examples/simple.rs index 4d6d704431..0760a98066 100644 --- a/crates/goose-llm/examples/simple.rs +++ b/crates/goose-llm/examples/simple.rs @@ -93,13 +93,13 @@ async fn main() -> Result<()> { println!("\n---------------\n"); println!("User Input: {text}"); let messages = vec![Message::user().with_text(text)]; - let completion_response: CompletionResponse = completion(CompletionRequest::new( - provider, - model_config.clone(), - system_preamble, - &messages, - &extensions, - )) + let completion_response: CompletionResponse = completion(CompletionRequest { + provider_name: provider.to_string(), + model_config: model_config.clone(), + system_preamble: system_preamble.to_string(), + messages: messages, + extensions: extensions.clone(), + }) .await?; // Print the response println!("\nCompletion Response:"); diff --git a/crates/goose-llm/src/completion.rs b/crates/goose-llm/src/completion.rs index cfa693d8dd..e4936b7007 100644 --- a/crates/goose-llm/src/completion.rs +++ b/crates/goose-llm/src/completion.rs @@ -17,32 +17,40 @@ use crate::{ }, }; +#[uniffi::export] +pub fn print_messages(messages: Vec) { + for msg in messages { + println!("[{:?} @ {}] {:?}", msg.role, msg.created, msg.content); + } +} + /// Public API for the Goose LLM completion function -pub async fn completion(req: CompletionRequest<'_>) -> Result { +#[uniffi::export(async_runtime = "tokio")] +pub async fn completion(req: CompletionRequest) -> Result { let start_total = Instant::now(); - let provider = create(req.provider_name, req.model_config) + let provider = create(&req.provider_name, req.model_config) .map_err(|_| CompletionError::UnknownProvider(req.provider_name.to_string()))?; - let system_prompt = construct_system_prompt(req.system_preamble, req.extensions)?; - let tools = collect_prefixed_tools(req.extensions); + let system_prompt = construct_system_prompt(&req.system_preamble, &req.extensions)?; + let tools = collect_prefixed_tools(&req.extensions); // Call the LLM provider let start_provider = Instant::now(); let mut response = provider - .complete(&system_prompt, req.messages, &tools) + .complete(&system_prompt, &req.messages, &tools) .await?; - let provider_elapsed_ms = start_provider.elapsed().as_millis(); + let provider_elapsed_sec = start_provider.elapsed().as_secs_f32(); let usage_tokens = response.usage.total_tokens; - let tool_configs = collect_prefixed_tool_configs(req.extensions); + let tool_configs = collect_prefixed_tool_configs(&req.extensions); update_needs_approval_for_tool_calls(&mut response.message, &tool_configs)?; Ok(CompletionResponse::new( response.message, response.model, response.usage, - calculate_runtime_metrics(start_total, provider_elapsed_ms, usage_tokens), + calculate_runtime_metrics(start_total, provider_elapsed_sec, usage_tokens), )) } @@ -81,8 +89,8 @@ pub fn update_needs_approval_for_tool_calls( tool_configs: &HashMap, ) -> Result<(), CompletionError> { for content in &mut message.content.iter_mut() { - if let MessageContent::ToolRequest(req) = content { - if let Ok(call) = &mut req.tool_call { + if let MessageContent::ToolReq(req) = content { + if let Ok(call) = &mut req.tool_call.0 { // Provide a clear error message when the tool config is missing let config = tool_configs.get(&call.name).ok_or_else(|| { CompletionError::ToolNotFound(format!( @@ -117,16 +125,16 @@ fn collect_prefixed_tool_configs(extensions: &[ExtensionConfig]) -> HashMap, ) -> RuntimeMetrics { - let total_ms = total_start.elapsed().as_millis(); + let total_ms = total_start.elapsed().as_secs_f32(); let tokens_per_sec = token_count.and_then(|toks| { - if provider_elapsed_ms > 0 { - Some(toks as f64 / (provider_elapsed_ms as f64 / 1_000.0)) + if provider_elapsed_sec > 0.0 { + Some(toks as f64 / (provider_elapsed_sec as f64)) } else { None } }); - RuntimeMetrics::new(total_ms, provider_elapsed_ms, tokens_per_sec) + RuntimeMetrics::new(total_ms, provider_elapsed_sec, tokens_per_sec) } diff --git a/crates/goose-llm/src/extractors/session_name.rs b/crates/goose-llm/src/extractors/session_name.rs index 4358a7ece4..6aeccd60da 100644 --- a/crates/goose-llm/src/extractors/session_name.rs +++ b/crates/goose-llm/src/extractors/session_name.rs @@ -49,6 +49,7 @@ fn build_system_prompt() -> String { } /// Generates a short (≤4 words) session name +#[uniffi::export(async_runtime = "tokio")] pub async fn generate_session_name(messages: &[Message]) -> Result { // Collect up to the first 3 user messages (truncated to 300 chars each) let context: Vec = messages diff --git a/crates/goose-llm/src/extractors/tooltip.rs b/crates/goose-llm/src/extractors/tooltip.rs index 164823c609..5529ef322b 100644 --- a/crates/goose-llm/src/extractors/tooltip.rs +++ b/crates/goose-llm/src/extractors/tooltip.rs @@ -53,6 +53,7 @@ fn build_system_prompt() -> String { /// Generates a tooltip summarizing the last two messages in the session, /// including any tool calls or results. +#[uniffi::export(async_runtime = "tokio")] pub async fn generate_tooltip(messages: &[Message]) -> Result { // Need at least two messages to summarize if messages.len() < 2 { @@ -72,17 +73,17 @@ pub async fn generate_tooltip(messages: &[Message]) -> Result { - if let Ok(tool_call) = &req.tool_call { + MessageContent::ToolReq(req) => { + if let Ok(tool_call) = &req.tool_call.0 { parts.push(format!( "called tool '{}' with args {}", tool_call.name, tool_call.arguments )); - } else if let Err(e) = &req.tool_call { + } else if let Err(e) = &req.tool_call.0 { parts.push(format!("tool request error: {}", e)); } } - MessageContent::ToolResponse(resp) => match &resp.tool_result { + MessageContent::ToolResp(resp) => match &resp.tool_result.0 { Ok(contents) => { let results: Vec = contents .iter() diff --git a/crates/goose-llm/src/lib.rs b/crates/goose-llm/src/lib.rs index 5dc8dac61b..02ea8333b2 100644 --- a/crates/goose-llm/src/lib.rs +++ b/crates/goose-llm/src/lib.rs @@ -1,3 +1,5 @@ +uniffi::setup_scaffolding!(); + mod completion; pub mod extractors; pub mod message; diff --git a/crates/goose-llm/src/message.rs b/crates/goose-llm/src/message.rs deleted file mode 100644 index a8271d649d..0000000000 --- a/crates/goose-llm/src/message.rs +++ /dev/null @@ -1,531 +0,0 @@ -use std::{collections::HashSet, iter::FromIterator, ops::Deref}; - -/// Messages which represent the content sent back and forth to LLM provider -/// -/// We use these messages in the agent code, and interfaces which interact with -/// the agent. That let's us reuse message histories across different interfaces. -/// -/// The content of the messages uses MCP types to avoid additional conversions -/// when interacting with MCP servers. -use chrono::Utc; -use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; - -use crate::types::core::{Content, ImageContent, Role, TextContent, ToolCall, ToolResult}; - -mod tool_result_serde; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ToolRequest { - pub id: String, - #[serde(with = "tool_result_serde")] - pub tool_call: ToolResult, -} - -impl ToolRequest { - pub fn to_readable_string(&self) -> String { - match &self.tool_call { - Ok(tool_call) => { - format!( - "Tool: {}, Args: {}", - tool_call.name, - serde_json::to_string_pretty(&tool_call.arguments) - .unwrap_or_else(|_| "<>".to_string()) - ) - } - Err(e) => format!("Invalid tool call: {}", e), - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ToolResponse { - pub id: String, - #[serde(with = "tool_result_serde")] - pub tool_result: ToolResult>, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ThinkingContent { - pub thinking: String, - pub signature: String, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct RedactedThinkingContent { - pub data: String, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -/// Content passed inside a message, which can be both simple content and tool content -#[serde(tag = "type", rename_all = "camelCase")] -pub enum MessageContent { - Text(TextContent), - Image(ImageContent), - ToolRequest(ToolRequest), - ToolResponse(ToolResponse), - Thinking(ThinkingContent), - RedactedThinking(RedactedThinkingContent), -} - -impl MessageContent { - pub fn text>(text: S) -> Self { - MessageContent::Text(TextContent { text: text.into() }) - } - - pub fn image, T: Into>(data: S, mime_type: T) -> Self { - MessageContent::Image(ImageContent { - data: data.into(), - mime_type: mime_type.into(), - }) - } - - pub fn tool_request>(id: S, tool_call: ToolResult) -> Self { - MessageContent::ToolRequest(ToolRequest { - id: id.into(), - tool_call, - }) - } - - pub fn tool_response>(id: S, tool_result: ToolResult>) -> Self { - MessageContent::ToolResponse(ToolResponse { - id: id.into(), - tool_result, - }) - } - - pub fn thinking, S2: Into>(thinking: S1, signature: S2) -> Self { - MessageContent::Thinking(ThinkingContent { - thinking: thinking.into(), - signature: signature.into(), - }) - } - - pub fn redacted_thinking>(data: S) -> Self { - MessageContent::RedactedThinking(RedactedThinkingContent { data: data.into() }) - } - - pub fn as_tool_request(&self) -> Option<&ToolRequest> { - if let MessageContent::ToolRequest(ref tool_request) = self { - Some(tool_request) - } else { - None - } - } - - pub fn as_tool_response(&self) -> Option<&ToolResponse> { - if let MessageContent::ToolResponse(ref tool_response) = self { - Some(tool_response) - } else { - None - } - } - - pub fn as_tool_response_text(&self) -> Option { - if let Some(tool_response) = self.as_tool_response() { - if let Ok(contents) = &tool_response.tool_result { - let texts: Vec = contents - .iter() - .filter_map(|content| content.as_text().map(String::from)) - .collect(); - if !texts.is_empty() { - return Some(texts.join("\n")); - } - } - } - None - } - - pub fn as_tool_request_id(&self) -> Option<&str> { - if let Self::ToolRequest(r) = self { - Some(&r.id) - } else { - None - } - } - - pub fn as_tool_response_id(&self) -> Option<&str> { - if let Self::ToolResponse(r) = self { - Some(&r.id) - } else { - None - } - } - - /// Get the text content if this is a TextContent variant - pub fn as_text(&self) -> Option<&str> { - match self { - MessageContent::Text(text) => Some(&text.text), - _ => None, - } - } - - /// Get the thinking content if this is a ThinkingContent variant - pub fn as_thinking(&self) -> Option<&ThinkingContent> { - match self { - MessageContent::Thinking(thinking) => Some(thinking), - _ => None, - } - } - - /// Get the redacted thinking content if this is a RedactedThinkingContent variant - pub fn as_redacted_thinking(&self) -> Option<&RedactedThinkingContent> { - match self { - MessageContent::RedactedThinking(redacted) => Some(redacted), - _ => None, - } - } - - pub fn is_text(&self) -> bool { - matches!(self, Self::Text(_)) - } - pub fn is_image(&self) -> bool { - matches!(self, Self::Image(_)) - } - pub fn is_tool_request(&self) -> bool { - matches!(self, Self::ToolRequest(_)) - } - pub fn is_tool_response(&self) -> bool { - matches!(self, Self::ToolResponse(_)) - } -} - -impl From for MessageContent { - fn from(content: Content) -> Self { - match content { - Content::Text(text) => MessageContent::Text(text), - Content::Image(image) => MessageContent::Image(image), - } - } -} - -// ──────────────────────────────────────────────────────────────────────────── -// 2. Contents – a new-type wrapper around SmallVec -// ──────────────────────────────────────────────────────────────────────────── - -/// Holds the heterogeneous fragments that make up one chat message. -/// -/// * Up to two items are stored inline on the stack. -/// * Falls back to a heap allocation only when necessary. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] -#[serde(transparent)] -pub struct Contents(SmallVec<[MessageContent; 2]>); - -impl Contents { - /*---------------------------------------------------------- - * 1-line ergonomic helpers - *---------------------------------------------------------*/ - - pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, MessageContent> { - self.0.iter_mut() - } - - pub fn push(&mut self, item: impl Into) { - self.0.push(item.into()); - } - - pub fn texts(&self) -> impl Iterator { - self.0.iter().filter_map(|c| c.as_text()) - } - - pub fn concat_text_str(&self) -> String { - self.texts().collect::>().join("\n") - } - - /// Returns `true` if *any* item satisfies the predicate. - pub fn any_is

(&self, pred: P) -> bool - where - P: FnMut(&MessageContent) -> bool, - { - self.iter().any(pred) - } - - /// Returns `true` if *every* item satisfies the predicate. - pub fn all_are

(&self, pred: P) -> bool - where - P: FnMut(&MessageContent) -> bool, - { - self.iter().all(pred) - } -} - -impl From> for Contents { - fn from(v: Vec) -> Self { - Contents(SmallVec::from_vec(v)) - } -} - -impl FromIterator for Contents { - fn from_iter>(iter: I) -> Self { - Contents(SmallVec::from_iter(iter)) - } -} - -/*-------------------------------------------------------------- - * Allow &message.content to behave like a slice of fragments. - *-------------------------------------------------------------*/ -impl Deref for Contents { - type Target = [MessageContent]; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -/// A message to or from an LLM -#[serde(rename_all = "camelCase")] -pub struct Message { - pub role: Role, - pub created: i64, - pub content: Contents, -} - -impl Message { - pub fn new(role: Role) -> Self { - Self { - role, - created: Utc::now().timestamp_millis(), - content: Contents::default(), - } - } - - /// Create a new user message with the current timestamp - pub fn user() -> Self { - Self::new(Role::User) - } - - /// Create a new assistant message with the current timestamp - pub fn assistant() -> Self { - Self::new(Role::Assistant) - } - - /// Add any item that implements Into to the message - pub fn with_content(mut self, item: impl Into) -> Self { - self.content.push(item); - self - } - - /// Add text content to the message - pub fn with_text>(self, text: S) -> Self { - self.with_content(MessageContent::text(text)) - } - - /// Add image content to the message - pub fn with_image, T: Into>(self, data: S, mime_type: T) -> Self { - self.with_content(MessageContent::image(data, mime_type)) - } - - /// Add a tool request to the message - pub fn with_tool_request>( - self, - id: S, - tool_call: ToolResult, - ) -> Self { - self.with_content(MessageContent::tool_request(id, tool_call)) - } - - /// Add a tool response to the message - pub fn with_tool_response>( - self, - id: S, - result: ToolResult>, - ) -> Self { - self.with_content(MessageContent::tool_response(id, result)) - } - - /// Add thinking content to the message - pub fn with_thinking, S2: Into>( - self, - thinking: S1, - signature: S2, - ) -> Self { - self.with_content(MessageContent::thinking(thinking, signature)) - } - - /// Add redacted thinking content to the message - pub fn with_redacted_thinking>(self, data: S) -> Self { - self.with_content(MessageContent::redacted_thinking(data)) - } - - /// Check if the message is a tool call - pub fn contains_tool_call(&self) -> bool { - self.content.any_is(MessageContent::is_tool_request) - } - - /// Check if the message is a tool response - pub fn contains_tool_response(&self) -> bool { - self.content.any_is(MessageContent::is_tool_response) - } - - /// Check if the message contains only text content - pub fn has_only_text_content(&self) -> bool { - self.content.all_are(MessageContent::is_text) - } - - /// Retrieves all tool `id` from ToolRequest messages - pub fn tool_request_ids(&self) -> HashSet<&str> { - self.content - .iter() - .filter_map(MessageContent::as_tool_request_id) - .collect() - } - - /// Retrieves all tool `id` from ToolResponse messages - pub fn tool_response_ids(&self) -> HashSet<&str> { - self.content - .iter() - .filter_map(MessageContent::as_tool_response_id) - .collect() - } - - /// Retrieves all tool `id` from the message - pub fn tool_ids(&self) -> HashSet<&str> { - self.tool_request_ids() - .into_iter() - .chain(self.tool_response_ids()) - .collect() - } -} - -#[cfg(test)] -mod tests { - use serde_json::{json, Value}; - - use super::*; - use crate::types::core::ToolError; - - #[test] - fn test_message_serialization() { - let message = Message::assistant() - .with_text("Hello, I'll help you with that.") - .with_tool_request( - "tool123", - Ok(ToolCall::new("test_tool", json!({"param": "value"}))), - ); - - let json_str = serde_json::to_string_pretty(&message).unwrap(); - println!("Serialized message: {}", json_str); - - // Parse back to Value to check structure - let value: Value = serde_json::from_str(&json_str).unwrap(); - - // Check top-level fields - assert_eq!(value["role"], "assistant"); - assert!(value["created"].is_i64()); - assert!(value["content"].is_array()); - - // Check content items - let content = &value["content"]; - - // First item should be text - assert_eq!(content[0]["type"], "text"); - assert_eq!(content[0]["text"], "Hello, I'll help you with that."); - - // Second item should be toolRequest - assert_eq!(content[1]["type"], "toolRequest"); - assert_eq!(content[1]["id"], "tool123"); - - // Check tool_call serialization - assert_eq!(content[1]["toolCall"]["status"], "success"); - assert_eq!(content[1]["toolCall"]["value"]["name"], "test_tool"); - assert_eq!( - content[1]["toolCall"]["value"]["arguments"]["param"], - "value" - ); - } - - #[test] - fn test_error_serialization() { - let message = Message::assistant().with_tool_request( - "tool123", - Err(ToolError::ExecutionError( - "Something went wrong".to_string(), - )), - ); - - let json_str = serde_json::to_string_pretty(&message).unwrap(); - println!("Serialized error: {}", json_str); - - // Parse back to Value to check structure - let value: Value = serde_json::from_str(&json_str).unwrap(); - - // Check tool_call serialization with error - let tool_call = &value["content"][0]["toolCall"]; - assert_eq!(tool_call["status"], "error"); - assert_eq!(tool_call["error"], "Execution failed: Something went wrong"); - } - - #[test] - fn test_deserialization() { - // Create a JSON string with our new format - let json_str = r#"{ - "role": "assistant", - "created": 1740171566, - "content": [ - { - "type": "text", - "text": "I'll help you with that." - }, - { - "type": "toolRequest", - "id": "tool123", - "toolCall": { - "status": "success", - "value": { - "name": "test_tool", - "arguments": {"param": "value"}, - "needsApproval": false - } - } - } - ] - }"#; - - let message: Message = serde_json::from_str(json_str).unwrap(); - - assert_eq!(message.role, Role::Assistant); - assert_eq!(message.created, 1740171566); - assert_eq!(message.content.len(), 2); - - // Check first content item - if let MessageContent::Text(text) = &message.content[0] { - assert_eq!(text.text, "I'll help you with that."); - } else { - panic!("Expected Text content"); - } - - // Check second content item - if let MessageContent::ToolRequest(req) = &message.content[1] { - assert_eq!(req.id, "tool123"); - if let Ok(tool_call) = &req.tool_call { - assert_eq!(tool_call.name, "test_tool"); - assert_eq!(tool_call.arguments, json!({"param": "value"})); - } else { - panic!("Expected successful tool call"); - } - } else { - panic!("Expected ToolRequest content"); - } - } - - #[test] - fn test_message_with_text() { - let message = Message::user().with_text("Hello"); - assert_eq!(message.content.concat_text_str(), "Hello"); - } - - #[test] - fn test_message_with_tool_request() { - let tool_call = Ok(ToolCall::new("test_tool", json!({}))); - - let message = Message::assistant().with_tool_request("req1", tool_call); - assert!(message.contains_tool_call()); - assert!(!message.contains_tool_response()); - - let ids = message.tool_ids(); - assert_eq!(ids.len(), 1); - assert!(ids.contains("req1")); - } -} diff --git a/crates/goose-llm/src/message/contents.rs b/crates/goose-llm/src/message/contents.rs new file mode 100644 index 0000000000..89e80f790d --- /dev/null +++ b/crates/goose-llm/src/message/contents.rs @@ -0,0 +1,84 @@ +use std::{iter::FromIterator, ops::Deref}; + +use crate::message::MessageContent; +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; + +/// Holds the heterogeneous fragments that make up one chat message. +/// +/// * Up to two items are stored inline on the stack. +/// * Falls back to a heap allocation only when necessary. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(transparent)] +pub struct Contents(SmallVec<[MessageContent; 2]>); + +impl Contents { + /*---------------------------------------------------------- + * 1-line ergonomic helpers + *---------------------------------------------------------*/ + + pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, MessageContent> { + self.0.iter_mut() + } + + pub fn push(&mut self, item: impl Into) { + self.0.push(item.into()); + } + + pub fn texts(&self) -> impl Iterator { + self.0.iter().filter_map(|c| c.as_text()) + } + + pub fn concat_text_str(&self) -> String { + self.texts().collect::>().join("\n") + } + + /// Returns `true` if *any* item satisfies the predicate. + pub fn any_is

(&self, pred: P) -> bool + where + P: FnMut(&MessageContent) -> bool, + { + self.iter().any(pred) + } + + /// Returns `true` if *every* item satisfies the predicate. + pub fn all_are

(&self, pred: P) -> bool + where + P: FnMut(&MessageContent) -> bool, + { + self.iter().all(pred) + } +} + +impl From> for Contents { + fn from(v: Vec) -> Self { + Contents(SmallVec::from_vec(v)) + } +} + +impl FromIterator for Contents { + fn from_iter>(iter: I) -> Self { + Contents(SmallVec::from_iter(iter)) + } +} + +/*-------------------------------------------------------------- + * Allow &message.content to behave like a slice of fragments. + *-------------------------------------------------------------*/ +impl Deref for Contents { + type Target = [MessageContent]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// — Register the contents type with UniFFI, converting to/from Vec — +// We need to do this because UniFFI’s FFI layer supports only primitive buffers (here Vec), +uniffi::custom_type!(Contents, Vec, { + lower: |contents: &Contents| { + contents.0.to_vec() + }, + try_lift: |contents: Vec| { + Ok(Contents::from(contents)) + }, +}); diff --git a/crates/goose-llm/src/message/message_content.rs b/crates/goose-llm/src/message/message_content.rs new file mode 100644 index 0000000000..b657fbfb00 --- /dev/null +++ b/crates/goose-llm/src/message/message_content.rs @@ -0,0 +1,240 @@ +use serde::{Deserialize, Serialize}; +use serde_json; + +use crate::message::tool_result_serde; +use crate::types::core::{Content, ImageContent, TextContent, ToolCall, ToolResult}; + +// — Newtype wrappers (local structs) so we satisfy Rust’s orphan rules — +// We need these because we can’t implement UniFFI’s FfiConverter directly on a type alias. + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ToolRequestToolCall(#[serde(with = "tool_result_serde")] pub ToolResult); + +impl ToolRequestToolCall { + pub fn as_result(&self) -> &ToolResult { + &self.0 + } +} +impl std::ops::Deref for ToolRequestToolCall { + type Target = ToolResult; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl From> for ToolRequestToolCall { + fn from(res: Result) -> Self { + ToolRequestToolCall(res) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ToolResponseToolResult( + #[serde(with = "tool_result_serde")] pub ToolResult>, +); + +impl ToolResponseToolResult { + pub fn as_result(&self) -> &ToolResult> { + &self.0 + } +} +impl std::ops::Deref for ToolResponseToolResult { + type Target = ToolResult>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl From, crate::types::core::ToolError>> for ToolResponseToolResult { + fn from(res: Result, crate::types::core::ToolError>) -> Self { + ToolResponseToolResult(res) + } +} + +// — Register the newtypes with UniFFI, converting via JSON strings — +// UniFFI’s FFI layer supports only primitive buffers (here String), so we JSON-serialize +// through our `tool_result_serde` to preserve the same success/error schema on both sides. + +uniffi::custom_type!(ToolRequestToolCall, String, { + lower: |obj| { + serde_json::to_string(&obj.0).unwrap() + }, + try_lift: |val| { + Ok(serde_json::from_str(&val).unwrap() ) + }, +}); + +uniffi::custom_type!(ToolResponseToolResult, String, { + lower: |obj| { + serde_json::to_string(&obj.0).unwrap() + }, + try_lift: |val| { + Ok(serde_json::from_str(&val).unwrap() ) + }, +}); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Record)] +#[serde(rename_all = "camelCase")] +pub struct ToolRequest { + pub id: String, + pub tool_call: ToolRequestToolCall, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Record)] +#[serde(rename_all = "camelCase")] +pub struct ToolResponse { + pub id: String, + pub tool_result: ToolResponseToolResult, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Record)] +pub struct ThinkingContent { + pub thinking: String, + pub signature: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Record)] +pub struct RedactedThinkingContent { + pub data: String, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Enum)] +/// Content passed inside a message, which can be both simple content and tool content +#[serde(tag = "type", rename_all = "camelCase")] +pub enum MessageContent { + Text(TextContent), + Image(ImageContent), + ToolReq(ToolRequest), + ToolResp(ToolResponse), + Thinking(ThinkingContent), + RedactedThinking(RedactedThinkingContent), +} + +impl MessageContent { + pub fn text>(text: S) -> Self { + MessageContent::Text(TextContent { text: text.into() }) + } + + pub fn image, T: Into>(data: S, mime_type: T) -> Self { + MessageContent::Image(ImageContent { + data: data.into(), + mime_type: mime_type.into(), + }) + } + + pub fn tool_request>(id: S, tool_call: ToolRequestToolCall) -> Self { + MessageContent::ToolReq(ToolRequest { + id: id.into(), + tool_call, + }) + } + + pub fn tool_response>(id: S, tool_result: ToolResponseToolResult) -> Self { + MessageContent::ToolResp(ToolResponse { + id: id.into(), + tool_result, + }) + } + + pub fn thinking, S2: Into>(thinking: S1, signature: S2) -> Self { + MessageContent::Thinking(ThinkingContent { + thinking: thinking.into(), + signature: signature.into(), + }) + } + + pub fn redacted_thinking>(data: S) -> Self { + MessageContent::RedactedThinking(RedactedThinkingContent { data: data.into() }) + } + + pub fn as_tool_request(&self) -> Option<&ToolRequest> { + if let MessageContent::ToolReq(ref tool_request) = self { + Some(tool_request) + } else { + None + } + } + + pub fn as_tool_response(&self) -> Option<&ToolResponse> { + if let MessageContent::ToolResp(ref tool_response) = self { + Some(tool_response) + } else { + None + } + } + + pub fn as_tool_response_text(&self) -> Option { + if let Some(tool_response) = self.as_tool_response() { + if let Ok(contents) = &tool_response.tool_result.0 { + let texts: Vec = contents + .iter() + .filter_map(|content| content.as_text().map(String::from)) + .collect(); + if !texts.is_empty() { + return Some(texts.join("\n")); + } + } + } + None + } + + pub fn as_tool_request_id(&self) -> Option<&str> { + if let Self::ToolReq(r) = self { + Some(&r.id) + } else { + None + } + } + + pub fn as_tool_response_id(&self) -> Option<&str> { + if let Self::ToolResp(r) = self { + Some(&r.id) + } else { + None + } + } + + /// Get the text content if this is a TextContent variant + pub fn as_text(&self) -> Option<&str> { + match self { + MessageContent::Text(text) => Some(&text.text), + _ => None, + } + } + + /// Get the thinking content if this is a ThinkingContent variant + pub fn as_thinking(&self) -> Option<&ThinkingContent> { + match self { + MessageContent::Thinking(thinking) => Some(thinking), + _ => None, + } + } + + /// Get the redacted thinking content if this is a RedactedThinkingContent variant + pub fn as_redacted_thinking(&self) -> Option<&RedactedThinkingContent> { + match self { + MessageContent::RedactedThinking(redacted) => Some(redacted), + _ => None, + } + } + + pub fn is_text(&self) -> bool { + matches!(self, Self::Text(_)) + } + pub fn is_image(&self) -> bool { + matches!(self, Self::Image(_)) + } + pub fn is_tool_request(&self) -> bool { + matches!(self, Self::ToolReq(_)) + } + pub fn is_tool_response(&self) -> bool { + matches!(self, Self::ToolResp(_)) + } +} + +impl From for MessageContent { + fn from(content: Content) -> Self { + match content { + Content::Text(text) => MessageContent::Text(text), + Content::Image(image) => MessageContent::Image(image), + } + } +} diff --git a/crates/goose-llm/src/message/mod.rs b/crates/goose-llm/src/message/mod.rs new file mode 100644 index 0000000000..0212fbc3fb --- /dev/null +++ b/crates/goose-llm/src/message/mod.rs @@ -0,0 +1,284 @@ +//! Messages which represent the content sent back and forth to LLM provider +//! +//! We use these messages in the agent code, and interfaces which interact with +//! the agent. That let's us reuse message histories across different interfaces. +//! +//! The content of the messages uses MCP types to avoid additional conversions +//! when interacting with MCP servers. + +mod contents; +mod message_content; +mod tool_result_serde; + +pub use contents::Contents; +pub use message_content::{ + MessageContent, RedactedThinkingContent, ThinkingContent, ToolRequest, ToolRequestToolCall, + ToolResponse, ToolResponseToolResult, +}; + +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +use crate::types::core::Role; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Record)] +/// A message to or from an LLM +#[serde(rename_all = "camelCase")] +pub struct Message { + pub role: Role, + pub created: i64, + pub content: Contents, +} + +impl Message { + pub fn new(role: Role) -> Self { + Self { + role, + created: Utc::now().timestamp_millis(), + content: Contents::default(), + } + } + + /// Create a new user message with the current timestamp + pub fn user() -> Self { + Self::new(Role::User) + } + + /// Create a new assistant message with the current timestamp + pub fn assistant() -> Self { + Self::new(Role::Assistant) + } + + /// Add any item that implements Into to the message + pub fn with_content(mut self, item: impl Into) -> Self { + self.content.push(item); + self + } + + /// Add text content to the message + pub fn with_text>(self, text: S) -> Self { + self.with_content(MessageContent::text(text)) + } + + /// Add image content to the message + pub fn with_image, T: Into>(self, data: S, mime_type: T) -> Self { + self.with_content(MessageContent::image(data, mime_type)) + } + + /// Add a tool request to the message + pub fn with_tool_request, T: Into>( + self, + id: S, + tool_call: T, + ) -> Self { + self.with_content(MessageContent::tool_request(id, tool_call.into())) + } + + /// Add a tool response to the message + pub fn with_tool_response>( + self, + id: S, + result: ToolResponseToolResult, + ) -> Self { + self.with_content(MessageContent::tool_response(id, result)) + } + + /// Add thinking content to the message + pub fn with_thinking, S2: Into>( + self, + thinking: S1, + signature: S2, + ) -> Self { + self.with_content(MessageContent::thinking(thinking, signature)) + } + + /// Add redacted thinking content to the message + pub fn with_redacted_thinking>(self, data: S) -> Self { + self.with_content(MessageContent::redacted_thinking(data)) + } + + /// Check if the message is a tool call + pub fn contains_tool_call(&self) -> bool { + self.content.any_is(MessageContent::is_tool_request) + } + + /// Check if the message is a tool response + pub fn contains_tool_response(&self) -> bool { + self.content.any_is(MessageContent::is_tool_response) + } + + /// Check if the message contains only text content + pub fn has_only_text_content(&self) -> bool { + self.content.all_are(MessageContent::is_text) + } + + /// Retrieves all tool `id` from ToolRequest messages + pub fn tool_request_ids(&self) -> HashSet<&str> { + self.content + .iter() + .filter_map(MessageContent::as_tool_request_id) + .collect() + } + + /// Retrieves all tool `id` from ToolResponse messages + pub fn tool_response_ids(&self) -> HashSet<&str> { + self.content + .iter() + .filter_map(MessageContent::as_tool_response_id) + .collect() + } + + /// Retrieves all tool `id` from the message + pub fn tool_ids(&self) -> HashSet<&str> { + self.tool_request_ids() + .into_iter() + .chain(self.tool_response_ids()) + .collect() + } +} + +#[cfg(test)] +mod tests { + use serde_json::{json, Value}; + + use super::*; + use crate::types::core::{ToolCall, ToolError}; + + #[test] + fn test_message_serialization() { + let message = Message::assistant() + .with_text("Hello, I'll help you with that.") + .with_tool_request( + "tool123", + Ok(ToolCall::new("test_tool", json!({"param": "value"})).into()), + ); + + let json_str = serde_json::to_string_pretty(&message).unwrap(); + println!("Serialized message: {}", json_str); + + // Parse back to Value to check structure + let value: Value = serde_json::from_str(&json_str).unwrap(); + println!( + "Read back serialized message: {}", + serde_json::to_string_pretty(&value).unwrap() + ); + + // Check top-level fields + assert_eq!(value["role"], "assistant"); + assert!(value["created"].is_i64()); + assert!(value["content"].is_array()); + + // Check content items + let content = &value["content"]; + + // First item should be text + assert_eq!(content[0]["type"], "text"); + assert_eq!(content[0]["text"], "Hello, I'll help you with that."); + + // Second item should be toolRequest + assert_eq!(content[1]["type"], "toolReq"); + assert_eq!(content[1]["id"], "tool123"); + + // Check tool_call serialization + assert_eq!(content[1]["toolCall"]["status"], "success"); + assert_eq!(content[1]["toolCall"]["value"]["name"], "test_tool"); + assert_eq!( + content[1]["toolCall"]["value"]["arguments"]["param"], + "value" + ); + } + + #[test] + fn test_error_serialization() { + let message = Message::assistant().with_tool_request( + "tool123", + Err(ToolError::ExecutionError( + "Something went wrong".to_string(), + )), + ); + + let json_str = serde_json::to_string_pretty(&message).unwrap(); + println!("Serialized error: {}", json_str); + + // Parse back to Value to check structure + let value: Value = serde_json::from_str(&json_str).unwrap(); + + // Check tool_call serialization with error + let tool_call = &value["content"][0]["toolCall"]; + assert_eq!(tool_call["status"], "error"); + assert_eq!(tool_call["error"], "Execution failed: Something went wrong"); + } + + #[test] + fn test_deserialization() { + // Create a JSON string with our new format + let json_str = r#"{ + "role": "assistant", + "created": 1740171566, + "content": [ + { + "type": "text", + "text": "I'll help you with that." + }, + { + "type": "toolReq", + "id": "tool123", + "toolCall": { + "status": "success", + "value": { + "name": "test_tool", + "arguments": {"param": "value"}, + "needsApproval": false + } + } + } + ] + }"#; + + let message: Message = serde_json::from_str(json_str).unwrap(); + + assert_eq!(message.role, Role::Assistant); + assert_eq!(message.created, 1740171566); + assert_eq!(message.content.len(), 2); + + // Check first content item + if let MessageContent::Text(text) = &message.content[0] { + assert_eq!(text.text, "I'll help you with that."); + } else { + panic!("Expected Text content"); + } + + // Check second content item + if let MessageContent::ToolReq(req) = &message.content[1] { + assert_eq!(req.id, "tool123"); + if let Ok(tool_call) = req.tool_call.as_result() { + assert_eq!(tool_call.name, "test_tool"); + assert_eq!(tool_call.arguments, json!({"param": "value"})); + } else { + panic!("Expected successful tool call"); + } + } else { + panic!("Expected ToolRequest content"); + } + } + + #[test] + fn test_message_with_text() { + let message = Message::user().with_text("Hello"); + assert_eq!(message.content.concat_text_str(), "Hello"); + } + + #[test] + fn test_message_with_tool_request() { + let tool_call = Ok(ToolCall::new("test_tool", json!({}))); + + let message = Message::assistant().with_tool_request("req1", tool_call); + assert!(message.contains_tool_call()); + assert!(!message.contains_tool_response()); + + let ids = message.tool_ids(); + assert_eq!(ids.len(), 1); + assert!(ids.contains("req1")); + } +} diff --git a/crates/goose-llm/src/model.rs b/crates/goose-llm/src/model.rs index 5e0d92d9cb..ca9ecdad2e 100644 --- a/crates/goose-llm/src/model.rs +++ b/crates/goose-llm/src/model.rs @@ -1,14 +1,14 @@ use serde::{Deserialize, Serialize}; -const DEFAULT_CONTEXT_LIMIT: usize = 128_000; +const DEFAULT_CONTEXT_LIMIT: u32 = 128_000; /// Configuration for model-specific settings and limits -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct ModelConfig { /// The name of the model to use pub model_name: String, /// Optional explicit context limit that overrides any defaults - pub context_limit: Option, + pub context_limit: Option, /// Optional temperature setting (0.0 - 1.0) pub temperature: Option, /// Optional maximum tokens to generate @@ -34,7 +34,7 @@ impl ModelConfig { } /// Get model-specific context limit based on model name - fn get_model_specific_limit(model_name: &str) -> Option { + fn get_model_specific_limit(model_name: &str) -> Option { // Implement some sensible defaults match model_name { // OpenAI models, https://platform.openai.com/docs/models#models-overview @@ -52,7 +52,7 @@ impl ModelConfig { } /// Set an explicit context limit - pub fn with_context_limit(mut self, limit: Option) -> Self { + pub fn with_context_limit(mut self, limit: Option) -> Self { // Default is None and therefore DEFAULT_CONTEXT_LIMIT, only set // if input is Some to allow passing through with_context_limit in // configuration cases @@ -76,7 +76,7 @@ impl ModelConfig { /// Get the context_limit for the current model /// If none are defined, use the DEFAULT_CONTEXT_LIMIT - pub fn context_limit(&self) -> usize { + pub fn context_limit(&self) -> u32 { self.context_limit.unwrap_or(DEFAULT_CONTEXT_LIMIT) } } diff --git a/crates/goose-llm/src/providers/base.rs b/crates/goose-llm/src/providers/base.rs index c03f5f0412..2a6a918fc0 100644 --- a/crates/goose-llm/src/providers/base.rs +++ b/crates/goose-llm/src/providers/base.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use super::errors::ProviderError; use crate::{message::Message, types::core::Tool}; -#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, uniffi::Record)] pub struct Usage { pub input_tokens: Option, pub output_tokens: Option, @@ -26,7 +26,7 @@ impl Usage { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, uniffi::Record)] pub struct ProviderCompleteResponse { pub message: Message, pub model: String, diff --git a/crates/goose-llm/src/providers/errors.rs b/crates/goose-llm/src/providers/errors.rs index c44ff14070..826a6e1167 100644 --- a/crates/goose-llm/src/providers/errors.rs +++ b/crates/goose-llm/src/providers/errors.rs @@ -1,6 +1,6 @@ use thiserror::Error; -#[derive(Error, Debug)] +#[derive(Error, Debug, uniffi::Error)] pub enum ProviderError { #[error("Authentication error: {0}")] Authentication(String), diff --git a/crates/goose-llm/src/providers/formats/databricks.rs b/crates/goose-llm/src/providers/formats/databricks.rs index 2c621e6870..649d2f67f1 100644 --- a/crates/goose-llm/src/providers/formats/databricks.rs +++ b/crates/goose-llm/src/providers/formats/databricks.rs @@ -83,9 +83,9 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec< ] })); } - MessageContent::ToolRequest(request) => { + MessageContent::ToolReq(request) => { has_tool_calls = true; - match &request.tool_call { + match &request.tool_call.as_result() { Ok(tool_call) => { let sanitized_name = sanitize_function_name(&tool_call.name); @@ -114,8 +114,8 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec< } } } - MessageContent::ToolResponse(response) => { - match &response.tool_result { + MessageContent::ToolResp(response) => { + match &response.tool_result.0 { Ok(contents) => { // Process all content, replacing images with placeholder text let mut tool_content = Vec::new(); @@ -300,13 +300,13 @@ pub fn response_to_message(response: Value) -> anyhow::Result { "The provided function name '{}' had invalid characters, it must match this regex [a-zA-Z0-9_-]+", function_name )); - content.push(MessageContent::tool_request(id, Err(error))); + content.push(MessageContent::tool_request(id, Err(error).into())); } else { match serde_json::from_str::(&arguments) { Ok(params) => { content.push(MessageContent::tool_request( id, - Ok(ToolCall::new(&function_name, params)), + Ok(ToolCall::new(&function_name, params)).into(), )); } Err(e) => { @@ -314,7 +314,7 @@ pub fn response_to_message(response: Value) -> anyhow::Result { "Could not interpret tool use parameters for id {}: {}", id, e )); - content.push(MessageContent::tool_request(id, Err(error))); + content.push(MessageContent::tool_request(id, Err(error).into())); } } } @@ -681,19 +681,20 @@ mod tests { Message::user().with_text("How are you?"), Message::assistant().with_tool_request( "tool1", - Ok(ToolCall::new("example", json!({"param1": "value1"}))), + Ok(ToolCall::new("example", json!({"param1": "value1"})).into()), ), ]; // Get the ID from the tool request to use in the response - let tool_id = if let MessageContent::ToolRequest(request) = &messages[2].content[0] { + let tool_id = if let MessageContent::ToolReq(request) = &messages[2].content[0] { request.id.clone() } else { panic!("should be tool request"); }; - messages - .push(Message::user().with_tool_response(tool_id, Ok(vec![Content::text("Result")]))); + messages.push( + Message::user().with_tool_response(tool_id, Ok(vec![Content::text("Result")]).into()), + ); let spec = format_messages(&messages, &ImageFormat::OpenAi); @@ -719,14 +720,15 @@ mod tests { )]; // Get the ID from the tool request to use in the response - let tool_id = if let MessageContent::ToolRequest(request) = &messages[0].content[0] { + let tool_id = if let MessageContent::ToolReq(request) = &messages[0].content[0] { request.id.clone() } else { panic!("should be tool request"); }; - messages - .push(Message::user().with_tool_response(tool_id, Ok(vec![Content::text("Result")]))); + messages.push( + Message::user().with_tool_response(tool_id, Ok(vec![Content::text("Result")]).into()), + ); let spec = format_messages(&messages, &ImageFormat::OpenAi); @@ -857,7 +859,7 @@ mod tests { let message = response_to_message(response)?; assert_eq!(message.content.len(), 1); - if let MessageContent::ToolRequest(request) = &message.content[0] { + if let MessageContent::ToolReq(request) = &message.content[0] { let tool_call = request.tool_call.as_ref().unwrap(); assert_eq!(tool_call.name, "example_fn"); assert_eq!(tool_call.arguments, json!({"param": "value"})); @@ -876,8 +878,8 @@ mod tests { let message = response_to_message(response)?; - if let MessageContent::ToolRequest(request) = &message.content[0] { - match &request.tool_call { + if let MessageContent::ToolReq(request) = &message.content[0] { + match &request.tool_call.as_result() { Err(ToolError::NotFound(msg)) => { assert!(msg.starts_with("The provided function name")); } @@ -898,8 +900,8 @@ mod tests { let message = response_to_message(response)?; - if let MessageContent::ToolRequest(request) = &message.content[0] { - match &request.tool_call { + if let MessageContent::ToolReq(request) = &message.content[0] { + match &request.tool_call.as_result() { Err(ToolError::InvalidParameters(msg)) => { assert!(msg.starts_with("Could not interpret tool use parameters")); } @@ -920,7 +922,7 @@ mod tests { let message = response_to_message(response)?; - if let MessageContent::ToolRequest(request) = &message.content[0] { + if let MessageContent::ToolReq(request) = &message.content[0] { let tool_call = request.tool_call.as_ref().unwrap(); assert_eq!(tool_call.name, "example_fn"); assert_eq!(tool_call.arguments, json!({})); diff --git a/crates/goose-llm/src/providers/formats/openai.rs b/crates/goose-llm/src/providers/formats/openai.rs index be52dde56f..06fd527eee 100644 --- a/crates/goose-llm/src/providers/formats/openai.rs +++ b/crates/goose-llm/src/providers/formats/openai.rs @@ -56,7 +56,7 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec< // Redacted thinking blocks are not directly used in OpenAI format continue; } - MessageContent::ToolRequest(request) => match &request.tool_call { + MessageContent::ToolReq(request) => match &request.tool_call.as_result() { Ok(tool_call) => { let sanitized_name = sanitize_function_name(&tool_call.name); let tool_calls = converted @@ -82,8 +82,8 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec< })); } }, - MessageContent::ToolResponse(response) => { - match &response.tool_result { + MessageContent::ToolResp(response) => { + match &response.tool_result.0 { Ok(contents) => { // Process all content, replacing images with placeholder text let mut tool_content = Vec::new(); @@ -210,13 +210,13 @@ pub fn response_to_message(response: Value) -> anyhow::Result { "The provided function name '{}' had invalid characters, it must match this regex [a-zA-Z0-9_-]+", function_name )); - content.push(MessageContent::tool_request(id, Err(error))); + content.push(MessageContent::tool_request(id, Err(error).into())); } else { match serde_json::from_str::(&arguments) { Ok(params) => { content.push(MessageContent::tool_request( id, - Ok(ToolCall::new(&function_name, params)), + Ok(ToolCall::new(&function_name, params)).into(), )); } Err(e) => { @@ -224,7 +224,7 @@ pub fn response_to_message(response: Value) -> anyhow::Result { "Could not interpret tool use parameters for id {}: {}", id, e )); - content.push(MessageContent::tool_request(id, Err(error))); + content.push(MessageContent::tool_request(id, Err(error).into())); } } } @@ -559,14 +559,15 @@ mod tests { ]; // Get the ID from the tool request to use in the response - let tool_id = if let MessageContent::ToolRequest(request) = &messages[2].content[0] { + let tool_id = if let MessageContent::ToolReq(request) = &messages[2].content[0] { request.id.clone() } else { panic!("should be tool request"); }; - messages - .push(Message::user().with_tool_response(tool_id, Ok(vec![Content::text("Result")]))); + messages.push( + Message::user().with_tool_response(tool_id, Ok(vec![Content::text("Result")]).into()), + ); let spec = format_messages(&messages, &ImageFormat::OpenAi); @@ -592,14 +593,15 @@ mod tests { )]; // Get the ID from the tool request to use in the response - let tool_id = if let MessageContent::ToolRequest(request) = &messages[0].content[0] { + let tool_id = if let MessageContent::ToolReq(request) = &messages[0].content[0] { request.id.clone() } else { panic!("should be tool request"); }; - messages - .push(Message::user().with_tool_response(tool_id, Ok(vec![Content::text("Result")]))); + messages.push( + Message::user().with_tool_response(tool_id, Ok(vec![Content::text("Result")]).into()), + ); let spec = format_messages(&messages, &ImageFormat::OpenAi); @@ -730,7 +732,7 @@ mod tests { let message = response_to_message(response)?; assert_eq!(message.content.len(), 1); - if let MessageContent::ToolRequest(request) = &message.content[0] { + if let MessageContent::ToolReq(request) = &message.content[0] { let tool_call = request.tool_call.as_ref().unwrap(); assert_eq!(tool_call.name, "example_fn"); assert_eq!(tool_call.arguments, json!({"param": "value"})); @@ -749,8 +751,8 @@ mod tests { let message = response_to_message(response)?; - if let MessageContent::ToolRequest(request) = &message.content[0] { - match &request.tool_call { + if let MessageContent::ToolReq(request) = &message.content[0] { + match &request.tool_call.as_result() { Err(ToolError::NotFound(msg)) => { assert!(msg.starts_with("The provided function name")); } @@ -771,8 +773,8 @@ mod tests { let message = response_to_message(response)?; - if let MessageContent::ToolRequest(request) = &message.content[0] { - match &request.tool_call { + if let MessageContent::ToolReq(request) = &message.content[0] { + match &request.tool_call.as_result() { Err(ToolError::InvalidParameters(msg)) => { assert!(msg.starts_with("Could not interpret tool use parameters")); } @@ -793,7 +795,7 @@ mod tests { let message = response_to_message(response)?; - if let MessageContent::ToolRequest(request) = &message.content[0] { + if let MessageContent::ToolReq(request) = &message.content[0] { let tool_call = request.tool_call.as_ref().unwrap(); assert_eq!(tool_call.name, "example_fn"); assert_eq!(tool_call.arguments, json!({})); diff --git a/crates/goose-llm/src/types/completion.rs b/crates/goose-llm/src/types/completion.rs index 039c68d7eb..ce460081d9 100644 --- a/crates/goose-llm/src/types/completion.rs +++ b/crates/goose-llm/src/types/completion.rs @@ -7,36 +7,24 @@ use thiserror::Error; use serde::{Deserialize, Serialize}; +use crate::types::json_value_ffi::JsonValueFfi; use crate::{message::Message, providers::Usage}; use crate::{model::ModelConfig, providers::errors::ProviderError}; -pub struct CompletionRequest<'a> { - pub provider_name: &'a str, +// Lifetimes are not supported in Uniffi, cause other languages don't have them +// https://github.com/mozilla/uniffi-rs/issues/1526#issuecomment-1528851837 +#[derive(uniffi::Record)] +pub struct CompletionRequest { + pub provider_name: String, pub model_config: ModelConfig, - pub system_preamble: &'a str, - pub messages: &'a [Message], - pub extensions: &'a [ExtensionConfig], + pub system_preamble: String, + pub messages: Vec, + pub extensions: Vec, } -impl<'a> CompletionRequest<'a> { - pub fn new( - provider_name: &'a str, - model_config: ModelConfig, - system_preamble: &'a str, - messages: &'a [Message], - extensions: &'a [ExtensionConfig], - ) -> Self { - Self { - provider_name, - model_config, - system_preamble, - messages, - extensions, - } - } -} - -#[derive(Debug, Error)] +// https://mozilla.github.io/uniffi-rs/latest/proc_macro/errors.html +#[derive(Debug, Error, uniffi::Error)] +#[uniffi(flat_error)] pub enum CompletionError { #[error("failed to create provider: {0}")] UnknownProvider(String), @@ -54,7 +42,7 @@ pub enum CompletionError { ToolNotFound(String), } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct CompletionResponse { pub message: Message, pub model: String, @@ -78,35 +66,35 @@ impl CompletionResponse { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, uniffi::Record)] pub struct RuntimeMetrics { - pub total_time_ms: u128, - pub total_time_ms_provider: u128, + pub total_time_sec: f32, + pub total_time_sec_provider: f32, pub tokens_per_second: Option, } impl RuntimeMetrics { pub fn new( - total_time_ms: u128, - total_time_ms_provider: u128, + total_time_sec: f32, + total_time_sec_provider: f32, tokens_per_second: Option, ) -> Self { Self { - total_time_ms, - total_time_ms_provider, + total_time_sec, + total_time_sec_provider, tokens_per_second, } } } -#[derive(Debug, Clone, PartialEq, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Enum)] pub enum ToolApprovalMode { Auto, Manual, Smart, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ToolConfig { pub name: String, pub description: String, @@ -140,7 +128,28 @@ impl ToolConfig { } } -#[derive(Debug, Clone, Serialize)] +#[uniffi::export] +pub fn create_tool_config( + name: &str, + description: &str, + input_schema: JsonValueFfi, + approval_mode: ToolApprovalMode, +) -> ToolConfig { + ToolConfig::new(name, description, input_schema.into(), approval_mode) +} + +uniffi::custom_type!(ToolConfig, String, { + lower: |tc: &ToolConfig| { + serde_json::to_string(&tc).unwrap() + }, + try_lift: |s: String| { + Ok(serde_json::from_str(&s).unwrap()) + }, +}); + +// — Register the newtypes with UniFFI, converting via JSON strings — + +#[derive(Debug, Clone, Serialize, uniffi::Record)] pub struct ExtensionConfig { name: String, instructions: Option, diff --git a/crates/goose-llm/src/types/core.rs b/crates/goose-llm/src/types/core.rs index 2555c6bb5d..3e45d27604 100644 --- a/crates/goose-llm/src/types/core.rs +++ b/crates/goose-llm/src/types/core.rs @@ -4,14 +4,14 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Enum)] #[serde(rename_all = "lowercase")] pub enum Role { User, Assistant, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Enum)] #[serde(tag = "type", rename_all = "camelCase")] pub enum Content { Text(TextContent), @@ -47,13 +47,13 @@ impl Content { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Record)] #[serde(rename_all = "camelCase")] pub struct TextContent { pub text: String, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, uniffi::Record)] #[serde(rename_all = "camelCase")] pub struct ImageContent { pub data: String, @@ -116,7 +116,7 @@ impl ToolCall { } #[non_exhaustive] -#[derive(Error, Debug, Clone, Deserialize, Serialize, PartialEq)] +#[derive(Error, Debug, Clone, Deserialize, Serialize, PartialEq, uniffi::Error)] pub enum ToolError { #[error("Invalid parameters: {0}")] InvalidParameters(String), diff --git a/crates/goose-llm/src/types/json_value_ffi.rs b/crates/goose-llm/src/types/json_value_ffi.rs new file mode 100644 index 0000000000..76a94bd6e9 --- /dev/null +++ b/crates/goose-llm/src/types/json_value_ffi.rs @@ -0,0 +1,84 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +// `serde_json::Value` gets converted to a `String` to pass across the FFI. +// https://github.com/mozilla/uniffi-rs/blob/main/docs/manual/src/types/custom_types.md?plain=1 +// https://github.com/mozilla/uniffi-rs/blob/c7f6caa3d1bf20f934346cefd8e82b5093f0dc6f/examples/custom-types/src/lib.rs#L63-L69 + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct JsonValueFfi(Value); + +impl From for Value { + fn from(val: JsonValueFfi) -> Self { + val.0 + } +} + +impl From for JsonValueFfi { + fn from(val: Value) -> Self { + JsonValueFfi(val) + } +} + +uniffi::custom_type!(JsonValueFfi, String, { + lower: |obj| { + serde_json::to_string(&obj.0).unwrap() + }, + try_lift: |val| { + Ok(serde_json::from_str(&val).unwrap() ) + }, +}); + +// Write some tests to ensure that the conversion works as expected +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_json_value_ffi_conversion() { + let original = JsonValueFfi(json!({"key": "value"})); + let serialized = serde_json::to_string(&original).unwrap(); + let deserialized: JsonValueFfi = serde_json::from_str(&serialized).unwrap(); + + assert_eq!(original.0, deserialized.0); + } + + #[test] + fn test_json_value_ffi_to_serde() { + let original = JsonValueFfi(json!({"key": "value"})); + let value: Value = original.into(); + assert_eq!(value, json!({"key": "value"})); + } + + #[test] + fn test_json_value_ffi_from_serde() { + let value = json!({"key": "value"}); + let original: JsonValueFfi = value.into(); + assert_eq!(original.0, json!({"key": "value"})); + } + + #[test] + fn test_json_value_ffi_lower() { + let original = JsonValueFfi(json!({"key": "value"})); + let serialized = serde_json::to_string(&original).unwrap(); + + assert_eq!(serialized, "{\"key\":\"value\"}"); + } + + #[test] + fn test_json_value_ffi_try_lift() { + let json_str = "{\"key\":\"value\"}"; + let deserialized: JsonValueFfi = serde_json::from_str(json_str).unwrap(); + let expected = JsonValueFfi(json!({"key": "value"})); + assert_eq!(deserialized.0, expected.0); + } + + #[test] + fn test_json_value_ffi_custom_type() { + let json_str = "{\"key\":\"value\"}"; + let deserialized: JsonValueFfi = serde_json::from_str(json_str).unwrap(); + let serialized = serde_json::to_string(&deserialized).unwrap(); + assert_eq!(serialized, json_str); + } +} diff --git a/crates/goose-llm/src/types/mod.rs b/crates/goose-llm/src/types/mod.rs index 5e2ededc6d..a2c2f35c59 100644 --- a/crates/goose-llm/src/types/mod.rs +++ b/crates/goose-llm/src/types/mod.rs @@ -1,2 +1,3 @@ pub mod completion; pub mod core; +pub mod json_value_ffi; diff --git a/crates/goose-llm/tests/extract_tooltip.rs b/crates/goose-llm/tests/extract_tooltip.rs index 8ad1c3c583..488b33d7c4 100644 --- a/crates/goose-llm/tests/extract_tooltip.rs +++ b/crates/goose-llm/tests/extract_tooltip.rs @@ -45,14 +45,14 @@ async fn test_generate_tooltip_with_tools() -> Result<(), ProviderError> { let mut tool_req_msg = Message::assistant(); let req = ToolRequest { id: "1".to_string(), - tool_call: Ok(ToolCall::new("get_time", json!({"timezone": "UTC"}))), + tool_call: Ok(ToolCall::new("get_time", json!({"timezone": "UTC"}))).into(), }; - tool_req_msg.content.push(MessageContent::ToolRequest(req)); + tool_req_msg.content.push(MessageContent::ToolReq(req)); // 2) User message with the tool response let tool_resp_msg = Message::user().with_tool_response( "1", - Ok(vec![Content::text("The current time is 12:00 UTC")]), + Ok(vec![Content::text("The current time is 12:00 UTC")]).into(), ); let messages = vec![tool_req_msg, tool_resp_msg]; diff --git a/crates/goose-llm/tests/providers_complete.rs b/crates/goose-llm/tests/providers_complete.rs index 43212bfdec..74a22064c6 100644 --- a/crates/goose-llm/tests/providers_complete.rs +++ b/crates/goose-llm/tests/providers_complete.rs @@ -147,7 +147,7 @@ impl ProviderTester { .message .content .iter() - .any(|content| matches!(content, MessageContent::ToolRequest(_))), + .any(|content| matches!(content, MessageContent::ToolReq(_))), "Expected tool request in response" ); @@ -171,7 +171,8 @@ impl ProviderTester { Weather Saturday 9:00 PM Clear", - )]), + )]) + .into(), ); // Verify we construct a valid payload including the request/response pair for the next inference diff --git a/crates/goose-llm/uniffi-bindgen.rs b/crates/goose-llm/uniffi-bindgen.rs new file mode 100644 index 0000000000..f6cff6cf1d --- /dev/null +++ b/crates/goose-llm/uniffi-bindgen.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::uniffi_bindgen_main() +}