diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0fd55dcc7..96b659b52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -246,6 +246,16 @@ jobs: cd ui/sdk pnpm run lint + gdk-api-docs-check: + name: Check GDK API Reference is Up-to-Date + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Check generated GDK API reference data + run: python3 documentation/automation/gdk-api/generate.py --check + desktop-lint: name: Test and Lint Electron Desktop App runs-on: macos-latest diff --git a/.github/workflows/docs-update-gdk-api.yml b/.github/workflows/docs-update-gdk-api.yml new file mode 100644 index 000000000..72b7a2d6e --- /dev/null +++ b/.github/workflows/docs-update-gdk-api.yml @@ -0,0 +1,50 @@ +# Regenerates the GDK API reference data from the goose-sdk UniFFI surface and +# opens a PR when it drifts from what is committed. +# +# The generator is deterministic and needs no build or API key, so this stays a +# plain script run rather than an AI-assisted job. + +name: Update GDK API Reference + +on: + workflow_dispatch: + release: + types: [published] + +permissions: + contents: read + +jobs: + update-gdk-api-docs: + name: Update GDK API reference + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Regenerate GDK API reference data + run: python3 documentation/automation/gdk-api/generate.py + + - name: Create pull request + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + branch: docs/auto-gdk-api-reference + delete-branch: true + add-paths: documentation/src/data/gdk-api.json + commit-message: "docs: update GDK API reference data" + title: "docs: update GDK API reference" + body: | + Regenerated `documentation/src/data/gdk-api.json` from + `crates/goose-sdk/src/bindings.rs`. + + - **Triggered by**: ${{ github.event_name }} + - **Release**: ${{ github.event.release.tag_name || 'n/a' }} + + The GDK API reference renders from this file, so merging publishes the + updated Rust, Python, and Kotlin reference docs. + labels: | + documentation + automated diff --git a/.github/workflows/maven-sdk.yml b/.github/workflows/maven-sdk.yml index eb283b484..8f5d6a018 100644 --- a/.github/workflows/maven-sdk.yml +++ b/.github/workflows/maven-sdk.yml @@ -1,4 +1,4 @@ -name: Maven SDK +name: Maven GDK on: workflow_dispatch: diff --git a/.github/workflows/python-sdk-wheels.yml b/.github/workflows/python-sdk-wheels.yml index 0a837e666..c587e4fbe 100644 --- a/.github/workflows/python-sdk-wheels.yml +++ b/.github/workflows/python-sdk-wheels.yml @@ -1,4 +1,4 @@ -name: Python SDK Wheels +name: Python GDK Wheels on: workflow_dispatch: diff --git a/crates/goose-agent/README.md b/crates/goose-agent/README.md new file mode 100644 index 000000000..0a65d743f --- /dev/null +++ b/crates/goose-agent/README.md @@ -0,0 +1,49 @@ +# goose-agent + +The GDK's agent loop, unrolled into a state machine you assemble yourself. + +Instead of a fixed "call the model, run the tools, repeat" loop, an agent here is +an ordered list of steps. Each step gets a chance to look at the conversation and +either decline or produce effects. The machine walks the list, applies the first +step that applies, and starts over from the top — so the whole agent's behavior +is a function of the persisted conversation, not of in-memory loop state. + +## The pieces + +- **`Operation`** — one step. Implement `run` to act on the conversation, + plus any of `inference_tools`, `prompt_parts`, and `moim_parts` to contribute + to the model request. Returns `OperationResult::NotApplicable` to pass, or + `applied(..)` / `yielded(..)` to take the step. Helpers: `not_applicable()`, + `applied()`, `yielded()`, `yielded_with()`. +- **`Inference`** — the step that reaches the provider. Before calling it, + the machine collects tools and prompt parts from *every* operation in the list + into an `InferenceInput`. +- **`StateMachine<'a, S, E>`** — holds `Vec>` and a `CancellationToken`. + `step()` runs one pass, `apply()` writes effects back, `run()` loops until a + step yields to the client or no step applies. +- **`ConversationEffect`** — the default effect type: `AppendMessage`, + `ReplaceConversation`, `PatchToolRequestMeta`, `SetMessageVisibility`. Bring + your own by implementing `MachineEffect`. +- **`Emitter`** — streams `AgentEvent`s (`Message`, `Usage`, `MessageUsage`, + `McpNotification`, `HistoryReplaced`) to the client while a step runs, and + carries the cancellation token. +- **`SessionLoader`** / **`EffectHandler`** / **`MachineSession`** — the traits + your runtime implements so the machine can load a session by id and persist + effects. The machine reloads the session between passes; it never caches it. + +## Reading the conversation + +Because steps re-derive their decisions from history, the crate ships the +predicates they need: `messages_since_kickoff`, `last_effective_role`, +`assistant_turn_count`, `ends_turn`, and `trailing_error`. When a step must +remember that it already did something, it records that on the message itself via +`Operation::set_message_meta` / `message_meta` rather than in memory. + +## Cancellation + +Cancellation is cooperative. Once the token fires, remaining steps are treated as +not-applicable and each step's `cancel` hook gets a chance to rewrite its result; +anything applied while cancelled yields to the client. + +The reference assembly of these pieces is `goose::agents::state_machine` in the +[`goose`](../goose) crate. diff --git a/crates/goose-context-management/README.md b/crates/goose-context-management/README.md new file mode 100644 index 000000000..3d59da147 --- /dev/null +++ b/crates/goose-context-management/README.md @@ -0,0 +1,57 @@ +# goose-context-management + +Conversation compaction: summarizing a message history down to a single message so +a conversation can continue past a model's context window. + +The crate is layered, smallest first — take the layer you need. + +## `summarize` + +Given a model and a slice of messages, produce one summary message. + +```rust +use goose_context_management::{summarize, Templates}; + +let summary = summarize(&model, None, &Templates::default(), &messages).await?; +// summary.message, summary.usage +``` + +## `compact` + +The trait-based API, for callers that own their own conversation representation. +Implement `CompactionInput` to expose messages (and optionally `Templates`), and +`CompactionOutput` to receive the summary and usage: + +```rust +pub trait CompactionInput { + fn messages(&self) -> Vec; + fn templates(&self) -> Templates { Templates::default() } +} + +pub trait CompactionOutput { + fn set_summary(&mut self, summary: Message); + fn set_usage(&mut self, usage: ProviderUsage); +} +``` + +`Vec` already implements `CompactionInput`, so the simple case needs no +wrapper type. + +## Other exports + +- `CompactionModel` — the model abstraction compaction runs against, with + `ProviderModel` adapting any [`goose-providers`](../goose-providers) provider. +- `TokenEstimator` — optional token counting, used to decide how much history to + feed the summarizer. +- `CompactingProvider` — a `Provider` wrapper that compacts as it goes. +- `StructuredSummary` / `FileActivity` — structured summary output, including + which files the conversation touched. +- `Templates` and `format_message_for_compacting` — prompt shaping. +- `DEFAULT_COMPACTION_THRESHOLD` (`0.8`) — the default fraction of the context + window at which callers compact. + +## Cross-language access + +Python and Kotlin reach compaction through [`goose-sdk`](../goose-sdk), which +wraps this crate in its UniFFI bindings. The trait-based `compact` API is Rust +only. diff --git a/crates/goose-local-inference/README.md b/crates/goose-local-inference/README.md new file mode 100644 index 000000000..9de480356 --- /dev/null +++ b/crates/goose-local-inference/README.md @@ -0,0 +1,46 @@ +# goose-local-inference + +On-device model inference for goose. Runs GGUF models through `llama.cpp` (via +`llama-cpp-2`), with an optional MLX backend on Apple silicon. + +Reach it through [`goose-providers`](../goose-providers) with the +`local-inference` feature, which exposes `LocalInferenceProvider` as an ordinary +`Provider`. Depend on this crate directly only when you need model management. + +## Features + +Default is `[]` — CPU inference. + +- `cuda`, `vulkan` — GPU acceleration via the corresponding `llama-cpp-2` backend. +- `mlx` — the MLX backend for Apple silicon. + +## What it handles + +- **Runtime and placement** — `InferenceRuntime` describes the machine; + `available_inference_memory_bytes` and `recommend_local_model` pick a model that + will actually fit. +- **Model lifecycle** — `is_model_loaded`, `loaded_model_ids`, and `evict_model` + manage what's resident. `management`, `local_model_registry`, `hf_models`, and + `paths` cover discovery, on-disk layout, and the Hugging Face catalog; + `huggingface_auth` handles gated repos. Downloads go through + [`goose-download-manager`](../goose-download-manager), re-exported here as + `download_manager`. +- **Prompt formatting** — `prompt_template` applies the model's chat template; + `builtin_chat_template_names()` lists the bundled ones. +- **Tool calling** — `native_tool_parsing` and `tool_parsing` extract tool calls + from model output, and `tool_emulation` (toolshim) fills in for models with no + native tool support. +- **Richer outputs** — `thinking_output` separates reasoning blocks from the + answer; `multimodal` handles image input. +- **Config** — `config_resolver` and `provider_utils` resolve settings such as + `LOCAL_LLM_MODEL`. + +## Building + +The `llama.cpp` backends compile native code, so a C/C++ toolchain is required, +plus the CUDA or Vulkan SDK when selecting those features. + +```bash +cargo build -p goose-local-inference +cargo build -p goose-local-inference --features mlx +``` diff --git a/crates/goose-provider-types/README.md b/crates/goose-provider-types/README.md new file mode 100644 index 000000000..c661b04c2 --- /dev/null +++ b/crates/goose-provider-types/README.md @@ -0,0 +1,59 @@ +# goose-provider-types + +The provider contract and the conversation types that flow through it. This is the +crate to depend on if you want to implement a provider, or to work with goose +messages without pulling in the whole agent. + +Provider implementations live in [`goose-providers`](../goose-providers), which +re-exports every module here. + +## The `Provider` trait + +```rust +#[async_trait] +pub trait Provider: Send + Sync { + fn get_name(&self) -> &str; + + async fn stream( + &self, + model_config: &ModelConfig, + system: &str, + messages: &[Message], + tools: &[Tool], + ) -> Result; +} +``` + +`stream` is the only required method. Everything else has a default: + +- `complete` collects the stream into a single `(Message, ProviderUsage)`. +- `get_context_limit` falls back to the model config's limit. +- `fetch_supported_models` / `fetch_supported_model_info` / `fetch_model_info` + describe the provider's inventory; `fetch_recommended_models` filters it + through the bundled canonical registry (and keeps tool-less models when + toolshim emulation is on). +- `retry_config`, `resume`, and `provider_session_id` cover retries and + provider-side session state. + +A `MessageStream` yields partial text but *complete* tool calls — a text chunk may +be a single word, while a tool call is only emitted once fully assembled. Helpers: +`collect_stream` and `stream_from_single_message`. + +Static metadata is declared separately through `ProviderDescriptor::metadata()`, +returning `ProviderMetadata` with its `ConfigKey`s, models, and any +`ProviderDeprecation`. + +## Modules + +| Module | Contents | +| --- | --- | +| `base` | `Provider`, `ProviderDescriptor`, `ProviderMetadata`, `ModelInfo`, `ConfigKey`, `MessageStream`, `PermissionRouting` | +| `conversation` | `Conversation`, `message`, `token_usage`, `tool_request` — the message model shared across the workspace | +| `model` | `ModelConfig`: model name, context limit, reasoning detection | +| `canonical` | Bundled canonical model registry and provider-model mapping | +| `errors` | `ProviderError` | +| `formats`, `json`, `images`, `mcp_utils` | Wire-format conversion helpers | +| `cache_semantics`, `thinking` | Prompt caching and reasoning/thinking-block handling | +| `retry` | `RetryConfig` | +| `permission`, `goose_mode` | Tool-approval modes | +| `request_log`, `utils` | Request logging and shared helpers | diff --git a/crates/goose-providers/README.md b/crates/goose-providers/README.md new file mode 100644 index 000000000..798452a9e --- /dev/null +++ b/crates/goose-providers/README.md @@ -0,0 +1,54 @@ +# goose-providers + +Provider implementations for goose. The trait they implement and the conversation +types they exchange live in [`goose-provider-types`](../goose-provider-types), +which this crate re-exports — depend on this crate when you want working +providers, and on the types crate when you only need the contract. + +## Native providers + +| Module | Provider | +| --- | --- | +| `anthropic` | Anthropic | +| `openai` | OpenAI | +| `openai_compatible` | Any OpenAI-compatible endpoint | +| `google` | Google Gemini | +| `databricks`, `databricks_v2`, `databricks_auth` | Databricks, including OAuth | +| `azure_foundry` | Azure AI Foundry | +| `snowflake` | Snowflake Cortex | +| `ollama` | Ollama | +| `local_inference` | On-device models (requires `local-inference`) | + +## Declarative providers + +Most OpenAI-compatible services don't need Rust code — they're a JSON file in +`src/declarative/definitions/` (Groq, Mistral, Together, Cerebras, DeepSeek, +Perplexity, LM Studio, Vercel AI Gateway, and ~30 more). Each definition declares +its engine, base URL, env vars, and models. + +`declarative` exposes the same shape at runtime: + +- `deserialize_provider_config` / `from_json` — build a `DeclarativeProviderConfig` + from JSON. +- `load_custom_providers(dir)` — load user-supplied definitions from disk. +- `fixed_provider_configs` — the bundled set. + +```bash +cargo run -p goose-providers --example declarative +cargo run -p goose-providers --example streaming +``` + +## Features + +Default is `[]`. + +- **TLS (pick one):** `rustls-tls` or `native-tls`. +- `local-inference` — pulls in [`goose-local-inference`](../goose-local-inference); + `cuda`, `vulkan`, `mlx` select an accelerator and imply it. + +## Shared plumbing + +`api_client` (HTTP with auth and retries), `http_status` (mapping responses to +`ProviderError`), and the re-exported `retry`, `cache_semantics`, `thinking`, and +`formats` modules are what the provider implementations are built from — start +there when adding a new one. diff --git a/crates/goose-sdk-types/Cargo.toml b/crates/goose-sdk-types/Cargo.toml index ae631c11b..1003ac1bd 100644 --- a/crates/goose-sdk-types/Cargo.toml +++ b/crates/goose-sdk-types/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true authors.workspace = true license.workspace = true repository.workspace = true -description = "Shared types for the Goose SDK" +description = "Shared types for the goose Development Kit (GDK)" [dependencies] agent-client-protocol = { workspace = true } diff --git a/crates/goose-sdk/Cargo.toml b/crates/goose-sdk/Cargo.toml index 52a914e3f..b51ef8db8 100644 --- a/crates/goose-sdk/Cargo.toml +++ b/crates/goose-sdk/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true authors.workspace = true license.workspace = true repository.workspace = true -description = "Rust SDK for Goose with optional uniffi bindings for Python/Kotlin" +description = "The goose Development Kit (GDK) for Rust, with optional uniffi bindings for Python/Kotlin" [lib] name = "goose_sdk" diff --git a/crates/goose-sdk/README.md b/crates/goose-sdk/README.md index 0ec1418c5..af2f02b69 100644 --- a/crates/goose-sdk/README.md +++ b/crates/goose-sdk/README.md @@ -1,7 +1,8 @@ # goose-sdk -The bindings layer for Goose. It houses the shared types used for both ACP and -SDK access, and exposes a cross-language version of the Goose API. +The bindings layer for goose, published as the goose Development Kit (GDK). It +houses the shared types used for both ACP and GDK access, and exposes a +cross-language version of the goose API. With `--features uniffi` the crate compiles to native bindings for Python and Kotlin (namespace `goose` / `io.github.aaif_goose`). The UniFFI surface lets diff --git a/crates/goose-sdk/examples/uniffi/README.md b/crates/goose-sdk/examples/uniffi/README.md index 95c3fee7b..cf8926a23 100644 --- a/crates/goose-sdk/examples/uniffi/README.md +++ b/crates/goose-sdk/examples/uniffi/README.md @@ -1,6 +1,6 @@ # UniFFI examples -These examples exercise the in-process Goose SDK UniFFI bindings from Python and Kotlin. +These examples exercise the in-process GDK UniFFI bindings from Python and Kotlin. ## Prerequisites @@ -55,4 +55,4 @@ just --justfile crates/goose-sdk/justfile kotlin The Kotlin example consumes the local Maven artifact `io.github.aaif-goose:gdk` from `mavenLocal()` and imports the generated package namespace `io.github.aaif_goose`. -On newer JDKs, the example enables native access with `--enable-native-access=ALL-UNNAMED` because the SDK uses JNA to load the bundled native library. +On newer JDKs, the example enables native access with `--enable-native-access=ALL-UNNAMED` because the GDK uses JNA to load the bundled native library. diff --git a/crates/goose-sdk/examples/uniffi/provider.py b/crates/goose-sdk/examples/uniffi/provider.py index 016973555..aa533eaa5 100755 --- a/crates/goose-sdk/examples/uniffi/provider.py +++ b/crates/goose-sdk/examples/uniffi/provider.py @@ -1,5 +1,5 @@ #!/usr/bin/env -S uv run --script -"""Goose SDK demo: build a declarative provider and stream a completion.""" +"""GDK demo: build a declarative provider and stream a completion.""" import asyncio import sys from pathlib import Path diff --git a/crates/goose-sdk/maven/README.md b/crates/goose-sdk/maven/README.md index 80d51d17c..ebfdee4fa 100644 --- a/crates/goose-sdk/maven/README.md +++ b/crates/goose-sdk/maven/README.md @@ -1,4 +1,4 @@ -# Goose SDK Maven package +# GDK Maven package This project packages the UniFFI-generated Kotlin/JVM bindings for `goose-sdk` as the Maven artifact `io.github.aaif-goose:gdk`. diff --git a/crates/goose-sdk/maven/build.gradle.kts b/crates/goose-sdk/maven/build.gradle.kts index ce16ed8f9..cb8e2af9b 100644 --- a/crates/goose-sdk/maven/build.gradle.kts +++ b/crates/goose-sdk/maven/build.gradle.kts @@ -36,7 +36,7 @@ dependencies { tasks.jar { manifest { attributes( - "Implementation-Title" to "Goose SDK", + "Implementation-Title" to "Goose GDK", "Implementation-Version" to project.version, ) } @@ -56,7 +56,7 @@ mavenPublishing { pom { name.set("Goose GDK") - description.set("Kotlin/JVM bindings for the Goose SDK") + description.set("Kotlin/JVM bindings for the goose Development Kit (GDK)") inceptionYear.set("2026") url.set("https://github.com/aaif-goose/goose") licenses { diff --git a/crates/goose-sdk/python/README.md b/crates/goose-sdk/python/README.md index 492d4d4fc..fe967838e 100644 --- a/crates/goose-sdk/python/README.md +++ b/crates/goose-sdk/python/README.md @@ -1,6 +1,6 @@ # goose-sdk -Python bindings for the Goose SDK. +Python bindings for the goose Development Kit (GDK). This package is generated from the Rust `goose-sdk` crate using UniFFI. diff --git a/crates/goose-sdk/python/pyproject.toml b/crates/goose-sdk/python/pyproject.toml index 4b5d44336..2d3037b53 100644 --- a/crates/goose-sdk/python/pyproject.toml +++ b/crates/goose-sdk/python/pyproject.toml @@ -5,12 +5,12 @@ build-backend = "setuptools.build_meta" [project] name = "goose-sdk" version = "0.1.0a6" -description = "Python bindings for the Goose SDK" +description = "Python bindings for the goose Development Kit (GDK)" readme = "README.md" requires-python = ">=3.9" license = "Apache-2.0" authors = [{ name = "AAIF", email = "ai-oss-tools@block.xyz" }] -keywords = ["goose", "sdk", "ai", "agent", "uniffi"] +keywords = ["goose", "gdk", "sdk", "ai", "agent", "uniffi"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", diff --git a/crates/goose-sdk/src/bindings.rs b/crates/goose-sdk/src/bindings.rs index 2b771f1ec..ae530df94 100644 --- a/crates/goose-sdk/src/bindings.rs +++ b/crates/goose-sdk/src/bindings.rs @@ -1,4 +1,4 @@ -//! In-process uniffi bindings for the Goose SDK. +//! In-process uniffi bindings for the GDK. //! //! This is the API surface exposed to Python and Kotlin. It focuses on native //! Goose providers and mirrors the provider message/tool/streaming model closely diff --git a/crates/goose-sdk/src/lib.rs b/crates/goose-sdk/src/lib.rs index 4742ea2de..379caa9b6 100644 --- a/crates/goose-sdk/src/lib.rs +++ b/crates/goose-sdk/src/lib.rs @@ -1,6 +1,6 @@ -//! Goose SDK. +//! The goose Development Kit (GDK). //! -//! With default features this crate re-exports the shared SDK wire types from +//! With default features this crate re-exports the shared GDK wire types from //! `goose-sdk-types` so you can build an Agent Client Protocol (ACP) client //! that talks to `goose acp` over stdio. //! diff --git a/documentation/automation/gdk-api/generate.py b/documentation/automation/gdk-api/generate.py new file mode 100644 index 000000000..67191f1dd --- /dev/null +++ b/documentation/automation/gdk-api/generate.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +"""Generate GDK API reference data from the UniFFI surface in goose-sdk. + +`crates/goose-sdk/src/bindings.rs` is the single source of truth for the Rust, +Python, and Kotlin GDK APIs, so the docs are derived from it instead of being +written by hand. Output is `documentation/src/data/gdk-api.json`, holding one +entry per GDK release series, consumed by the GdkApiReference component. + +Usage: + python3 documentation/automation/gdk-api/generate.py [--check] +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from dataclasses import dataclass, field +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[3] +BINDINGS = REPO_ROOT / "crates/goose-sdk/src/bindings.rs" +CARGO_TOML = REPO_ROOT / "crates/goose-sdk/Cargo.toml" +OUT_FILE = REPO_ROOT / "documentation/src/data/gdk-api.json" + + +def crate_version() -> str: + match = re.search(r'^version\s*=\s*"([^"]+)"', CARGO_TOML.read_text(), re.MULTILINE) + if not match: + sys.exit(f"could not read version from {CARGO_TOML}") + return match.group(1) + + +def doc_version(version: str) -> str: + """Docs are versioned per release series, e.g. 0.1.0-alpha.6 -> 0.1.""" + major, minor = version.split(".")[:2] + return f"{major}.{minor}" + + +@dataclass +class Param: + name: str + type: str + default: str | None = None + docs: str = "" + + +@dataclass +class Func: + name: str + docs: str = "" + params: list[Param] = field(default_factory=list) + returns: str | None = None + throws: str | None = None + is_async: bool = False + + +@dataclass +class Item: + name: str + kind: str + docs: str = "" + fields: list[Param] = field(default_factory=list) + variants: list[dict] = field(default_factory=list) + methods: list[Func] = field(default_factory=list) + + +def split_top_level(text: str, sep: str = ",") -> list[str]: + parts, depth, current = [], 0, "" + for char in text: + if char in "<([{": + depth += 1 + elif char in ">)]}": + depth -= 1 + if char == sep and depth == 0: + parts.append(current) + current = "" + else: + current += char + if current.strip(): + parts.append(current) + return [part.strip() for part in parts if part.strip()] + + +def unwrap(type_text: str, wrapper: str) -> str | None: + match = re.fullmatch(rf"{wrapper}\s*<(.+)>", type_text.strip(), re.DOTALL) + return match.group(1).strip() if match else None + + +def clean_type(type_text: str) -> str: + type_text = re.sub(r"\s+", " ", type_text).strip() + while True: + inner = unwrap(type_text, r"(?:std::sync::)?Arc") or unwrap(type_text, r"Box<\s*dyn") + if inner is None: + inner = unwrap(type_text, "Box") + if inner is None: + break + type_text = re.sub(r"^dyn\s+", "", inner) + return type_text + + +class Scanner: + """Line scanner that pairs doc comments and attributes with the next item.""" + + def __init__(self, source: str) -> None: + self.lines = source.splitlines() + self.index = 0 + self.docs: list[str] = [] + self.attrs: list[str] = [] + + def take_docs(self) -> str: + docs = "\n".join(self.docs).strip() + self.docs = [] + return docs + + def block(self) -> str: + """Consume from the current line through its balanced brace block.""" + text, depth, started = "", 0, False + while self.index < len(self.lines): + line = self.lines[self.index] + self.index += 1 + text += line + "\n" + depth += line.count("{") - line.count("}") + started = started or "{" in line + if started and depth <= 0: + break + if not started and line.rstrip().endswith(";"): + break + return text + + +def parse_fields(block: str) -> list[Param]: + fields: list[Param] = [] + docs: list[str] = [] + default: str | None = None + for line in block.splitlines(): + stripped = line.strip() + if stripped.startswith("///"): + docs.append(stripped[3:].strip()) + continue + match = re.match(r"#\[uniffi\(default\s*=\s*(.+?)\)\]", stripped) + if match: + default = match.group(1).strip() + continue + match = re.match(r"pub\s+([a-z_0-9]+)\s*:\s*(.+?),?$", stripped) + if match: + fields.append( + Param( + match.group(1), + clean_type(match.group(2)), + default, + " ".join(docs).strip(), + ) + ) + docs, default = [], None + return fields + + +def parse_variants(block: str) -> list[dict]: + body = block[block.index("{") + 1 : block.rindex("}")] + variants: list[dict] = [] + for chunk in split_top_level(re.sub(r"#\[[^\]]*\]", "", body)): + chunk = chunk.strip() + match = re.match(r"^([A-Z]\w*)\s*\{(.*)\}$", chunk, re.DOTALL) + if match: + fields = [ + Param(name.strip(), clean_type(type_text)) + for name, _, type_text in ( + part.partition(":") for part in split_top_level(match.group(2)) + ) + if name.strip() and not name.strip().startswith("#") + ] + variants.append({"name": match.group(1), "fields": [vars(f) for f in fields]}) + elif re.fullmatch(r"[A-Z]\w*", chunk): + variants.append({"name": chunk, "fields": []}) + return variants + + +def parse_signature(signature: str, docs: str) -> Func: + signature = re.sub(r"\s+", " ", signature).strip().rstrip("{;").strip() + is_async = " async fn " in f" {signature} " + match = re.search(r"fn\s+(\w+)\s*\((.*)\)\s*(?:->\s*(.+))?$", signature, re.DOTALL) + if not match: + return Func(name=signature, docs=docs) + name, raw_params, raw_return = match.group(1), match.group(2), match.group(3) + + params = [] + for part in split_top_level(raw_params): + if re.fullmatch(r"&?\s*(mut\s+)?self", part): + continue + param_name, _, type_text = part.partition(":") + if type_text: + params.append(Param(param_name.strip(), clean_type(type_text))) + + returns, throws = None, None + if raw_return: + result = clean_type(raw_return) + inner = unwrap(result, "Result") + if inner: + parts = split_top_level(inner) + returns = clean_type(parts[0]) + throws = clean_type(parts[1]) if len(parts) > 1 else "GooseError" + else: + returns = result + if returns in ("()", ""): + returns = None + return Func(name=name, docs=docs, params=params, returns=returns, throws=throws, is_async=is_async) + + +def parse_bindings(source: str) -> dict[str, list[Item] | list[Func]]: + source = source.split("#[cfg(test)]")[0] + scanner = Scanner(source) + items: list[Item] = [] + functions: list[Func] = [] + + while scanner.index < len(scanner.lines): + line = scanner.lines[scanner.index] + stripped = line.strip() + + if stripped.startswith("///"): + scanner.docs.append(stripped[3:].strip()) + scanner.index += 1 + continue + if stripped.startswith("#["): + scanner.attrs.append(stripped) + scanner.index += 1 + continue + if not stripped or stripped.startswith("//"): + scanner.index += 1 + scanner.docs = [] + continue + + attrs = " ".join(scanner.attrs) + scanner.attrs = [] + docs = scanner.take_docs() + exported = "uniffi::export" in attrs + + if "uniffi::Record" in attrs and stripped.startswith("pub struct"): + block = scanner.block() + name = re.search(r"pub struct\s+(\w+)", block).group(1) + items.append(Item(name, "record", docs, fields=parse_fields(block))) + continue + if "uniffi::Object" in attrs and stripped.startswith("pub struct"): + block = scanner.block() + name = re.search(r"pub struct\s+(\w+)", block).group(1) + items.append(Item(name, "object", docs)) + continue + if ("uniffi::Enum" in attrs or "uniffi::Error" in attrs) and stripped.startswith("pub enum"): + block = scanner.block() + name = re.search(r"pub enum\s+(\w+)", block).group(1) + kind = "error" if "uniffi::Error" in attrs else "enum" + items.append(Item(name, kind, docs, variants=parse_variants(block))) + continue + if exported and stripped.startswith("pub trait"): + block = scanner.block() + name = re.search(r"pub trait\s+(\w+)", block).group(1) + methods = [ + parse_signature(match, "") + for match in re.findall(r"fn\s+\w+\s*\([^;]*?\)\s*(?:->[^;]+)?;", block) + ] + items.append(Item(name, "callback", docs, methods=methods)) + continue + if exported and stripped.startswith("impl "): + block = scanner.block() + target = re.search(r"impl\s+(\w+)", block).group(1) + owner = next((item for item in items if item.name == target), None) + if owner: + owner.methods.extend(parse_impl_methods(block)) + continue + if exported and re.match(r"pub\s+(async\s+)?fn", stripped): + block = scanner.block() + functions.append(parse_signature(block.split("{")[0], docs)) + continue + + scanner.index += 1 + + return {"items": items, "functions": functions} + + +def parse_impl_methods(block: str) -> list[Func]: + methods: list[Func] = [] + lines = block.splitlines() + docs: list[str] = [] + index = 0 + while index < len(lines): + stripped = lines[index].strip() + if stripped.startswith("///"): + docs.append(stripped[3:].strip()) + index += 1 + continue + if re.match(r"pub\s+(async\s+)?fn", stripped): + signature, depth = "", 0 + while index < len(lines): + signature += lines[index] + "\n" + depth += lines[index].count("(") - lines[index].count(")") + if depth <= 0 and ("{" in lines[index] or ";" in lines[index]): + break + index += 1 + methods.append(parse_signature(signature.split("{")[0], "\n".join(docs).strip())) + docs = [] + elif stripped and not stripped.startswith("#"): + docs = [] + index += 1 + return methods + + +def build(version: str) -> dict: + parsed = parse_bindings(BINDINGS.read_text()) + items: list[Item] = parsed["items"] + functions: list[Func] = parsed["functions"] + + if not items or not functions: + sys.exit("parsed no API items; the bindings layout likely changed") + + def serialize_func(func: Func) -> dict: + return { + "name": func.name, + "docs": func.docs, + "params": [vars(param) for param in func.params], + "returns": func.returns, + "throws": func.throws, + "isAsync": func.is_async, + } + + def serialize_item(item: Item) -> dict: + return { + "name": item.name, + "kind": item.kind, + "docs": item.docs, + "fields": [vars(field_) for field_ in item.fields], + "variants": item.variants, + "methods": [serialize_func(method) for method in item.methods], + } + + return { + "version": version, + "docVersion": doc_version(version), + "source": "crates/goose-sdk/src/bindings.rs", + "functions": [serialize_func(func) for func in sorted(functions, key=lambda f: f.name)], + "items": [serialize_item(item) for item in items], + } + + +def merge(current: dict) -> dict: + """Upserts the current release series, keeping older series newest-first.""" + existing = json.loads(OUT_FILE.read_text())["versions"] if OUT_FILE.exists() else [] + versions = [ + entry for entry in existing if entry["docVersion"] != current["docVersion"] + ] + [current] + versions.sort( + key=lambda entry: [int(part) for part in entry["docVersion"].split(".")], + reverse=True, + ) + return {"versions": versions} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="fail if output is stale") + args = parser.parse_args() + + version = crate_version() + payload = json.dumps(merge(build(version)), indent=2) + "\n" + relative = OUT_FILE.relative_to(REPO_ROOT) + + if args.check: + if not OUT_FILE.exists() or OUT_FILE.read_text() != payload: + print(f"{relative} is out of date; run {Path(__file__).name}") + return 1 + print(f"{relative} is up to date") + return 0 + + OUT_FILE.parent.mkdir(parents=True, exist_ok=True) + OUT_FILE.write_text(payload) + print(f"wrote {relative} for goose-sdk {version}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/documentation/docs/experimental/_category_.json b/documentation/docs/experimental/_category_.json index a2a2a930f..9877735ea 100644 --- a/documentation/docs/experimental/_category_.json +++ b/documentation/docs/experimental/_category_.json @@ -1,6 +1,6 @@ { "label": "Experimental", - "position": 7, + "position": 8, "link": { "type": "doc", "id": "experimental/index" diff --git a/documentation/docs/gdk/_category_.json b/documentation/docs/gdk/_category_.json new file mode 100644 index 000000000..8a9ac50c5 --- /dev/null +++ b/documentation/docs/gdk/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "GDK", + "position": 3, + "link": { + "type": "doc", + "id": "gdk/index" + } +} diff --git a/documentation/docs/gdk/api-reference.mdx b/documentation/docs/gdk/api-reference.mdx new file mode 100644 index 000000000..9437ac341 --- /dev/null +++ b/documentation/docs/gdk/api-reference.mdx @@ -0,0 +1,18 @@ +--- +sidebar_position: 2 +title: API Reference +sidebar_label: API Reference +description: Complete goose GDK API reference for Rust, Python, and Kotlin. +--- + +import GdkApiReference from '@site/src/components/GdkApiReference'; + +# API Reference + +The complete GDK surface. Use the toggles to switch language and GDK +version — names and types are shown using each language's own conventions. + +New to the GDK? Start with the [GDK overview](/docs/gdk) for installation and +runnable examples. + + diff --git a/documentation/docs/gdk/index.md b/documentation/docs/gdk/index.md new file mode 100644 index 000000000..decae1647 --- /dev/null +++ b/documentation/docs/gdk/index.md @@ -0,0 +1,239 @@ +--- +sidebar_position: 1 +title: GDK Overview +sidebar_label: Overview +description: Build with goose providers in Rust, Python, and Kotlin. +--- + +# GDK + +The goose Development Kit (GDK) exposes goose's provider layer as a library so you +can call models, stream completions, and compact conversations from your own +application. + +One Rust crate, `goose-sdk`, is the source of every language binding. Python and +Kotlin are generated from it with [UniFFI](https://github.com/mozilla/uniffi-rs), +so all three languages share the same types, behavior, and version number. + +See the [API Reference](/docs/gdk/api-reference) for the complete surface in your +language of choice. + +:::info Alpha +The GDK is in alpha. The surface may change between `0.x` releases. Pin an exact +version and check the API reference version selector when upgrading. +::: + +## What you can do + +- Construct providers for OpenAI, Anthropic, Groq, Databricks, or any + [declarative provider](#declarative-providers) defined in JSON +- Stream a completion chunk by chunk, including tool calls and reasoning output +- Request a single non-streaming completion +- Compact a long conversation into a summary so it can continue past the + model's context window +- Capture provider request logs as JSONL + +## Install + + + +### Rust + +```bash +cargo add goose-sdk +``` + +By default the crate re-exports the Agent Client Protocol (ACP) wire types for +talking to `goose acp` over stdio. Enable the `uniffi` feature for the +in-process provider API documented in the reference: + +```bash +cargo add goose-sdk --features uniffi +``` + +### Python + +```bash +pip install goose-sdk +``` + +The package installs as `goose-sdk` and imports as `goose`. Wheels bundle the +native library, so there is nothing else to build. Requires Python 3.9+. + +```python +import goose +``` + +### Kotlin / JVM + +```kotlin +dependencies { + implementation("io.github.aaif-goose:gdk:") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") +} +``` + +The artifact version matches the Rust crate version. Classes live in the +`io.github.aaif_goose` package. The jar bundles native libraries for +macOS (arm64, x86-64), Linux (arm64, x86-64), and Windows (x86-64). + +On JDK 24+, add `--enable-native-access=ALL-UNNAMED` because the GDK loads its +native library through JNA. + + + +## Quickstart + +Each example builds a provider, sends one message, and prints the streamed +response. + +### Python + +```python +import asyncio +from goose import ( + MessageContent, + MessageRole, + ProviderMessage, + ProviderModelConfig, + StreamChunk, + openai_default_model, + openai_provider, +) + + +async def main() -> None: + provider = openai_provider(api_key="...") + model = ProviderModelConfig(model_name=openai_default_model()) + messages = [ + ProviderMessage( + role=MessageRole.USER, + content=[MessageContent.Text(text="What is the capital of France?")], + ) + ] + + stream = await provider.stream(model, "You are a geography expert.", messages, []) + while chunk := await stream.next_chunk(): + if isinstance(chunk, StreamChunk.TextChunk): + print(chunk.text, end="") + + +asyncio.run(main()) +``` + +### Kotlin + +```kotlin +import io.github.aaif_goose.MessageContent +import io.github.aaif_goose.MessageRole +import io.github.aaif_goose.ProviderMessage +import io.github.aaif_goose.ProviderModelConfig +import io.github.aaif_goose.StreamChunk +import io.github.aaif_goose.streamFlow +import io.github.aaif_goose.providers.openai.defaultModel +import io.github.aaif_goose.providers.openai.provider as openAiProvider +import kotlinx.coroutines.runBlocking + +fun main() = runBlocking { + val provider = openAiProvider(System.getenv("OPENAI_API_KEY")) + val model = ProviderModelConfig(modelName = defaultModel()) + val messages = listOf( + ProviderMessage( + role = MessageRole.USER, + content = listOf(MessageContent.Text(text = "What is the capital of France?")), + ), + ) + + provider.streamFlow(model, "You are a geography expert.", messages) + .collect { chunk -> + if (chunk is StreamChunk.TextChunk) print(chunk.text) + } +} +``` + +### Rust + +```rust +use goose_sdk::bindings::{ + openai_default_model, openai_provider, MessageContent, MessageRole, ProviderMessage, + ProviderModelConfig, StreamChunk, +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let provider = openai_provider(std::env::var("OPENAI_API_KEY")?)?; + let model = ProviderModelConfig { + model_name: openai_default_model(), + ..Default::default() + }; + let messages = vec![ProviderMessage { + role: MessageRole::User, + content: vec![MessageContent::Text { + text: "What is the capital of France?".to_string(), + }], + }]; + + let stream = provider + .stream(model, "You are a geography expert.".to_string(), messages, vec![]) + .await?; + + while let Some(chunk) = stream.next_chunk().await? { + if let StreamChunk::TextChunk { text } = chunk { + print!("{text}"); + } + } + Ok(()) +} +``` + +## Kotlin idioms + +The Kotlin package adds a few conveniences on top of the generated bindings: + +| Kotlin API | Equivalent generated call | +| --- | --- | +| `provider.streamFlow(model, system, messages, tools)` | `stream(...)` plus a `nextChunk()` loop, as a `Flow` | +| `providers.openai.provider(apiKey)` | `openaiProvider(apiKey)` | +| `providers.openai.defaultModel()` | `openaiDefaultModel()` | +| `providers.anthropic.provider(apiKey, baseUrl, betaHeaders)` | `anthropicProvider(...)` | +| `providers.groq.provider(apiKey)` | `groqProvider(apiKey)` | +| `providers.databricks.provider(host, token)` | `databricksProvider(host, token)` | + +`tools` defaults to an empty list in the Kotlin helpers, and suspending +functions map to Kotlin coroutines. Errors surface as `GooseException` +subclasses. + +## Declarative providers + +Any provider that speaks an OpenAI- or Anthropic-compatible API can be defined +in JSON and loaded without new Rust code: + +```python +provider = goose.declarative_provider_from_json(open("deepseek.json").read()) +``` + +Environment variable placeholders such as `${DEEPSEEK_API_KEY}` in the JSON are +resolved when the provider is constructed. + +## Streaming model + +`stream()` returns a `ProviderStream`. Call `next_chunk()` until it returns +`None` to consume the response: + +| Chunk | Meaning | +| --- | --- | +| `TextChunk` | Assistant text | +| `ToolChunk` | A tool call request with JSON arguments | +| `ThinkingChunk` / `RedactedThinkingChunk` | Reasoning output | +| `EndChunk` | Stream finished, carries final token `Usage` | +| `ErrorChunk` | Mid-stream failure, carries a `GooseStreamError` | + +Errors raised before the stream starts are thrown as `GooseError` +(`GooseException` in Kotlin). Errors that occur mid-stream arrive as an +`ErrorChunk` instead. + +## Next steps + +- [API Reference](/docs/gdk/api-reference) — every function, type, and error +- [goose in ACP clients](/docs/guides/acp-clients) — drive the full goose agent + over the Agent Client Protocol diff --git a/documentation/docs/goose-architecture/_category_.json b/documentation/docs/goose-architecture/_category_.json index 95fa3a866..8214789d1 100644 --- a/documentation/docs/goose-architecture/_category_.json +++ b/documentation/docs/goose-architecture/_category_.json @@ -1,6 +1,6 @@ { "label": "Architecture Overview", - "position": 6, + "position": 7, "link": { "type": "generated-index", "description": "Extend goose functionalities with extensions and custom configurations" diff --git a/documentation/docs/guides/_category_.json b/documentation/docs/guides/_category_.json index 2eb549a15..63809d877 100644 --- a/documentation/docs/guides/_category_.json +++ b/documentation/docs/guides/_category_.json @@ -1,6 +1,6 @@ { "label": "Guides", - "position": 3, + "position": 4, "link": { "type": "generated-index", "description": "Learn essential tips and recommendations for using goose" diff --git a/documentation/docs/mcp/_category_.json b/documentation/docs/mcp/_category_.json index a574b31e7..abd8d503c 100644 --- a/documentation/docs/mcp/_category_.json +++ b/documentation/docs/mcp/_category_.json @@ -1,6 +1,6 @@ { "label": "MCP Servers", - "position": 5, + "position": 6, "link": { "type": "generated-index", "description": "How to integrate and use MCP servers as goose extensions" diff --git a/documentation/docs/tutorials/_category_.json b/documentation/docs/tutorials/_category_.json index afb6bcb28..75d5833c2 100644 --- a/documentation/docs/tutorials/_category_.json +++ b/documentation/docs/tutorials/_category_.json @@ -1,6 +1,6 @@ { "label": "Tutorials", - "position": 4, + "position": 5, "link": { "type": "generated-index", "description": "How to use goose in various ways" diff --git a/documentation/src/components/GdkApiReference/index.tsx b/documentation/src/components/GdkApiReference/index.tsx new file mode 100644 index 000000000..dcadf618b --- /dev/null +++ b/documentation/src/components/GdkApiReference/index.tsx @@ -0,0 +1,292 @@ +import React, { useMemo, useState } from "react"; +import CodeBlock from "@theme/CodeBlock"; +import apiData from "@site/src/data/gdk-api.json"; +import { LANGUAGES, Language, LanguageId } from "./languages"; +import styles from "./styles.module.css"; + +type GdkParam = { + name: string; + type: string; + default: string | null; + docs: string; +}; + +type GdkFunc = { + name: string; + docs: string; + params: GdkParam[]; + returns: string | null; + throws: string | null; + isAsync: boolean; +}; + +type GdkItem = { + name: string; + kind: "object" | "callback" | "record" | "enum" | "error"; + docs: string; + fields: GdkParam[]; + variants: { name: string; fields: GdkParam[] }[]; + methods: GdkFunc[]; +}; + +type GdkApiDoc = { + version: string; + docVersion: string; + source: string; + functions: GdkFunc[]; + items: GdkItem[]; +}; + +const VERSIONS = (apiData as { versions: GdkApiDoc[] }).versions; + +const KIND_LABELS: Record = { + object: "Class", + callback: "Interface", + record: "Data type", + enum: "Enum", + error: "Error", +}; + +const KIND_HEADINGS: Record = { + object: "Classes", + callback: "Interfaces", + record: "Data types", + enum: "Enums", + error: "Errors", +}; + +const slug = (...parts: string[]) => + parts.join("-").replace(/[^a-zA-Z0-9]+/g, "-").toLowerCase(); + +function signature(func: GdkFunc, language: Language, owner?: string): string { + const params = func.params + .map((param) => { + const type = language.type(param.type); + const suffix = param.default ? ` = ${language.default(param.default)}` : ""; + switch (language.id) { + case "rust": + return `${param.name}: ${type}${suffix}`; + case "python": + return `${language.field(param.name)}: ${type}${suffix}`; + default: + return `${language.field(param.name)}: ${type}${suffix}`; + } + }) + .join(", "); + + const name = language.func(func.name); + const returns = func.returns ? language.type(func.returns) : null; + const prefix = owner ? `${owner}.` : ""; + + if (language.id === "rust") { + const asyncKeyword = func.isAsync ? "async " : ""; + const result = func.throws + ? `Result<${returns ?? "()"}, ${func.throws}>` + : returns; + return `${asyncKeyword}fn ${prefix}${name}(${params})${result ? ` -> ${result}` : ""}`; + } + + if (language.id === "python") { + const asyncKeyword = func.isAsync ? "async " : ""; + return `${asyncKeyword}def ${prefix}${name}(${params})${returns ? ` -> ${returns}` : ""}`; + } + + const suspend = func.isAsync ? "suspend " : ""; + const throwsAnnotation = func.throws + ? `@Throws(${language.errorType(func.throws)}::class)\n` + : ""; + return `${throwsAnnotation}${suspend}fun ${prefix}${name}(${params})${returns ? `: ${returns}` : ""}`; +} + +function ParamTable({ + rows, + language, + caption, +}: { + rows: GdkParam[]; + language: Language; + caption: string; +}) { + if (rows.length === 0) return null; + const hasDefaults = rows.some((row) => row.default); + return ( + + + + + + {hasDefaults && } + + + + + {rows.map((row) => ( + + + + {hasDefaults && ( + + )} + + + ))} + +
{caption}TypeDefaultDescription
+ {language.field(row.name)} + + {language.type(row.type)} + {row.default ? {language.default(row.default)} : "—"}{row.docs || "—"}
+ ); +} + +function FuncEntry({ + func, + language, + owner, +}: { + func: GdkFunc; + language: Language; + owner?: string; +}) { + return ( +
+

+ {language.func(func.name)} +

+ {func.docs &&

{func.docs}

} + {signature(func, language, owner)} + + {func.throws && ( +

+ Raises {language.errorType(func.throws)} +

+ )} +
+ ); +} + +function ItemEntry({ item, language }: { item: GdkItem; language: Language }) { + const dataCarrying = item.variants.some((variant) => variant.fields.length > 0); + return ( +
+

+ {item.kind === "error" ? language.errorType(item.name) : item.name} + {KIND_LABELS[item.kind]} +

+ {item.docs &&

{item.docs}

} + + + + {item.variants.length > 0 && ( + + + + + + + + + {item.variants.map((variant) => ( + + + + + ))} + +
{item.kind === "error" ? "Variant" : "Case"}Associated data
+ + {item.kind === "error" && language.id === "kotlin" + ? `${language.errorType(item.name)}.${variant.name}` + : language.variant(variant.name, dataCarrying)} + + + {variant.fields.length === 0 + ? "—" + : variant.fields.map((field) => ( +
+ + {language.field(field.name)}: {language.type(field.type)} + +
+ ))} +
+ )} + + {item.methods.map((method) => ( + + ))} +
+ ); +} + +export default function GdkApiReference() { + const [languageId, setLanguageId] = useState("rust"); + const [docVersion, setDocVersion] = useState(VERSIONS[0].docVersion); + + const language = LANGUAGES.find((entry) => entry.id === languageId)!; + const doc = useMemo( + () => VERSIONS.find((entry) => entry.docVersion === docVersion) ?? VERSIONS[0], + [docVersion], + ); + + const grouped = useMemo(() => { + const order: GdkItem["kind"][] = ["object", "callback", "record", "enum", "error"]; + return order + .map((kind) => ({ kind, items: doc.items.filter((item) => item.kind === kind) })) + .filter((group) => group.items.length > 0); + }, [doc]); + + return ( +
+
+
+ {LANGUAGES.map((entry) => ( + + ))} +
+ + +
+ +

+ Generated from {doc.source} at goose-sdk {doc.version}. +

+ +

Functions

+ {doc.functions.map((func) => ( + + ))} + + {grouped.map((group) => ( + +

{KIND_HEADINGS[group.kind]}

+ {group.items.map((item) => ( + + ))} +
+ ))} +
+ ); +} diff --git a/documentation/src/components/GdkApiReference/languages.ts b/documentation/src/components/GdkApiReference/languages.ts new file mode 100644 index 000000000..a1b59fefa --- /dev/null +++ b/documentation/src/components/GdkApiReference/languages.ts @@ -0,0 +1,161 @@ +// Renders the Rust API surface in each target language's idioms. The rules +// mirror the uniffi 0.32 code generators, which are the actual source of the +// Python and Kotlin bindings. + +export type LanguageId = "rust" | "python" | "kotlin"; + +const toSnake = (name: string) => name; +const toCamel = (name: string) => + name.replace(/_([a-z0-9])/g, (_, char: string) => char.toUpperCase()); +const toShoutySnake = (name: string) => + name + .replace(/([a-z0-9])([A-Z])/g, "$1_$2") + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") + .toUpperCase(); + +type Scalars = Record; + +const PYTHON_SCALARS: Scalars = { + String: "str", + bool: "bool", + i8: "int", + i16: "int", + i32: "int", + i64: "int", + u8: "int", + u16: "int", + u32: "int", + u64: "int", + f32: "float", + f64: "float", + "()": "None", +}; + +const KOTLIN_SCALARS: Scalars = { + String: "String", + bool: "Boolean", + i8: "Byte", + i16: "Short", + i32: "Int", + i64: "Long", + u8: "UByte", + u16: "UShort", + u32: "UInt", + u64: "ULong", + f32: "Float", + f64: "Double", + "()": "Unit", +}; + +const generic = (type: string, name: string): string[] | null => { + const match = new RegExp(`^${name}\\s*<(.+)>$`, "s").exec(type.trim()); + if (!match) return null; + const args: string[] = []; + let depth = 0; + let current = ""; + for (const char of match[1]) { + if (char === "<") depth += 1; + if (char === ">") depth -= 1; + if (char === "," && depth === 0) { + args.push(current.trim()); + current = ""; + } else { + current += char; + } + } + if (current.trim()) args.push(current.trim()); + return args; +}; + +const mapType = (type: string, language: LanguageId): string => { + const trimmed = type.trim(); + if (language === "rust") return trimmed; + + const scalars = language === "python" ? PYTHON_SCALARS : KOTLIN_SCALARS; + if (scalars[trimmed]) return scalars[trimmed]; + + const option = generic(trimmed, "Option"); + if (option) { + const inner = mapType(option[0], language); + return language === "python" ? `${inner} | None` : `${inner}?`; + } + + const bytes = generic(trimmed, "Vec"); + if (bytes && bytes[0].trim() === "u8") { + return language === "python" ? "bytes" : "ByteArray"; + } + if (bytes) { + const inner = mapType(bytes[0], language); + return language === "python" ? `list[${inner}]` : `List<${inner}>`; + } + + const map = generic(trimmed, "HashMap"); + if (map) { + const [key, value] = map.map((arg) => mapType(arg, language)); + return language === "python" ? `dict[${key}, ${value}]` : `Map<${key}, ${value}>`; + } + + return trimmed; +}; + +const mapDefault = (value: string, language: LanguageId): string => { + if (language === "rust") return value; + if (value === "None") return language === "python" ? "None" : "null"; + if (value === "true" || value === "false") { + return language === "python" ? (value === "true" ? "True" : "False") : value; + } + return value; +}; + +export type Language = { + id: LanguageId; + label: string; + /** Prism language for syntax highlighting. */ + prism: string; + func: (name: string) => string; + field: (name: string) => string; + variant: (name: string, isDataCarrying: boolean) => string; + type: (type: string) => string; + default: (value: string) => string; + errorType: (name: string) => string; +}; + +export const LANGUAGES: Language[] = [ + { + id: "rust", + label: "Rust", + prism: "rust", + func: toSnake, + field: toSnake, + variant: (name) => name, + type: (type) => mapType(type, "rust"), + default: (value) => mapDefault(value, "rust"), + errorType: (name) => name, + }, + { + id: "python", + label: "Python", + prism: "python", + func: toSnake, + field: toSnake, + // Flat enums become `enum.Enum` members; data-carrying variants become + // nested dataclasses that keep their Rust casing. + variant: (name, isDataCarrying) => (isDataCarrying ? name : toShoutySnake(name)), + type: (type) => mapType(type, "python"), + default: (value) => mapDefault(value, "python"), + errorType: (name) => name, + }, + { + id: "kotlin", + label: "Kotlin", + prism: "kotlin", + func: toCamel, + field: toCamel, + // Flat enums become `enum class` entries; data-carrying variants become + // `sealed class` subclasses that keep their Rust casing. + variant: (name, isDataCarrying) => (isDataCarrying ? name : toShoutySnake(name)), + type: (type) => mapType(type, "kotlin"), + default: (value) => mapDefault(value, "kotlin"), + errorType: (name) => name.replace(/Error$/, "Exception"), + }, +]; diff --git a/documentation/src/components/GdkApiReference/styles.module.css b/documentation/src/components/GdkApiReference/styles.module.css new file mode 100644 index 000000000..675c0640d --- /dev/null +++ b/documentation/src/components/GdkApiReference/styles.module.css @@ -0,0 +1,103 @@ +.toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} + +.tabs { + display: flex; + gap: 0.25rem; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: var(--ifm-global-radius); + padding: 0.25rem; +} + +.tab, +.tabActive { + border: 0; + border-radius: var(--ifm-global-radius); + padding: 0.35rem 0.9rem; + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + background: transparent; + color: var(--ifm-color-emphasis-700); +} + +.tab:hover { + background: var(--ifm-color-emphasis-200); +} + +.tabActive { + background: var(--ifm-color-primary); + color: var(--ifm-color-primary-contrast-background); +} + +.version { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9rem; + font-weight: 600; +} + +.version select { + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: var(--ifm-global-radius); + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); + padding: 0.35rem 0.5rem; + font: inherit; +} + +.meta { + font-size: 0.9rem; + color: var(--ifm-color-emphasis-700); +} + +.item { + margin-bottom: 2.5rem; + padding-top: 0.5rem; + border-top: 1px solid var(--ifm-color-emphasis-200); +} + +.itemTitle { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.badge { + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + padding: 0.15rem 0.5rem; + border-radius: 999px; + background: var(--ifm-color-emphasis-200); + color: var(--ifm-color-emphasis-800); +} + +.entry { + margin: 1.25rem 0 1.75rem; +} + +.entryTitle { + margin-bottom: 0.5rem; +} + +.table { + display: table; + width: 100%; + margin-bottom: 1rem; + font-size: 0.9rem; +} + +.table th, +.table td { + vertical-align: top; +} diff --git a/documentation/src/data/gdk-api.json b/documentation/src/data/gdk-api.json new file mode 100644 index 000000000..7e05a287c --- /dev/null +++ b/documentation/src/data/gdk-api.json @@ -0,0 +1,1055 @@ +{ + "versions": [ + { + "version": "0.1.0-alpha.6", + "docVersion": "0.1", + "source": "crates/goose-sdk/src/bindings.rs", + "functions": [ + { + "name": "anthropic_default_model", + "docs": "", + "params": [], + "returns": "String", + "throws": null, + "isAsync": false + }, + { + "name": "anthropic_provider", + "docs": "", + "params": [ + { + "name": "api_key", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "base_url", + "type": "Option", + "default": null, + "docs": "" + }, + { + "name": "beta_headers", + "type": "Vec", + "default": null, + "docs": "" + } + ], + "returns": "Provider", + "throws": "GooseError", + "isAsync": false + }, + { + "name": "databricks_default_model", + "docs": "", + "params": [], + "returns": "String", + "throws": null, + "isAsync": false + }, + { + "name": "databricks_provider", + "docs": "", + "params": [ + { + "name": "host", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "token", + "type": "String", + "default": null, + "docs": "" + } + ], + "returns": "Provider", + "throws": "GooseError", + "isAsync": false + }, + { + "name": "databricks_v2_default_model", + "docs": "", + "params": [], + "returns": "String", + "throws": null, + "isAsync": false + }, + { + "name": "databricks_v2_provider", + "docs": "", + "params": [ + { + "name": "host", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "token", + "type": "String", + "default": null, + "docs": "" + } + ], + "returns": "Provider", + "throws": "GooseError", + "isAsync": false + }, + { + "name": "declarative_provider_from_json", + "docs": "", + "params": [ + { + "name": "json", + "type": "String", + "default": null, + "docs": "" + } + ], + "returns": "Provider", + "throws": "GooseError", + "isAsync": false + }, + { + "name": "default_compaction_templates", + "docs": "", + "params": [], + "returns": "CompactionTemplates", + "throws": null, + "isAsync": false + }, + { + "name": "groq_default_model", + "docs": "", + "params": [], + "returns": "String", + "throws": null, + "isAsync": false + }, + { + "name": "groq_provider", + "docs": "", + "params": [ + { + "name": "api_key", + "type": "String", + "default": null, + "docs": "" + } + ], + "returns": "Provider", + "throws": "GooseError", + "isAsync": false + }, + { + "name": "install_request_logger", + "docs": "Installs the process-wide provider request logger.\n\nA logger can only be installed once for the lifetime of the process.", + "params": [ + { + "name": "logger", + "type": "RequestLogger", + "default": null, + "docs": "" + } + ], + "returns": null, + "throws": "GooseError", + "isAsync": false + }, + { + "name": "openai_default_model", + "docs": "", + "params": [], + "returns": "String", + "throws": null, + "isAsync": false + }, + { + "name": "openai_provider", + "docs": "", + "params": [ + { + "name": "api_key", + "type": "String", + "default": null, + "docs": "" + } + ], + "returns": "Provider", + "throws": "GooseError", + "isAsync": false + } + ], + "items": [ + { + "name": "GooseError", + "kind": "error", + "docs": "", + "fields": [], + "variants": [ + { + "name": "RateLimited", + "fields": [ + { + "name": "retry_after_ms", + "type": "Option", + "default": null, + "docs": "" + }, + { + "name": "retry_after_suffix", + "type": "String", + "default": null, + "docs": "" + } + ] + }, + { + "name": "OutputTokenLimitExceeded", + "fields": [ + { + "name": "details", + "type": "String", + "default": null, + "docs": "" + } + ] + }, + { + "name": "ContextLengthExceeded", + "fields": [ + { + "name": "details", + "type": "String", + "default": null, + "docs": "" + } + ] + }, + { + "name": "Authentication", + "fields": [ + { + "name": "details", + "type": "String", + "default": null, + "docs": "" + } + ] + }, + { + "name": "Timeout", + "fields": [ + { + "name": "details", + "type": "String", + "default": null, + "docs": "" + } + ] + }, + { + "name": "ProviderUnavailable", + "fields": [ + { + "name": "details", + "type": "String", + "default": null, + "docs": "" + } + ] + }, + { + "name": "Generic", + "fields": [ + { + "name": "details", + "type": "String", + "default": null, + "docs": "" + } + ] + } + ], + "methods": [] + }, + { + "name": "RequestLogger", + "kind": "callback", + "docs": "Receives provider request logs as JSONL records.\n\n`start` returns an identifier that is passed to `write` for every record in\nthat request, allowing callers to keep concurrent request logs separate.", + "fields": [], + "variants": [], + "methods": [ + { + "name": "start", + "docs": "", + "params": [], + "returns": "u64", + "throws": "GooseError", + "isAsync": false + }, + { + "name": "write", + "docs": "", + "params": [ + { + "name": "request_id", + "type": "u64", + "default": null, + "docs": "" + }, + { + "name": "record", + "type": "String", + "default": null, + "docs": "" + } + ], + "returns": null, + "throws": "GooseError", + "isAsync": false + } + ] + }, + { + "name": "ProviderMessage", + "kind": "record", + "docs": "A text message passed to a provider.", + "fields": [ + { + "name": "role", + "type": "MessageRole", + "default": null, + "docs": "" + }, + { + "name": "content", + "type": "Vec", + "default": null, + "docs": "" + } + ], + "variants": [], + "methods": [] + }, + { + "name": "MessageRole", + "kind": "enum", + "docs": "", + "fields": [], + "variants": [ + { + "name": "User", + "fields": [] + }, + { + "name": "Assistant", + "fields": [] + }, + { + "name": "Tool", + "fields": [] + } + ], + "methods": [] + }, + { + "name": "MessageContent", + "kind": "enum", + "docs": "", + "fields": [], + "variants": [ + { + "name": "Text", + "fields": [ + { + "name": "text", + "type": "String", + "default": null, + "docs": "" + } + ] + }, + { + "name": "Image", + "fields": [ + { + "name": "mime_type", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "data", + "type": "Vec", + "default": null, + "docs": "" + } + ] + }, + { + "name": "ToolRequest", + "fields": [ + { + "name": "id", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "name", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "arguments_json", + "type": "String", + "default": null, + "docs": "" + } + ] + }, + { + "name": "ToolResult", + "fields": [ + { + "name": "id", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "success", + "type": "bool", + "default": null, + "docs": "" + }, + { + "name": "content_json", + "type": "String", + "default": null, + "docs": "" + } + ] + }, + { + "name": "Thinking", + "fields": [ + { + "name": "thinking", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "signature", + "type": "String", + "default": null, + "docs": "" + } + ] + }, + { + "name": "RedactedThinking", + "fields": [ + { + "name": "data", + "type": "String", + "default": null, + "docs": "" + } + ] + } + ], + "methods": [] + }, + { + "name": "ProviderTool", + "kind": "record", + "docs": "", + "fields": [ + { + "name": "name", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "description", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "input_schema_json", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "annotations_json", + "type": "Option", + "default": "None", + "docs": "" + } + ], + "variants": [], + "methods": [] + }, + { + "name": "ProviderModelConfig", + "kind": "record", + "docs": "", + "fields": [ + { + "name": "model_name", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "context_limit", + "type": "Option", + "default": "None", + "docs": "" + }, + { + "name": "temperature", + "type": "Option", + "default": "None", + "docs": "" + }, + { + "name": "max_tokens", + "type": "Option", + "default": "None", + "docs": "" + }, + { + "name": "toolshim", + "type": "bool", + "default": "false", + "docs": "" + }, + { + "name": "toolshim_model", + "type": "Option", + "default": "None", + "docs": "" + }, + { + "name": "request_params_json", + "type": "Option", + "default": "None", + "docs": "" + }, + { + "name": "provider_params_json", + "type": "Option", + "default": "None", + "docs": "" + }, + { + "name": "reasoning", + "type": "Option", + "default": "None", + "docs": "" + }, + { + "name": "timeout_ms", + "type": "Option", + "default": "None", + "docs": "" + }, + { + "name": "request_headers", + "type": "Option>", + "default": "None", + "docs": "Per-request HTTP headers attached to the outgoing provider call. These override any static headers configured on the provider." + } + ], + "variants": [], + "methods": [] + }, + { + "name": "Usage", + "kind": "record", + "docs": "", + "fields": [ + { + "name": "input_tokens", + "type": "Option", + "default": null, + "docs": "" + }, + { + "name": "output_tokens", + "type": "Option", + "default": null, + "docs": "" + }, + { + "name": "total_tokens", + "type": "Option", + "default": null, + "docs": "" + }, + { + "name": "cache_read_input_tokens", + "type": "Option", + "default": null, + "docs": "" + }, + { + "name": "cache_creation_input_tokens", + "type": "Option", + "default": null, + "docs": "" + }, + { + "name": "reasoning_tokens", + "type": "Option", + "default": null, + "docs": "" + }, + { + "name": "model", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "provider_metadata_json", + "type": "Option", + "default": null, + "docs": "" + } + ], + "variants": [], + "methods": [] + }, + { + "name": "StreamChunk", + "kind": "enum", + "docs": "", + "fields": [], + "variants": [ + { + "name": "TextChunk", + "fields": [ + { + "name": "text", + "type": "String", + "default": null, + "docs": "" + } + ] + }, + { + "name": "ToolChunk", + "fields": [ + { + "name": "id", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "name", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "arguments_json", + "type": "String", + "default": null, + "docs": "" + } + ] + }, + { + "name": "ThinkingChunk", + "fields": [ + { + "name": "thinking", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "signature", + "type": "String", + "default": null, + "docs": "" + } + ] + }, + { + "name": "RedactedThinkingChunk", + "fields": [ + { + "name": "data", + "type": "String", + "default": null, + "docs": "" + } + ] + }, + { + "name": "EndChunk", + "fields": [ + { + "name": "usage", + "type": "Option", + "default": null, + "docs": "" + } + ] + }, + { + "name": "ErrorChunk", + "fields": [ + { + "name": "error", + "type": "GooseStreamError", + "default": null, + "docs": "" + } + ] + } + ], + "methods": [] + }, + { + "name": "GooseStreamError", + "kind": "record", + "docs": "", + "fields": [ + { + "name": "kind", + "type": "GooseStreamErrorKind", + "default": null, + "docs": "" + }, + { + "name": "message", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "retry_after_ms", + "type": "Option", + "default": null, + "docs": "" + } + ], + "variants": [], + "methods": [] + }, + { + "name": "GooseStreamErrorKind", + "kind": "enum", + "docs": "", + "fields": [], + "variants": [ + { + "name": "RateLimited", + "fields": [] + }, + { + "name": "OutputTokenLimitExceeded", + "fields": [] + }, + { + "name": "ContextLengthExceeded", + "fields": [] + }, + { + "name": "Authentication", + "fields": [] + }, + { + "name": "Timeout", + "fields": [] + }, + { + "name": "ProviderUnavailable", + "fields": [] + }, + { + "name": "Generic", + "fields": [] + } + ], + "methods": [] + }, + { + "name": "ProviderCompletion", + "kind": "record", + "docs": "", + "fields": [ + { + "name": "message_json", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "usage", + "type": "Option", + "default": null, + "docs": "" + } + ], + "variants": [], + "methods": [] + }, + { + "name": "Feature", + "kind": "enum", + "docs": "", + "fields": [], + "variants": [ + { + "name": "Tools", + "fields": [] + }, + { + "name": "Streaming", + "fields": [] + }, + { + "name": "Images", + "fields": [] + }, + { + "name": "JsonSchema", + "fields": [] + }, + { + "name": "Reasoning", + "fields": [] + } + ], + "methods": [] + }, + { + "name": "Provider", + "kind": "object", + "docs": "", + "fields": [], + "variants": [], + "methods": [ + { + "name": "name", + "docs": "", + "params": [], + "returns": "String", + "throws": null, + "isAsync": false + }, + { + "name": "supported_features", + "docs": "", + "params": [], + "returns": "Vec", + "throws": null, + "isAsync": false + }, + { + "name": "stream", + "docs": "", + "params": [ + { + "name": "model", + "type": "ProviderModelConfig", + "default": null, + "docs": "" + }, + { + "name": "system", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "messages", + "type": "Vec", + "default": null, + "docs": "" + }, + { + "name": "tools", + "type": "Vec", + "default": null, + "docs": "" + } + ], + "returns": "ProviderStream", + "throws": "GooseError", + "isAsync": true + }, + { + "name": "complete", + "docs": "", + "params": [ + { + "name": "model", + "type": "ProviderModelConfig", + "default": null, + "docs": "" + }, + { + "name": "system", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "messages", + "type": "Vec", + "default": null, + "docs": "" + }, + { + "name": "tools", + "type": "Vec", + "default": null, + "docs": "" + } + ], + "returns": "ProviderCompletion", + "throws": "GooseError", + "isAsync": true + }, + { + "name": "compact", + "docs": "Summarizes a conversation down to a single message so it can continue\npast this model's context window.", + "params": [ + { + "name": "model_name", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "messages", + "type": "Vec", + "default": null, + "docs": "" + }, + { + "name": "templates", + "type": "Option", + "default": null, + "docs": "" + } + ], + "returns": "CompactionSummary", + "throws": "GooseError", + "isAsync": true + } + ] + }, + { + "name": "CompactionMessage", + "kind": "record", + "docs": "A text-only message. Compaction reads conversations as text, so this is the\nwhole input shape callers need across the language boundary.", + "fields": [ + { + "name": "role", + "type": "MessageRole", + "default": null, + "docs": "" + }, + { + "name": "text", + "type": "String", + "default": null, + "docs": "" + } + ], + "variants": [], + "methods": [] + }, + { + "name": "CompactionTemplates", + "kind": "record", + "docs": "Overrides for the summarization and summary-rendering prompts.", + "fields": [ + { + "name": "compaction", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "summary", + "type": "String", + "default": null, + "docs": "" + } + ], + "variants": [], + "methods": [] + }, + { + "name": "CompactionSummary", + "kind": "record", + "docs": "", + "fields": [ + { + "name": "text", + "type": "String", + "default": null, + "docs": "" + }, + { + "name": "input_tokens", + "type": "Option", + "default": null, + "docs": "" + }, + { + "name": "output_tokens", + "type": "Option", + "default": null, + "docs": "" + }, + { + "name": "total_tokens", + "type": "Option", + "default": null, + "docs": "" + } + ], + "variants": [], + "methods": [] + }, + { + "name": "ProviderStream", + "kind": "object", + "docs": "", + "fields": [], + "variants": [], + "methods": [ + { + "name": "next_chunk", + "docs": "", + "params": [], + "returns": "Option", + "throws": "GooseError", + "isAsync": true + } + ] + } + ] + } + ] +}