docs: add SDK API reference for Rust, Python, and Kotlin (#11251)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Maven SDK
|
||||
name: Maven GDK
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Python SDK Wheels
|
||||
name: Python GDK Wheels
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -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<S, E>`** — 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<S, E>`** — 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<Step<..>>` 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.
|
||||
@@ -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<Message>;
|
||||
fn templates(&self) -> Templates { Templates::default() }
|
||||
}
|
||||
|
||||
pub trait CompactionOutput {
|
||||
fn set_summary(&mut self, summary: Message);
|
||||
fn set_usage(&mut self, usage: ProviderUsage);
|
||||
}
|
||||
```
|
||||
|
||||
`Vec<Message>` 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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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<MessageStream, ProviderError>;
|
||||
}
|
||||
```
|
||||
|
||||
`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 |
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
//!
|
||||
|
||||
@@ -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())
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"label": "Experimental",
|
||||
"position": 7,
|
||||
"position": 8,
|
||||
"link": {
|
||||
"type": "doc",
|
||||
"id": "experimental/index"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "GDK",
|
||||
"position": 3,
|
||||
"link": {
|
||||
"type": "doc",
|
||||
"id": "gdk/index"
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
<GdkApiReference />
|
||||
@@ -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
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
|
||||
### 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:<version>")
|
||||
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.
|
||||
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
## 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<dyn std::error::Error>> {
|
||||
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<StreamChunk>` |
|
||||
| `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
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"label": "Architecture Overview",
|
||||
"position": 6,
|
||||
"position": 7,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Extend goose functionalities with extensions and custom configurations"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"label": "Guides",
|
||||
"position": 3,
|
||||
"position": 4,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Learn essential tips and recommendations for using goose"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"label": "Tutorials",
|
||||
"position": 4,
|
||||
"position": 5,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "How to use goose in various ways"
|
||||
|
||||
@@ -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<GdkItem["kind"], string> = {
|
||||
object: "Class",
|
||||
callback: "Interface",
|
||||
record: "Data type",
|
||||
enum: "Enum",
|
||||
error: "Error",
|
||||
};
|
||||
|
||||
const KIND_HEADINGS: Record<GdkItem["kind"], string> = {
|
||||
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 (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{caption}</th>
|
||||
<th>Type</th>
|
||||
{hasDefaults && <th>Default</th>}
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row) => (
|
||||
<tr key={row.name}>
|
||||
<td>
|
||||
<code>{language.field(row.name)}</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>{language.type(row.type)}</code>
|
||||
</td>
|
||||
{hasDefaults && (
|
||||
<td>{row.default ? <code>{language.default(row.default)}</code> : "—"}</td>
|
||||
)}
|
||||
<td>{row.docs || "—"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
function FuncEntry({
|
||||
func,
|
||||
language,
|
||||
owner,
|
||||
}: {
|
||||
func: GdkFunc;
|
||||
language: Language;
|
||||
owner?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.entry} id={slug(owner ?? "fn", func.name)}>
|
||||
<h4 className={styles.entryTitle}>
|
||||
<code>{language.func(func.name)}</code>
|
||||
</h4>
|
||||
{func.docs && <p>{func.docs}</p>}
|
||||
<CodeBlock language={language.prism}>{signature(func, language, owner)}</CodeBlock>
|
||||
<ParamTable rows={func.params} language={language} caption="Parameter" />
|
||||
{func.throws && (
|
||||
<p className={styles.meta}>
|
||||
Raises <code>{language.errorType(func.throws)}</code>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemEntry({ item, language }: { item: GdkItem; language: Language }) {
|
||||
const dataCarrying = item.variants.some((variant) => variant.fields.length > 0);
|
||||
return (
|
||||
<section className={styles.item} id={slug(item.name)}>
|
||||
<h3 className={styles.itemTitle}>
|
||||
<code>{item.kind === "error" ? language.errorType(item.name) : item.name}</code>
|
||||
<span className={styles.badge}>{KIND_LABELS[item.kind]}</span>
|
||||
</h3>
|
||||
{item.docs && <p>{item.docs}</p>}
|
||||
|
||||
<ParamTable rows={item.fields} language={language} caption="Field" />
|
||||
|
||||
{item.variants.length > 0 && (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{item.kind === "error" ? "Variant" : "Case"}</th>
|
||||
<th>Associated data</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{item.variants.map((variant) => (
|
||||
<tr key={variant.name}>
|
||||
<td>
|
||||
<code>
|
||||
{item.kind === "error" && language.id === "kotlin"
|
||||
? `${language.errorType(item.name)}.${variant.name}`
|
||||
: language.variant(variant.name, dataCarrying)}
|
||||
</code>
|
||||
</td>
|
||||
<td>
|
||||
{variant.fields.length === 0
|
||||
? "—"
|
||||
: variant.fields.map((field) => (
|
||||
<div key={field.name}>
|
||||
<code>
|
||||
{language.field(field.name)}: {language.type(field.type)}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
{item.methods.map((method) => (
|
||||
<FuncEntry key={method.name} func={method} language={language} owner={item.name} />
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GdkApiReference() {
|
||||
const [languageId, setLanguageId] = useState<LanguageId>("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 (
|
||||
<div>
|
||||
<div className={styles.toolbar}>
|
||||
<div className={styles.tabs} role="tablist" aria-label="GDK language">
|
||||
{LANGUAGES.map((entry) => (
|
||||
<button
|
||||
key={entry.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={entry.id === languageId}
|
||||
className={entry.id === languageId ? styles.tabActive : styles.tab}
|
||||
onClick={() => setLanguageId(entry.id)}
|
||||
>
|
||||
{entry.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<label className={styles.version}>
|
||||
Version
|
||||
<select
|
||||
value={docVersion}
|
||||
onChange={(event) => setDocVersion(event.target.value)}
|
||||
aria-label="GDK version"
|
||||
>
|
||||
{VERSIONS.map((entry) => (
|
||||
<option key={entry.docVersion} value={entry.docVersion}>
|
||||
{entry.docVersion}.x
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p className={styles.meta}>
|
||||
Generated from <code>{doc.source}</code> at <code>goose-sdk {doc.version}</code>.
|
||||
</p>
|
||||
|
||||
<h2 id="functions">Functions</h2>
|
||||
{doc.functions.map((func) => (
|
||||
<FuncEntry key={func.name} func={func} language={language} />
|
||||
))}
|
||||
|
||||
{grouped.map((group) => (
|
||||
<React.Fragment key={group.kind}>
|
||||
<h2 id={slug(group.kind, "types")}>{KIND_HEADINGS[group.kind]}</h2>
|
||||
{group.items.map((item) => (
|
||||
<ItemEntry key={item.name} item={item} language={language} />
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<string, string>;
|
||||
|
||||
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"),
|
||||
},
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user