golem-rust 3.0.0-rc2

Golem Rust tooling library that facilitates writing Golem backends in Rust
Documentation

golem-rust

A library that help writing Golem programs by providing higher level Rust wrappers for Golem's runtime APIs, including functions for defining and performing operations transactionally.

Optional schema validation

Simple guests do not include regex automata or URL/IDNA/ICU tables. Enable the regex feature for regex-constrained text and regex union discriminators, and url for URL value validation and url::Url conversions. rich-validation enables both. The regex dialect and WHATWG URL/IDNA behavior are unchanged; validation that needs a disabled feature returns an explicit error, including when registering a tool with regex constraints or validating URL defaults. Schema representation, fixed MIME/unit/identifier grammars, and non-regex union discriminators remain available without these features.

Retrying user code with semantic policies

Named policies are selected by the Golem host. The selected policy can be compiled into a local schedule and applied to arbitrary async user code:

let raw = resolve_retry_policy("send", "email://welcome", &context)
    .expect("matching named policy");
let schedule = RetrySchedule::try_from(&raw)?;

schedule
    .retry_with_properties(
        async || send_email().await,
        |error| error.retry_properties(),
    )
    .await?;

To use a named policy definition without installing or resolving it through the host, compile its inner policy directly:

let named = NamedPolicy::named("email", Policy::immediate().max_retries(3));
let raw = named.try_to_raw()?;
RetrySchedule::try_from(&raw.policy)?
    .retry(async || send_email().await)
    .await?;

RetrySchedule is a user-space guest loop. It does not install a policy or create executor RetryAttempt entries, and its attempts are not one host-managed retry sequence. Host calls made by the loop retain their normal durable replay and suspension behavior.

External Durable Streams

golem_rust::durable_streams (the default json feature) reads and appends to external Durable Streams servers through golem:agent/durable-streams@2.0.0 resources. Each SDK reader or writer constructs one resource, journaling its immutable URL, mode or producer identity, deadline, and pinned secret identity without HTTP. All batches and retries reuse that resource; follow-up calls carry only read checkpoints/options or append payload/sequence/close. Rust ownership releases the resource when the client is dropped, including an unconsumed native stream. Reader close() stops consumption; writer close() sends a close-only append. Neither replaces the resource or releases it before the client is dropped. The host handles HTTP, SSE framing, authentication and protocol errors. No custom durable scope or SDK HTTP client is needed.

use golem_rust::durable_streams::{
    DurableStreamWriter, ExternalDurableStream, ReadOptions, WriteOptions,
};

async fn copy(source_url: String, target_url: String) -> Result<(), Box<dyn std::error::Error>> {
    let mut input = ExternalDurableStream::<Vec<u64>>::json(source_url, ReadOptions::default());
    let mut output = DurableStreamWriter::new(target_url, "application/json", WriteOptions::default())?;
    while let Some(value) = input.next().await? {
        // Each array is one logical JSON message, not a flattened batch.
        output.append_json(&[value], false).await?;
    }
    output.close().await?;
    Ok(())
}

Use ExternalDurableStream::bytes for individual bytes (u8), and append_bytes for byte payloads. Read batch boundaries do not preserve append chunk boundaries. JSON decoding uses Serde directly on each complete raw JSON value, retaining exact integers and nested arrays. Use Box<serde_json::value::RawValue> to retain arbitrary JSON without numeric conversion.

Readers default to offset -1, catch up, then long-poll. ReadOptions selects SSE or repeated catch-up, the per-attempt deadline, idle delay and consecutive retry budget. Offset now is resolved once by catch-up; subsequent reads use the returned opaque offset and cursor. A batch is drained before the next pull; its final payload is delivered before EOF. Empty, up-to-date, or 204 responses are not EOF, and 410 is a typed Gone error. The host caps batch size; retrying an oversized batch with the same limit cannot make progress.

next() retains typed Error details. With export_golem_agentic, into_agent_stream() creates an ordinary lazy AgentStream<T> for local reads, output, nested fields and RPC arguments. Local native reads use the existing Result<Option<T>, String> contract; a forwarded producer failure traps instead of silently ending the stream. Use result-valued native streams when an application error must be a recoverable item. Active streams cannot be snapshotted; replay reconstructs buffered items and their consumption index.

ReadOptions::auth and WriteOptions::auth take Arc<schema::wit::wire::Secret>. For an agent configuration Secret<String>, call handle() and transfer the returned GuestSecretHandle with take() into the Arc; do not call get() or a reveal operation. The host resolves the pinned secret revision only on the live request and sends it as a Bearer token. URLs must use HTTPS (HTTP is allowed only for localhost/loopback); redirects are not followed.

Writers generate one durable producer ID unless WriteOptions::producer_id is supplied. The ID, epoch, sequence, encoded payload and close flag remain fixed through retries. Cancelling or failing an append leaves it pending: call retry_pending() to resolve the exact request before supplying different data. Only acknowledgement advances the sequence. Close-only also consumes a sequence. AppendReceipt::next_offset is an Option<String>: a successful acknowledgement may omit the offset, and the SDK preserves None. Receipts do not expose a duplicate flag because HTTP 204 can acknowledge either a duplicate append or a fresh close-only request. Producer epoch and sequence determine acknowledgement, independently of whether an offset is present. Transient transport, timeout, 429 and unavailable errors use bounded retries and durable timers, respecting Retry-After; protocol/conflict errors do not. An active bounded HTTP request remains resident; idle/retry timers can suspend.

The SDK does not change Golem's idempotence mode. Explicitly disabling it retains fail-closed recovery for interrupted writes. Exactly-once external effects rely on the peer atomically persisting and retaining producer deduplication state. Golem forks copy checkpoints, producer IDs, epochs and pending data: they do not call a Durable Streams fork API or create independent producers. Divergent branches can collide on the same tuple; a later acknowledged sequence is a typed ProducerDiverged error, never automatic renumbering. Create a distinct producer explicitly when independent writes are required, without abandoning uncertain data.

Guest exports and binary size

Enable export_golem_agentic for every agent, tool, or middleware component. All such components export the same agent, tool, middleware, and snapshot interfaces. There is no role-selection or no-agent feature.

Implementation macros install capability-specific dispatch tables at component startup. #[agent_implementation] retains the agent and snapshot runtime, #[tool_implementation] retains tool dispatch, and #[tool_middleware] or #[universal_tool_middleware] retains middleware dispatch. Definition-only macros and unused generated clients do not install runtimes.

Absent capabilities discover as empty lists. Tool and middleware lookups and invocations return InvalidToolName; agent initialization and invocation return InvalidInput. Snapshot load returns an unsupported error. Agent definition and snapshot save trap with an explicit unsupported message because their WIT signatures have no error result.

Tool middleware

#[tool_middleware] and #[universal_tool_middleware] accept parameters = P for statically typed installation parameters. P must implement the SDK schema conversion traits. For monomorphic middleware, the declared constructor has signature fn(P) -> Self; universal middleware receives P as its first function argument. Without parameters, constructors remain zero-argument and universal functions have no parameter value.

Generated typed underlying proxies provide awaited command methods and start_<command>(...). Each started TypedUnderlyingInvocation has independent get(), public optional stdout and stderr, and cancel(), so calls may overlap and results and outputs may be observed in any order. Universal UnderlyingTool provides the corresponding start_with(...); invoke(...) remains the convenient awaited form.

Sequential and concurrent get() calls on the same observer share one host observation and return the cached terminal result, including errors. This does not duplicate or rewind either output. Underlying Cancelled and ResourceExhausted errors remain distinguishable to middleware code; they become ConstraintViolation only when forwarded as the middleware's own wire result.

For structural-subtype and nominal compatibility, every inner tool error must be declared by the expected tool with a compatible payload. Expected-only errors are allowed; inner-only errors are rejected. Strict equality requires matching error vocabularies.

Returning from the handler revokes new admissions but does not implicitly cancel admitted calls. Dropping a result observer releases observation rather than cancelling the invocation; call cancel() explicitly when intended. The SDK disposes abandoned observers and streams.

Universal middleware also receives the invocation's optional stdout and stderr OutputStreams. For pass-through, use invoke_forwarding_outputs(command_path, input, stdin, stdout, stderr). Typed started calls use get_forwarding_outputs(stdout, stderr). These helpers copy each readable underlying output into its matching host writer concurrently with the structured result, finish each after clean EOF, and fail it on forwarding errors without relabeling or serializing the channels.