docs: add SDK API reference for Rust, Python, and Kotlin (#11251)

This commit is contained in:
Alex Hancock
2026-08-24 20:07:29 +00:00
committed by GitHub
parent 4db9e21b98
commit f9ac24cbfc
33 changed files with 2606 additions and 23 deletions
+10
View File
@@ -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
+50
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
name: Maven SDK
name: Maven GDK
on:
workflow_dispatch:
+1 -1
View File
@@ -1,4 +1,4 @@
name: Python SDK Wheels
name: Python GDK Wheels
on:
workflow_dispatch:
+49
View File
@@ -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.
+57
View File
@@ -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.
+46
View File
@@ -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
```
+59
View File
@@ -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 |
+54
View File
@@ -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.
+1 -1
View File
@@ -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 }
+1 -1
View File
@@ -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"
+3 -2
View File
@@ -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
+2 -2
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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`.
+2 -2
View File
@@ -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 -1
View File
@@ -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.
+2 -2
View File
@@ -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 -1
View File
@@ -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
+2 -2
View File
@@ -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"
+8
View File
@@ -0,0 +1,8 @@
{
"label": "GDK",
"position": 3,
"link": {
"type": "doc",
"id": "gdk/index"
}
}
+18
View File
@@ -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 />
+239
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"label": "Guides",
"position": 3,
"position": 4,
"link": {
"type": "generated-index",
"description": "Learn essential tips and recommendations for using goose"
+1 -1
View File
@@ -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 -1
View File
@@ -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