Embed in Rust
pablo-core contains the runtime contracts and execution lifecycle. The pablo crate owns executable concerns such as CLI parsing, credential files, process protocols and its standalone OTel SDK.
The workspace packages are currently marked publish = false; consume them from this repository while the API is prerelease.
Smallest offline run
use opentelemetry_sdk::trace::SdkTracerProvider;
use pablo_core::{
JsonlSink, RunSpec, Runtime, ScriptedProvider, telemetry,
};
// Inside an async function running on Tokio:
let sdk = SdkTracerProvider::builder().build();
let runtime = Runtime::new(telemetry::tracer(&sdk));
let spec = RunSpec::new(
"A synthetic task",
std::env::current_dir()?,
"scripted/text-v1",
);
let provider = ScriptedProvider::text(["hello", " world"]);
let mut trace = JsonlSink::new(Vec::new(), &spec)?;
let outcome = runtime.run(&spec, &provider, &mut trace).await?;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Runtime::run grants no tools. The ScriptedProvider follows the asynchronous provider boundary without network I/O, which makes it useful for host tests.
RunSpec
RunSpec::new(input, workspace, model) fills current defaults. A host can then set:
use std::time::Duration;
use pablo_core::{ReasoningConfig, ReasoningEffort, RunSpec};
let mut spec = RunSpec::new(task, workspace, model);
spec.instructions = "Use cited evidence only.".into();
spec.session_id = Some(application_session_id);
spec.limits.max_run_duration_ms = Duration::from_secs(300).as_millis() as u64;
spec.limits.max_model_calls = Some(4);
spec.limits.max_tool_calls = Some(8);
spec.reasoning = ReasoningConfig::Effort(ReasoningEffort::Low);2
3
4
5
6
7
8
9
10
Validate current constructors against the checked-in crate when integrating; the public surface is still prerelease. The runtime contract is authoritative for semantics and defaults.
Inject a provider
A provider implements one async opening call and returns a bounded stream:
use pablo_core::provider::{ModelRequest, Provider, ProviderStream};
pub trait Provider: Send + Sync {
fn name(&self) -> &'static str;
fn stream<'a>(
&'a self,
request: ModelRequest<'a>,
) -> futures::future::BoxFuture<
'a,
Result<ProviderStream<'a>, pablo_core::provider::ProviderError>,
>;
// Optional capability, identity, routing and accounting methods omitted.
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
The request carries the stable instructions, complete accepted message history, tool catalog, output allowance, absolute deadline, reasoning intent, cancellation token and active model span context. Credentials belong in the provider object, outside RunSpec and model-visible messages.
Provider streams yield normalized text, tool-call fragments and one finish/usage record. Bound untrusted frames before converting them to events. A future/stream must yield promptly and release owned network work when dropped.
Override validate_model and validate_reasoning to reject unsupported combinations before effects. Override accounting_bounds only when the adapter can attest and enforce a complete per-call upper bound; observed usage or a price estimate is insufficient.
Consume events
EventSink is synchronous and ordered:
use pablo_core::{EventKind, RunEvent, SinkError};
let mut sink = |event: &RunEvent| -> Result<(), SinkError> {
match &event.kind {
EventKind::ModelTextDelta { text } => render(text),
EventKind::RunFinished { outcome, accounting, .. } => {
persist_terminal(outcome, accounting)
}
_ => {}
}
Ok(())
};2
3
4
5
6
7
8
9
10
11
12
The sink executes inline before the provider stream advances. Return promptly. If your UI or network sink is asynchronous, bridge it through your own bounded queue and ensure shutdown joins its consumer. A sink error stops the run and closes OTel spans.
JsonlSink<W> is the provided bounded projection. Give each ordinary run its own sink. For a supervised tree, use JsonlSink::for_tree with the root run ID.
Grant tools
Create a registry explicitly and use run_with_tools:
use pablo_core::{CancellationToken, ToolRegistry};
let tools = ToolRegistry::with_filesystem_reads()?;
let cancellation = CancellationToken::new();
let outcome = runtime
.run_with_tools(&spec, &provider, &tools, &cancellation, &mut sink)
.await?;2
3
4
5
6
7
8
Exact builder signatures vary across the configured registry paths; use the source and examples for the current prerelease revision. Filesystem writes and shell execution require explicit constructors. An empty catalog grants nothing.
Configured MCP tools use the asynchronous PreparedRun::tools_with_mcp path and must be explicitly closed/joined by the owner when unused or after the task.
Cancel correctly
let cancellation = CancellationToken::new();
let cancel_from_ui = cancellation.clone();
// Your signal/UI task calls:
cancel_from_ui.cancel();
// Keep awaiting the runtime future here.2
3
4
5
6
7
Cancellation is a request to settle. Do not drop the runtime future after calling cancel(). Awaiting lets Pablo close provider work, kill/reap shell groups, join file workers, close MCP sessions and settle child accounting.
Telemetry ownership
The core accepts a tracer and does not install a global provider or subscriber. Use telemetry::tracer(&sdk_provider) to construct the pinned instrumentation scope, or supply another valid OTel tracer implementation.
The standalone executable demonstrates bounded OTLP setup/shutdown. Embedders own the SDK, exporter, resources, incoming context and shutdown policy.
Configured reference host
crates/pablo/examples/preset_host.rs shows an end-to-end configured host. It:
- parses the same deployment inputs as the CLI
- prepares and preflights the selected provider
- creates the root owner and supervised child tools
- attaches a bounded native trace
- constructs configured telemetry
- runs
Runtimedirectly - handles Ctrl-C by cancelling and still awaiting cleanup
- prints the root task result
Build it with:
cargo build --locked -p pablo --example preset_hostThe example reuses executable-owned modules from source. Those modules are not promised library APIs; the typed pablo-core contracts are the integration boundary.