docs: add SDK API reference for Rust, Python, and Kotlin (#11251)
This commit is contained in:
@@ -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