mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-03 02:24:33 +00:00
Move the lithos-llm pin from 55add459 to 43a42ac28e9d9bcf40a91abc02be4f12ca274ebb, and the three Pebble pins from a39f43e to 67c9f48, Pebble `main`, which pins that same lithos-llm revision so Cargo holds one lithos-llm crate. lithos-llm `main` (ca19fac) is one commit further; that commit touches only its nightly workflow, so this pin stays on the revision Pebble unifies with. The `openai`, `anthropic`, `gemini`, and `openai-compatible` features are gone upstream; each expanded to `runtime`, which `bedrock` implies, so the four names leave the fabro-llm feature list. Every other manifest already names `runtime`. The catalog schema now names one adapter and many codecs per provider. `adapter` defaults to `http`, `codecs = [...]` replaces `codec` and defaults to `["openai-chat"]`, and the loader rejects the old `codec` key and the four protocol-named adapter ids. Every inline catalog in tests and docs moves to the new shape: the `openai-compatible` + `openai-chat` pair is dropped as the default, `adapter = "openai"` + `codec = "openai-responses"` becomes `codecs = ["openai-responses"]`, and the one test that swaps in a custom adapter id now adds the line instead of replacing one. The settings reference, the API schema's `Provider.adapter` description, and the SDK page describe the new fields; the three `docs/superpowers/plans/` files that show the old shape are dated, unchecked historical plans and are left as they are. The implied agent profile for an operator provider that declares none used to read the removed protocol adapter ids; it now reads the provider's first codec (Anthropic Messages and Gemini map to their harnesses, the `bedrock` adapter to Anthropic, everything else to OpenAI), with a test for the codec path. Absorbing the rest of the range: OpenRouter and Fireworks now ship enabled, so the two fabro-llm tests that used OpenRouter as the disabled fixture use `bedrock-openai`, and the docs and comments that said the two ship disabled are corrected. The built-in catalog grew past 100 enabled model rows (Vercel, TypeSafe, and the enabled OpenRouter and Fireworks rosters), so the pagination shape test walks `page[offset]` to the last page instead of assuming one page fits. `cargo update -p` on the four crates also re-resolved a few already-locked edges to match the lithos-llm lockfile: `windows-sys` 0.61.2/0.60.2 -> 0.59.0 under dirs-sys, errno, nu-ansi-term, quinn-udp, rustix, rustls-platform-verifier, tempfile, terminal_size, and winapi-util; `windows-core` 0.61.2 -> 0.62.2 under iana-time-zone; `errno` 0.2.8 -> 0.3.14 under signal-hook-registry; and `indexmap` 2.13.0 as a new public dependency of lithos-llm. No package version was added or removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
468 lines
25 KiB
Text
468 lines
25 KiB
Text
---
|
|
title: "Fabro SDK"
|
|
description: "Using Fabro as a Rust library for AI agents and multi-provider LLM completions"
|
|
---
|
|
|
|
Fabro can be used as a Rust SDK with two primary entry points:
|
|
|
|
- **`pebble-coding-agent`** — the coding agent Fabro runs its agent stages, Ask Fabro sessions, hook evaluators, and `fabro exec` on. Use it with `fabro-sandbox` when you want an agent that can read files, run commands, and interact with a codebase.
|
|
- **`fabro-llm`** — a standalone LLM client for multi-provider completions, streaming, and tool execution loops. Use this when you want direct control over LLM calls without the agent layer.
|
|
|
|
Both can be used independently of Fabro's workflow engine.
|
|
|
|
## Agent (`pebble-coding-agent` over `fabro-sandbox`)
|
|
|
|
Fabro does not ship its own agent loop. Its agent stages run pebble's `CodingAgent`, and `fabro-sandbox`'s `RunSandbox` is the `Environment` the agent's tools act through: the local filesystem, a Docker container, or a cloud sandbox. The agent loop streams model responses, executes tool calls (`shell`, `read_file`, `write_file`, `edit_file`, `apply_patch`, `glob`, `grep`, `web_fetch`, `web_search`, subagents), feeds results back, and repeats until the model answers or a limit is hit.
|
|
|
|
```toml title="Cargo.toml"
|
|
[dependencies]
|
|
fabro-auth = { git = "https://github.com/fabro-sh/fabro" }
|
|
fabro-llm = { git = "https://github.com/fabro-sh/fabro" }
|
|
fabro-sandbox = { git = "https://github.com/fabro-sh/fabro" }
|
|
pebble-coding-agent = { git = "https://github.com/lithoscomputer/pebble" }
|
|
tokio = { version = "1", features = ["full"] }
|
|
```
|
|
|
|
Pin `pebble-coding-agent` to the revision Fabro's workspace `Cargo.toml` pins; `RunSandbox` implements that revision's `Environment` contract.
|
|
|
|
### Quick start
|
|
|
|
```rust
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
use fabro_auth::VaultCredentialSource;
|
|
use fabro_llm::ClientOptions;
|
|
use fabro_sandbox::local_sandbox;
|
|
use pebble_coding_agent::environment::Environment;
|
|
use pebble_coding_agent::events::CodingEvent;
|
|
use pebble_coding_agent::tools::PermissionLevel;
|
|
use pebble_coding_agent::{CodingAgent, ShutdownReason};
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let catalog = fabro_llm::default_catalog();
|
|
let client = fabro_llm::build_client(
|
|
catalog,
|
|
Arc::new(VaultCredentialSource::environment_only()),
|
|
ClientOptions::standard(),
|
|
)
|
|
.await?
|
|
.client;
|
|
let sandbox: Arc<dyn Environment> = Arc::new(local_sandbox(PathBuf::from(".")).await?);
|
|
|
|
let mut agent = CodingAgent::builder(client, sandbox)
|
|
.model("anthropic/claude-sonnet-4.5")
|
|
.permission_level(PermissionLevel::Full)
|
|
.build()
|
|
.await?;
|
|
|
|
// Subscribe to events before sending input
|
|
let mut events = agent.subscribe();
|
|
tokio::spawn(async move {
|
|
while let Ok(event) = events.recv().await {
|
|
if let CodingEvent::TextDelta { delta } = &event.event {
|
|
print!("{delta}");
|
|
}
|
|
}
|
|
});
|
|
|
|
let report = agent.prompt("List the files in this directory").await;
|
|
agent.shutdown(ShutdownReason::Completed).await?;
|
|
report.result?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### CodingAgent
|
|
|
|
`CodingAgent` is the core type. `CodingAgent::builder(client, environment)` takes the lithos client and the environment; the builder picks the model (`provider/model`), the permission level, tool middleware, application tools, a human-input provider, a system prompt transform, an event sink, options, and subagent limits. `build()` initializes the agent: it probes the environment, loads memory files and skills, and assembles the system prompt.
|
|
|
|
**Lifecycle methods:**
|
|
|
|
| Method | Description |
|
|
|---|---|
|
|
| `prompt(input).await` | Runs one user prompt and every queued follow-up to completion. Returns a `PromptReport` with the result, token usage, cost, and timing. |
|
|
| `prompt_with_cancellation(input, &token).await` | The same, ending early when the token fires. The agent stays reusable. |
|
|
| `continue_prompt_with_cancellation(&token).await` | Continues an unfinished prompt on the history as it stands, such as after a model failover. |
|
|
| `shutdown(reason).await` | Ends the agent, emits `SessionEnded`, and flushes events. |
|
|
| `control_handle()` | A cloneable handle for steering, interrupting, and aborting from another task. |
|
|
|
|
**Inspection:**
|
|
|
|
| Method | Description |
|
|
|---|---|
|
|
| `history()` | The conversation as `History` (a sequence of `Message` values). |
|
|
| `snapshot()` | The agent's identity, route, tools, memory, and skills at the last committed event. |
|
|
| `subscribe()` | A broadcast receiver for `CodingAgentEvent` values. |
|
|
| `to_record()` | The durable `SessionRecord`, restored with `CodingAgent::resume`. |
|
|
|
|
**Steering** goes through the control handle: `queue_steering(message)` injects guidance at the next turn boundary, `steer_now(message)` interrupts the round first, `interrupt()` parks the prompt until a steer arrives, and `queue_follow_up(message)` queues another user turn.
|
|
|
|
### CodingAgentOptions
|
|
|
|
Set with the builder's `.options(...)`. Key settings with their defaults:
|
|
|
|
| Setter | Default | Description |
|
|
|---|---|---|
|
|
| `with_reasoning_effort` / `with_speed` | `None` | Request controls for the model. |
|
|
| `with_max_tokens` | catalog default | The most tokens the model may produce per turn. |
|
|
| `with_loop_detection` | `true` | Stop a session that is repeating itself. |
|
|
| `with_context_compaction` | `true` | Summarize old turns when approaching the context window limit. |
|
|
| `with_compaction_threshold_percent` | `80` | Context window usage that triggers compaction. |
|
|
| `with_wall_clock_timeout` | `None` | Hard timeout for a prompt. Reported as `InterruptReason::WallClockTimeout`. |
|
|
| `with_max_turns` | unlimited | The most model turns one prompt may use. |
|
|
| `with_memory_files` | none | Files loaded into the system prompt as memory (Fabro passes `AGENTS.md` and the profile's own file). |
|
|
| `with_skill_dirs` | none | Directories searched for `SKILL.md` files. |
|
|
|
|
Subagents are enabled with `.subagents(SubagentOptions::enabled())`; `SubagentLimits` bounds how many child sessions may be open at once.
|
|
|
|
### Sandbox
|
|
|
|
`RunSandbox` is where tools execute: the local filesystem, a Docker container,
|
|
or a cloud sandbox. It is one concrete type over a
|
|
[sandbox-driver](https://github.com/lithoscomputer/sandbox-driver) sandbox,
|
|
and every tool operation goes through it. Paths resolve against the run's
|
|
working directory, commands run as Bash with fabro's timeout and stop policy,
|
|
and output is drained even when the retained copy is capped.
|
|
|
|
```rust
|
|
impl RunSandbox {
|
|
pub async fn read_file_bytes(&self, path: &str) -> Result<Vec<u8>>;
|
|
pub async fn read_file_text(&self, path: &str) -> Result<String>;
|
|
pub async fn read_file(&self, path: &str, offset: Option<usize>, limit: Option<usize>) -> Result<String>;
|
|
pub async fn write_file(&self, path: &str, content: &str) -> Result<()>;
|
|
pub async fn delete_file(&self, path: &str) -> Result<()>;
|
|
pub async fn file_exists(&self, path: &str) -> Result<bool>;
|
|
pub async fn list_directory(&self, path: &str, depth: Option<usize>) -> Result<Vec<DirEntry>>;
|
|
pub async fn exec_command(
|
|
&self,
|
|
command: &str,
|
|
timeout_ms: u64,
|
|
working_dir: Option<&str>,
|
|
env_vars: Option<&HashMap<String, String>>,
|
|
cancel_token: Option<CancellationToken>,
|
|
) -> Result<ExecResult>;
|
|
pub async fn grep(&self, pattern: &str, path: &str, options: &GrepOptions) -> Result<Vec<GrepMatch>>;
|
|
pub async fn walk_files(&self, base: &str, relative_start: &str, options: &WalkOptions) -> Result<Vec<SandboxFile>>;
|
|
pub async fn glob(&self, pattern: &str, path: Option<&str>) -> Result<Vec<String>>;
|
|
pub async fn initialize(&self) -> Result<()>;
|
|
pub async fn cleanup(&self) -> Result<()>;
|
|
pub fn working_directory(&self) -> &str;
|
|
pub fn platform(&self) -> &str;
|
|
pub fn os_version(&self) -> String;
|
|
// ... plus git setup and push, credentials refresh, preview URLs, and access commands.
|
|
}
|
|
```
|
|
|
|
`RunSandbox` also implements pebble's `Environment` trait, so an `Arc<RunSandbox>` is what a `CodingAgent` is built over. The mapping lives in `fabro_sandbox::environment` and is checked against pebble's environment contract suite.
|
|
|
|
`DirEntry`, `GrepMatch`, `GrepOptions`, and `WalkOptions` are the driver's own
|
|
types, re-exported from `fabro_sandbox`.
|
|
|
|
**Constructors:**
|
|
|
|
| Function | Description |
|
|
|---|---|
|
|
| `local_sandbox(directory)` | Executes directly on the local filesystem through the sandbox driver Host provider. |
|
|
| `provider_sandbox(kind, ...)` | Runs on any sandbox driver provider by kind: the bundled `docker` and `daytona` providers in process, or a configured plugin. |
|
|
|
|
**Testing:** `fabro_sandbox::test_support::MockSandbox` (behind the
|
|
`test-support` feature) describes a scripted sandbox by its fields — seeded
|
|
files, the result every command returns, the platform — and hands out the
|
|
`RunSandbox` with `.sandbox()`. Afterwards it reads back what the code did:
|
|
`captured_commands()`, `written_files()`, `deleted_files()`, and so on.
|
|
|
|
### Provider profiles
|
|
|
|
Pebble picks the harness profile (system prompt, tool vocabulary, and capability defaults) from the catalog: `metadata.agent.profile` on the model, else on the provider. The `AgentProfileKind` values are `anthropic`, `claude-5`, `openai`, `gemini`, `kimi`, `gpt56`, and `gpt6`. Every lithos built-in provider declares its profile; `fabro_llm::build_catalog` fills in the profile implied by the adapter for an operator-defined provider that declares none, and `fabro_llm::catalog::agent_profile(catalog, provider, model)` reports the resolved profile.
|
|
|
|
### Events
|
|
|
|
All operations emit `CodingAgentEvent` values (a `CodingEvent` plus session ids, a sequence number, and a timestamp) through a tokio broadcast channel. Subscribe before calling `prompt()`. For a complete durable record install an `EventSink` with the builder; the broadcast channel is bounded and can lag.
|
|
|
|
```rust
|
|
let mut rx = agent.subscribe();
|
|
tokio::spawn(async move {
|
|
while let Ok(event) = rx.recv().await {
|
|
match event.event {
|
|
CodingEvent::TextDelta { delta } => print!("{delta}"),
|
|
CodingEvent::ToolCallStarted { tool_name, .. } => {
|
|
println!("[calling {tool_name}]");
|
|
}
|
|
CodingEvent::ToolCallCompleted { tool_name, is_error, .. } => {
|
|
println!("[{tool_name} done, error={is_error}]");
|
|
}
|
|
CodingEvent::LoopDetected => println!("[loop detected]"),
|
|
CodingEvent::CompactionCompleted { .. } => println!("[context compacted]"),
|
|
_ => {}
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
Key `CodingEvent` variants:
|
|
|
|
| Variant | Description |
|
|
|---|---|
|
|
| `SessionStarted` / `SessionEnded` | Session lifecycle. |
|
|
| `TextDelta { delta }` | Incremental text from the model. |
|
|
| `ReasoningDelta { delta }` | Incremental reasoning/thinking text. |
|
|
| `AssistantMessage { text, model, usage, tool_call_count, .. }` | Complete assistant turn with token usage. |
|
|
| `ToolCallStarted { tool_name, tool_call_id, arguments }` | A tool call is about to execute. |
|
|
| `ToolCallCompleted { tool_name, tool_call_id, output, is_error, .. }` | A tool call finished. |
|
|
| `Error { error }` | An `ErrorData` occurred. |
|
|
| `LoopDetected` | The agent is repeating itself. |
|
|
| `CompactionStarted` / `CompactionCompleted` | Context window compaction. |
|
|
| `SubAgentSpawned` / `SubAgentCompleted` | Sub-agent lifecycle. |
|
|
| `SteeringInjected` / `RoundInterrupted` | Steering and interrupts. |
|
|
|
|
Fabro stores every one of these as an `agent.*` run event whose properties are the `CodingAgentEvent` envelope; `fabro_types::coding_event_name` maps a variant to its run event name.
|
|
|
|
### Tool middleware
|
|
|
|
Implement pebble's `ToolMiddleware` to intercept tool calls for approval, logging, or transformation, and install it with the builder's `.tool_middleware(...)`. Fabro's `fabro_hooks::WorkflowToolHookCallback` is one: it runs the workflow's `pre_tool_use` hooks before each call and the `post_tool_use` hooks after.
|
|
|
|
```rust
|
|
use async_trait::async_trait;
|
|
use pebble_agent::{ToolCallNext, ToolCallRequest, ToolErrorKind, ToolMiddleware, ToolOutcome, ToolSystemError};
|
|
|
|
struct MyHooks;
|
|
|
|
#[async_trait]
|
|
impl ToolMiddleware for MyHooks {
|
|
async fn call(
|
|
&self,
|
|
request: ToolCallRequest,
|
|
next: ToolCallNext<'_>,
|
|
) -> Result<ToolOutcome, ToolSystemError> {
|
|
if request.call().name == "shell" {
|
|
return Ok(ToolOutcome::failure(ToolErrorKind::Denied, "shell is not allowed"));
|
|
}
|
|
next.run(request).await
|
|
}
|
|
}
|
|
```
|
|
|
|
For permission gating, `PermissionMiddleware::new(policy)` hides tools a `ToolPermissionPolicy` denies and routes the rest through an optional `ToolApprovalService`; `PermissionLevelPolicy::new(level)` is the read-only, read-write, full ladder `fabro exec --permissions` uses.
|
|
|
|
### Error handling
|
|
|
|
`PromptReport::result` is `Result<PromptOutput, pebble_coding_agent::Error>`:
|
|
|
|
| Variant | Description |
|
|
|---|---|
|
|
| `Llm(lithos_llm::Error)` | An error from the LLM provider. `llm_source()` reaches it from any variant that wraps one. |
|
|
| `SessionClosed` | A prompt was sent to a closed agent. |
|
|
| `InvalidState(String)` | The agent is in an unexpected state. |
|
|
| `ToolExecution(String)` | A tool execution failed in a way that stops the prompt. |
|
|
| `Interrupted(InterruptReason)` | The prompt was cancelled, timed out, or used every allowed turn. |
|
|
| `EventSink(EventSinkError)` | The durable event sink refused an event; the recorded stream is untrustworthy. |
|
|
|
|
---
|
|
|
|
## LLM client (`fabro-llm`)
|
|
|
|
The `fabro-llm` crate is Fabro's integration layer over [lithos-llm](https://docs.rs/lithos-llm), a provider-neutral LLM catalog and client. lithos owns the request and response vocabulary, the provider catalog, the wire codecs, streaming, and retries. `fabro-llm` adds what Fabro needs on top: building the catalog from lithos built-ins plus Fabro policy and the operator `[llm]` overlay, constructing a client from a Fabro credential source, inlining local file attachments, normalizing reasoning output, one-shot structured output, model probes, and the `fabro exec` server gateway adapter.
|
|
|
|
Everything below the Fabro layer is the lithos API. `fabro_llm` re-exports the pieces Fabro code touches most: `Client`, `Request`, `Response`, `StreamEvent`, `Error`, `ErrorKind`, `FinishReason`, and the `lithos_catalog`, `types`, `middleware`, `adapter`, and `credentials` modules. See the lithos-llm README for the full client, middleware, and streaming contract.
|
|
|
|
```toml title="Cargo.toml"
|
|
[dependencies]
|
|
fabro-auth = { git = "https://github.com/fabro-sh/fabro" }
|
|
fabro-llm = { git = "https://github.com/fabro-sh/fabro" }
|
|
fabro-types = { git = "https://github.com/fabro-sh/fabro" }
|
|
tokio = { version = "1", features = ["full"] }
|
|
serde_json = "1"
|
|
```
|
|
|
|
### Quick start
|
|
|
|
Build a catalog, build a client over a credential source, then send a lithos `Request`. `VaultCredentialSource::environment_only()` reads provider keys such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and `GEMINI_API_KEY` from the process environment.
|
|
|
|
```rust
|
|
use std::sync::Arc;
|
|
|
|
use fabro_auth::VaultCredentialSource;
|
|
use fabro_llm::{ClientOptions, Request};
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let catalog = fabro_llm::default_catalog();
|
|
let built = fabro_llm::build_client(
|
|
catalog,
|
|
Arc::new(VaultCredentialSource::environment_only()),
|
|
ClientOptions::standard(),
|
|
)
|
|
.await?;
|
|
for issue in &built.build_issues {
|
|
eprintln!("provider {} is unavailable: {}", issue.provider, issue.cause);
|
|
}
|
|
let client = built.client;
|
|
|
|
let request = Request::builder()
|
|
.model("claude-sonnet-4.5")
|
|
.user("Explain ownership in Rust in two sentences.")
|
|
.build()?;
|
|
let response = client.complete(request).await?;
|
|
|
|
println!("{}", response.text());
|
|
println!("Tokens used: {}", response.usage.input + response.usage.billable_output());
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Catalog
|
|
|
|
`fabro_llm::default_catalog()` is the lithos built-in catalog with Fabro's policy layer applied. `fabro_llm::build_catalog(&overlay, &env_lookup)` adds an operator `[llm]` overlay on top, the same layering the server and CLI use. `fabro_config::load_llm_overlay(None)` reads that overlay from the active settings file.
|
|
|
|
```rust
|
|
use fabro_config::load_llm_overlay;
|
|
|
|
let overlay = load_llm_overlay(None)?;
|
|
let catalog = fabro_llm::build_catalog(&overlay, &|name| std::env::var(name).ok())?;
|
|
```
|
|
|
|
The `fabro_llm::catalog` module reads Fabro policy from the catalog: `enabled_providers`, `models`, `model_on_provider`, `default_model`, `probe_model`, `small_default_for_ready`, and `agent_profile`. Disabled providers and models are invisible to every query. `fabro_llm::selection` chooses a provider and model before a request exists, the way run creation and validation do: a known selector resolves to its canonical offering, `provider/model` pins the provider, and an unknown selector on a passthrough provider passes through verbatim.
|
|
|
|
### Client
|
|
|
|
`fabro_llm::build_client(catalog, credentials, options)` takes any lithos `CredentialProvider` and returns a `FabroClient`: the lithos `Client`, the providers that are ready, the providers whose credentials could not be used, and the providers lithos could not build an adapter for. Credentials are read from the provider on every attempt, so a refreshed OAuth token is picked up without rebuilding the client.
|
|
|
|
`ClientOptions::standard()` turns on the lithos retry middleware (three attempts with short exponential backoff) and local attachment inlining. Add middleware with `with_middleware`, replace a provider's adapter with `with_adapter`, or set `http` to inject a configured HTTP client. `fabro_llm::build_offline_client(catalog, options)` builds a client whose only providers are custom adapters, which is how `fabro exec --server` routes every call through a Fabro server.
|
|
|
|
Credential sources live in `fabro-auth`: `VaultCredentialSource` reads a Fabro vault with an optional process-environment fallback (`VaultCredentialSource::environment_only()` for SDK callers with no vault), and `SqlVaultCredentialSource` reads the server's secret store. lithos-llm decides which secret names a provider reads (`OPENAI_API_KEY`, `MODAL_TOKEN_ID` and `MODAL_TOKEN_SECRET`, or `<PROVIDER>_API_KEY` for an operator-defined provider); Fabro's vault is keyed by those same names.
|
|
|
|
#### Requests and responses
|
|
|
|
`Request::builder()` is the lithos request builder. `model` takes a `provider/model` route, a model id or alias, or a provider id. `system`, `user`, and `message` add messages; `tool`, `tool_choice`, `response_format`, `max_output_tokens`, `temperature`, `reasoning_effort`, and `speed` set controls. `client.complete(request)` returns a `Response` whose `content` is a list of `ContentPart` values, with `text()` and `tool_calls()` helpers, plus `finish_reason`, `usage`, and `cost`.
|
|
|
|
```rust
|
|
use fabro_llm::Request;
|
|
use lithos_llm::types::{Message, Role};
|
|
|
|
let request = Request::builder()
|
|
.model("openai/gpt-5.4")
|
|
.system("You are a helpful assistant.")
|
|
.message(Message::text(Role::User, "What is the capital of France?"))
|
|
.temperature(0.0)
|
|
.build()?;
|
|
|
|
let response = client.complete(request).await?;
|
|
println!("{}", response.text());
|
|
```
|
|
|
|
There is no tool-execution loop in `fabro-llm`. The agent loop lives in `pebble-coding-agent`, which decides when to run a tool and feeds results back as `Role::Tool` messages.
|
|
|
|
### Streaming
|
|
|
|
`client.stream(request)` returns a lithos `ResponseStream`, a `Stream` of `StreamEvent` values. Events are discriminated by `type` on the wire: `started`, `content_block_start`, `text_delta`, `reasoning_delta`, `tool_call_delta`, `content_block_end`, `usage`, `rate_limits`, and `ended`, which carries the complete `Response`.
|
|
|
|
```rust
|
|
use fabro_llm::StreamEvent;
|
|
use futures::StreamExt;
|
|
|
|
let mut stream = client.stream(request).await?;
|
|
while let Some(event) = stream.next().await {
|
|
match event? {
|
|
StreamEvent::TextDelta { text, .. } => print!("{text}"),
|
|
StreamEvent::Ended { response } => {
|
|
println!("\n[done: {:?}]", response.finish_reason);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
```
|
|
|
|
A turn that ends with `FinishReason::Length` or `FinishReason::Incomplete` is not complete. Tool calls from such a turn arrive in `response.suppressed_tool_calls` and must not be executed. The coding agent treats both as a retryable failure of the turn.
|
|
|
|
### Structured output
|
|
|
|
`Client::complete_object` (a lithos method) attaches a JSON Schema as the request's response format and parses the reply into a `StructuredCompletion` with the response and the parsed document:
|
|
|
|
```rust
|
|
use fabro_llm::Request;
|
|
use serde_json::json;
|
|
|
|
let schema = json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"name": { "type": "string" },
|
|
"age": { "type": "integer" }
|
|
},
|
|
"required": ["name", "age"]
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.model("claude-sonnet-4.5")
|
|
.user("Generate a profile for a fictional character")
|
|
.build()?;
|
|
let completion = client.complete_object(request, "profile", schema).await?;
|
|
println!("Name: {}", completion.object["name"]);
|
|
```
|
|
|
|
### Reasoning
|
|
|
|
`response.reasoning()` (a lithos method) folds a response's readable reasoning parts into a `ReasoningOutput` with a summary and a trace, whichever channel the provider used. Provider replay data such as signatures and encrypted reasoning never appears in it; `ContentPart::is_replay_material()` marks the parts a conversation keeps for the next request instead.
|
|
|
|
### Middleware
|
|
|
|
Middleware is the lithos `Middleware` trait: `handle(&self, call: Call, next: Next)` sees the resolved route and request and returns an `Output` that is either a complete response or a stream. `ClientOptions::standard()` installs lithos's `InlineLocalFiles`, which rewrites local file paths in messages into inline media before dispatch.
|
|
|
|
### Error handling
|
|
|
|
Every fallible operation returns `Result<T, fabro_llm::Error>`, the lithos error. `error.kind()` is an `ErrorKind` such as `Authentication`, `RateLimit`, `Server`, `ContextLength`, `ContentFilter`, `Timeout`, `StreamDecode`, or `Cancelled`. `error.data()` is the `ErrorData` snapshot Fabro stores in run events; it reads like `Error`, prints its message, and implements `std::error::Error`.
|
|
|
|
Both `Error` and `ErrorData` answer the policy questions directly; only the loop-detection signature is Fabro's:
|
|
|
|
| Function | Description |
|
|
|---|---|
|
|
| `error.is_retryable()` | Safe to retry with the same provider, from lithos's retry classification |
|
|
| `error.failover_eligible()` | Safe to try a different provider |
|
|
| `error.is_auth_error()` | The credential was missing or rejected |
|
|
| `error.is_cancelled()` | The caller cancelled the call |
|
|
| `fabro_llm::failure_signature_hint(&data)` | A stable string for loop and restart detection |
|
|
|
|
### Retries
|
|
|
|
The lithos `RetryMiddleware` installed by `ClientOptions::standard()` retries a request until its stream delivers visible output. After visible output the client never replays on its own; the coding agent decides whether to replay a turn using `RetryPolicy::next_delay`, the same decision the middleware uses. Insert a `fabro_llm::RetryListener` into a call's context extensions to be told about each retry the middleware performs.
|
|
|
|
### Cancellation
|
|
|
|
Pass a `CallContext` with a cancellation token through `complete_with_context` or `stream_with_context`. Cancelling the token ends the call with `ErrorKind::Cancelled`.
|
|
|
|
```rust
|
|
use fabro_llm::CallContext;
|
|
|
|
let context = CallContext::new();
|
|
let cancel = context.cancellation().clone();
|
|
tokio::spawn(async move {
|
|
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
|
cancel.cancel();
|
|
});
|
|
let result = client.complete_with_context(request, context).await;
|
|
```
|
|
|
|
### Probes
|
|
|
|
`fabro_llm::probe::run_model_test(&client, "provider/model", mode, reasoning_effort, timeout)` sends the lithos model probe: one word in `Basic` mode, a two-step tool exchange in `Deep` mode. `probe_provider_with_api_key` validates an operator-supplied key against a provider's probe model before it is stored.
|
|
|
|
### Provider adapters
|
|
|
|
A provider names one lithos adapter in `adapter` (`http`, the default, or `bedrock`) and lists the wire codecs its host speaks in `codecs` (`openai-chat`, the default, `openai-responses`, `anthropic-messages`, `gemini-generate`, or `bedrock-converse`). A new OpenAI-compatible endpoint needs a catalog entry, not code.
|
|
|
|
To add a custom transport, implement the lithos `ProviderAdapter` trait and register it with `ClientOptions::with_adapter`. `fabro_llm::gateway::GatewayAdapter` is Fabro's own example: it posts each request to a Fabro server's completions endpoint, which returns lithos `Response` JSON and streams lithos `StreamEvent` JSON verbatim.
|
|
|
|
```rust
|
|
use std::sync::Arc;
|
|
|
|
use fabro_llm::ClientOptions;
|
|
use fabro_llm::gateway::GatewayAdapter;
|
|
use lithos_llm::catalog::ProviderId;
|
|
|
|
let adapter = Arc::new(GatewayAdapter::new(Box::new(my_transport)));
|
|
let built = fabro_llm::build_offline_client(
|
|
catalog,
|
|
ClientOptions::default().with_adapter(ProviderId::new("anthropic"), adapter),
|
|
)?;
|
|
```
|