pebble-coding-agent— the coding agent Fabro runs its agent stages, Ask Fabro sessions, hook evaluators, andfabro execon. Use it withfabro-sandboxwhen 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.
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.
Cargo.toml
pebble-coding-agent to the revision Fabro’s workspace Cargo.toml pins; RunSandbox implements that revision’s Environment contract.
Quick start
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:
Inspection:
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:
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 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.
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:
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 emitCodingAgentEvent 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.
CodingEvent variants:
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’sToolMiddleware 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.
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>:
LLM client (fabro-llm)
The fabro-llm crate is Fabro’s integration layer over 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.
Cargo.toml
Quick start
Build a catalog, build a client over a credential source, then send a lithosRequest. VaultCredentialSource::environment_only() reads provider keys such as ANTHROPIC_API_KEY, OPENAI_API_KEY, and GEMINI_API_KEY from the process environment.
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.
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.
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.
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:
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 lithosMiddleware 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 returnsResult<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:
Retries
The lithosRetryMiddleware 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 aCallContext with a cancellation token through complete_with_context or stream_with_context. Cancelling the token ends the call with ErrorKind::Cancelled.
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
Providers are lithos adapters selected by the catalogadapter id: anthropic, openai, gemini, openai-compatible, and bedrock. 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.