Sign in

    golem_sdk

    Golem SDK for MoonBit — build durable, fault-tolerant agents on the Golem platform

    golem
    wasm
    agent
    sdk
    webassembly
    durable
    component-model
    Download zip
    Version
    0.6.0-rc3
    License
    Apache-2.0
    Last updated
    23 days ago
    Downloads
    706

    No dependencies

    #Golem SDK for MoonBit

    Build durable, fault-tolerant agents on the Golem platform using MoonBit.

    #Overview

    This SDK lets you write Golem agents in MoonBit with minimal boilerplate. Agents are compiled to WebAssembly components and run on Golem, which provides automatic durable execution, persistent state, and agent-to-agent communication.

    #Quick Start

    Golem CLI 1.6.0 includes built-in MoonBit templates for ordinary agent components, pure tool middleware components, and combined agent/tool-middleware components. The golem_sdk_example1 package in this repository is the complete reference application.

    #0. Set up your project from the template

    Copy the golem_sdk_example1 directory and make the following changes:

    #moon.mod

    • Change name from "golemcloud/golem_sdk_example1" to your module name (e.g. "myorg/my_app")
    • Update description, repository, and other metadata fields

    #golem.yaml

    • Change app: from golem-sdk-example1 to your application name (e.g. my-app)
    • Under components:, rename the component key from golem:moonbit-examples: to your component name (e.g. myorg:my-agent:)

    #Component package directory

    • Rename the golem_moonbit_examples/ directory to your component name in snake_case (e.g. my_agent/)
    • The directory name must match the {{ component_name | to_snake_case }} value used in golem.yaml's build commands

    #Agent source files

    • Delete or modify the example agent files (counter.mbt, task_manager.mbt, multimodal_agent.mbt, rpc_example.mbt)
    • The generated files (golem_reexports.mbt, golem_agents.mbt, golem_derive.mbt, golem_clients.mbt, golem_tools.mbt, golem_tool_clients.mbt, and golem_tool_middlewares.mbt) will be regenerated by golem build
    • Add your own agents with #derive.agent structs
    • Make sure you have an empty fn main {} somewhere in the package

    #golem_moonbit_examples/moon.pkg (now your renamed directory)

    • The import and link sections are auto-managed by the reexports codegen tool — no manual changes needed; they will be regenerated on first golem build

    After these changes, run moon install to download dependencies, then golem build to build.

    MoonBit Golem SDK 0.5.x requires Golem CLI 1.6.0 or newer.

    #1. Define an agent

    Annotate a struct with #derive.agent, provide a ::new constructor, and add public methods:

    ///|
    #derive.agent
    struct Counter {
    name : String
    mut value : UInt64
    }

    ///|
    /// Creates a new counter with the given name
    fn Counter::new(name : String) -> Counter {
    { name, value: 0, }
    }

    ///|
    /// Increments the counter
    pub fn Counter::increment(self : Self) -> Unit {
    self.value += 1
    }

    ///|
    /// Returns the current value
    pub fn Counter::get_value(self : Self) -> UInt64 {
    self.value
    }

    #2. Use custom data types

    Annotate structs and enums with #derive.golem_schema to make them usable as method parameters and return types:

    ///|
    #derive.golem_schema
    pub(all) enum Priority {
    Low
    Medium
    High
    } derive(Eq)

    ///|
    #derive.golem_schema
    pub(all) struct TaskInfo {
    title : String
    priority : Priority
    description : String?
    }

    #3. Stream typed agent data

    Use @schema.AgentStream[T] in agent method parameters and results. Create a demand-driven producer with AgentStream::produce, and consume it one item at a time with read:

    ///|
    fn stream_values(values : Array[UInt]) -> @schema.AgentStream[UInt] {
    @schema.AgentStream::produce(async fn(writer) {
    for value in values {
    match writer.write_one(value) {
    @schema.Accepted => ()
    @schema.PeerDropped => return
    }
    }
    })
    }

    ///|
    pub async fn Counter::sum(
    self : Self,
    input : @schema.AgentStream[UInt],
    ) -> UInt {
    let _ = self
    defer input.drop()
    let mut total = 0U
    for ;; {
    match input.read() {
    Some(value) => total += value
    None => return total
    }
    }
    }

    A producer's normal return closes the writer, and read returns None only for that clean EOF or an explicit writer close. Each Accepted write waits for transport acceptance and therefore applies back-pressure; it does not mean the consumer has already processed the value. write_one and write_all consume all supplied values. If the consumer calls drop, unread values and their nested capabilities are released, and a later producer write returns PeerDropped. Peer drop is cooperative: it does not guarantee that unrelated pending producer work is interrupted.

    Stream endpoints have affine, send-once ownership even though MoonBit does not enforce affinity in its type system. An owner may read repeatedly, but encoding the stream as an invocation input or result transfers that endpoint. After clean EOF, drop, or transfer, do not read or send it again; repeated drop calls are harmless. Returning an unread input stream directly forwards the original endpoint without copying or pumping it; a partially read stream cannot be forwarded directly. Cancellation while producing, reading, writing, or forwarding propagates as canonical operation cancellation rather than EOF or an ordinary decode error.

    A producer failure traps the active operation; bare component-model streams do not have a recoverable error terminal. For recoverable application errors, stream Result values such as @schema.AgentStream[Result[Reading, SensorError]] and keep producing subsequent items as needed.

    Generated native RPC clients expose awaited methods for direct or recursively stream-bearing signatures. They do not generate trigger_*, schedule_*, or schedule_cancelable_* variants for those methods, and reject stream-bearing agent constructors because native client construction is synchronous.

    Guest bridges generated by golem build also emit schema-specific stream factories. For example, the first input of consume has new_consume_input_0_stream() (writer and reader endpoints) and produce_consume_input_0_stream(producer, on_unstarted_drop?) (a lazy producer). Output factories use produce_<method>_output_stream; nested occurrences append child indices. All factories return the native @schema.AgentStream[T], including generated record/variant items and streams of streams. Use the factory for the target schema occurrence: MoonBit Int alone cannot tell s8 from s32, Array cannot tell a fixed list from a list, and a single-element tuple is represented by its bare element. The factory binds the correct fallible item encoder and decoder without relying on a global trait implementation for that MoonBit type.

    Generated factories use AgentStream::new_with_codec / produce_with_codec. These low-level APIs take an encoder, a decoder, and an infallible typed release callback. The release callback must traverse owned endpoints directly, never rerun the encoder. If a batch conversion fails, the SDK releases converted values, the failing item, and the unconverted suffix. Generated RPC result decoders use from_value_with_codec for lazy reading; to_schema_value preserves the original affine holder for unread forwarding without executing either item codec.

    #4. Define an agent tool

    An empty struct annotated with #derive.tool defines a tool. Its public static methods become the root command and subcommands. The method whose lower-kebab-case name matches the tool name is the root command; other methods are subcommands.

    ///|
    enum WeatherError {
    #derive.error(kind="usage-error", exit_code="2")
    UnknownCity(city~ : String)
    }

    ///|
    /// Look up current weather conditions.
    #derive.tool("weather", version="1.0.0")
    struct Weather {}

    ///|
    #derive.arg("city", scope="positional", doc="city to query")
    pub fn Weather::weather(city : String) -> Result[String, WeatherError] {
    if city == "" {
    Err(UnknownCity(city~))
    } else {
    Ok("Sunny")
    }
    }

    ///|
    #derive.arg("days", scope="option", short="d", min="1", max="10")
    pub fn Weather::forecast(
    city : String,
    days : UInt?,
    ) -> Result[Array[String], WeatherError] {
    // ...
    }

    The agents code-generation command emits the tool descriptor and dispatcher in golem_tools.mbt, plus a typed RPC client in golem_tool_clients.mbt:

    let client = WeatherClient::new()
    let current : Result[String, @tool.ToolError[WeatherError]] =
    client.weather("London")
    client.drop()

    Parameters typed as @tool.Principal or @tool.ProviderOutput are provided by the runtime and omitted from typed client inputs; select stderr with #derive.arg("parameter-name", channel="stderr"). An @asyncCore.Stream[Byte] input becomes a client parameter. Either declared output selects the started-invocation result shape, with independent optional stdout/stderr streams, structured result, collection, and cancellation. Providers can write with write_all/write_all_bytes, select successful completion with finish, or select a typed @tool.ToolStreamError terminal with fail. Returning normally also finishes open outputs; dropping one while still open selects Abandoned for that channel. See the golem_sdk_tools documentation and the canonical grep/git examples for the complete annotation surface.

    #Call a tool discovered at runtime

    @reflection.get_tool_type(name) returns the tool descriptor visible to the caller. Select a command with tool.command(path); path segments may use command aliases, and the returned ToolCommand.path() contains their canonical names. Its arguments(), input_schema(), and result() describe the selected command. Supply every canonical input field in declaration order. An absent optional argument is an option value of None, while a defaulted argument carries its default.

    Discovery returns opaque immutable snapshots. Array and graph accessors return defensive copies, and a snapshot changes only after explicit rediscovery. Use SchemaRef::typed_value to validate a schema-native value and create the self-contained graph required by a fully dynamic invocation.

    Use command.pack_json(input) and command.invoke_json(input) for canonical JSON, or command.invoke_value(input) for schema-native values. The command validates inputs locally before opening RPC and validates declared results after invocation. start_value returns an invocation whose stdout, stderr, collect, get, and cancel methods handle pending and streaming calls. A command with either required output must use start_value; collect drains both outputs concurrently while awaiting its result. trigger_value is available only when the command does not require caller-readable stdout or stderr.

    @reflection.DynamicToolClient::new(name) accepts a caller-packed TypedSchemaValue and a command path when no descriptor is available. It cannot validate the input or output against a deployed schema. Reflected and dynamic calls return Result errors, including malformed remote output; generated WeatherClient methods keep their ordinary typed call behavior.

    #5. Define a tool middleware component

    A monomorphic middleware presents one statically declared #derive.tool shape. Its generated typed underlying wrapper follows the expected tool shape. Omitting expected makes it equal to presented, which is the transparent-policy case:

    ///|
    #derive.tool("messages")
    struct Messages {}

    ///|
    pub fn Messages::send(message : String) -> String {
    message
    }

    ///|
    #derive.tool_middleware("message-policy", presented="Messages")
    struct MessagePolicy {}

    ///|
    pub async fn MessagePolicy::send(
    underlying : MessagesUnderlying,
    message : String,
    ) -> Result[String, @toolMiddleware.ToolInvokeError[@tool.NoToolError]] {
    if message.contains("blocked") {
    // Zero underlying calls: this invocation is short-circuited.
    Err(
    @toolMiddleware.ToolInvokeError::ConstraintViolation(
    "messages containing 'blocked' are not allowed",
    ),
    )
    } else {
    // One underlying call: forward to the next inner layer.
    underlying.send(message)
    }
    }

    ///|
    fn main {

    }

    In a pure middleware component, the #derive.tool declarations, signatures, and annotations describe the typed tool shape for code generation. Their method bodies are not registered or invoked as ordinary tool implementations. A handler may make zero, one, or multiple calls through its underlying argument. The awaited generated methods are convenient for forwarding and retry. For overlap/fan-out, generated start_<command> methods return independent @toolMiddleware.UnderlyingInvocation values: call get() for the typed result, inspect stdout(), call cancel() explicitly to cancel, and call drop() only to release observation.

    A universal middleware handles any runtime-provided tool shape and forwards the opaque wire carriers without rebuilding them:

    Both middleware forms support typed static installation parameters. Add parameters="Type" to the derive annotation; Type must implement IntoSchema and FromSchema. A monomorphic handler then receives parameters : Type immediately after underlying, while a universal handler receives it before invocation. Without the option, the installation schema is the normalized empty record and no handler parameter is added.

    ///|
    #derive.universal_tool_middleware("audit")
    pub async fn audit(
    invocation : @toolMiddleware.RawToolInvocation,
    underlying : @toolMiddleware.UnderlyingTool,
    ) -> Result[
    @toolMiddleware.RawInvocationResult,
    @toolMiddleware.ToolInvokeError[@toolMiddleware.RawTypedSchemaValue],
    ] {
    ignore(invocation.tool_name())
    ignore(invocation.tool_metadata())
    ignore(invocation.principal())
    underlying.invoke_raw(
    invocation.command_path(),
    invocation.input(),
    invocation.stdin(),
    )
    }

    Use this minimal moon.pkg for a pure middleware package; code generation adds its other role-specific imports and exports:

    import {
    "golemcloud/golem_sdk/tool-core" @tool,
    "golemcloud/golem_sdk/tool-middleware" @toolMiddleware,
    }

    supported_targets = "+wasm"

    options(
    "is-main": true,
    )

    Select the default component template in golem.yaml:

    components: myorg:message-middlewares: templates: moonbit dir: message_middlewares

    Then run golem build. The build uses the single code-generation path and agent-guest world. Generated startup code registers every declared middleware when the component loads, while unused agent and tool discovery returns empty lists. The world imports ambient golem:tool/host, but the runtime-provided underlying capability remains the only path to the next pinned middleware layer; ambient calls remain subject to runtime permissions.

    The underlying capability and invocation streams belong to one middleware invocation. Generated wrappers enforce once-only transfer, cleanup, and revocation when the handler returns, but MoonBit itself does not provide affine type-system guarantees. Return only the final stdout stream; abandoned or unforwarded streams are cleaned up by the SDK.

    Sequential and concurrent get() calls on an underlying observer share one host observation and return the cached terminal result, including errors. This does not duplicate or rewind stdout. Cancelled and ResourceExhausted 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.

    Handler return revokes new underlying admissions but does not implicitly cancel admitted calls; observer disposal is likewise not cancellation. Commands declaring stdout receive a host writer at the guest boundary. Generated dispatch forwards the selected readable stdout into it concurrently with the result, finishes it after clean EOF, and fails it on forwarding errors; middleware authors return the readable stream and never operate the writer directly.

    Presented and expected tool declarations must currently be in the middleware's own MoonBit package. A qualified or cross-package reference is rejected with:

    Tool middleware '<middleware>' references tool '<tool>' outside its package; GOL-34 supports same-package tool shapes only

    An adapter names a different same-package expected shape; its handlers still implement the presented surface while the generated wrapper exposes the expected surface:

    ///|
    #derive.tool_middleware("file-adapter", presented="PublicFiles", expected="Storage")
    struct FileAdapter {}

    ///|
    pub async fn FileAdapter::read(
    underlying : StorageUnderlying,
    path : String,
    ) -> Result[String, @toolMiddleware.ToolInvokeError[@tool.NoToolError]] {
    underlying.fetch("files/" + path)
    }

    Middleware placement and chain order are selected by the runtime/application manifest, not by the MoonBit declarations or registration order. This SDK feature authors and builds named middleware; runtime chain traversal and manifest placement are separate platform concerns.

    See golem_sdk_example1/golem_tool_middleware_examples for a complete pure component containing the policy, universal, and adapter forms above.

    #6. Build and deploy

    Use golem build and golem deploy with a golem.yaml application manifest. See the example project for a complete setup.

    #Features

    • Agent registry — register multiple agent types in a single component via #derive.agent
    • HTTP routers — named ephemeral routers, incremental byte streams, immutable files, and OpenAPI providers; ordinary durable agents can expose live files
    • Custom data types — #derive.golem_schema implements every nexessary trait to use custom data types on the public interface of your agents
    • Agent-to-agent RPC — auto-generated client stubs (CounterClient); stream-bearing methods are awaited, while stream-free methods also support fire-and-forget and scheduled invocations
    • Runtime reflection — discover agent types, pack reflected schemas, define caller-codec clients, or invoke direct SchemaValues
    • Agent tools — code-first tool descriptors, command trees, constraints, custom errors, runtime dispatch, and typed tool RPC clients via #derive.tool
    • Tool middleware — monomorphic policy/adapter middleware and universal transparent middleware with invocation-scoped underlying capabilities
    • Multimodal input — accept mixed text, binary, and custom modality data via #derive.multimodal and @multimodal.Multimodal[T]
    • Logging — structured logging via @logging.with_name("my-agent") with level filtering
    • Tracing — span-based tracing via @context.with_span(...) with attributes
    • Host API - exports Golem's host API
    • Semantic retries — install policies in the host or interpret them around user code locally

    #Local semantic retries

    @api.retry_local interprets a Golem RetryPolicy around arbitrary asynchronous code without installing it as an executor policy. The property callback runs for every failure, and the final operation error is returned unchanged when the policy gives up:

    ///|
    let policy = try! @api.Policy::exponential(@api.Duration::millis(100), 2.0)
    .max_retries(4)
    .only_when(
    @api.Predicate::eq(@api.Props::error_type(), @api.Value::text("transient")),
    )
    .to_raw()

    ///|
    let result = @api.retry_local(policy, () => call_remote_service(), properties=error => {
    [("error-type", @api.PredicateValue::Text(error.kind()))]
    })

    Unlike retry policies installed with set_named_policy, local retries are ordinary user-space attempts. They do not create executor RetryAttempt oplog entries and do not survive recovery as one host-managed retry sequence.

    #Packages

    PackageDescription
    agentsAgent registry, RawAgent trait, register_agent, dispatch runtime
    schemaIntoSchema / FromSchema traits and impls for primitives, Option, Array, Result, records/enums/variants
    schema_modelThe recursive SchemaType / SchemaValue model + SchemaGraph / SchemaBuilder and WIT (golem:core/types@2.0.0) conversions
    multimodalMultimodal[T] + MultimodalModality trait (schema-native list<variant> with role = multimodal)
    configCode-first configuration (Config[T], Secret[T], #derive.config)
    errorsAgentError construction/decoding helpers
    loggingStructured logging with named loggers and level filtering
    contextSpan-based tracing and invocation context
    rpcAgent-to-agent RPC helpers
    httpCanonical HttpRequest, HttpResponse, HttpHeader, openapi_json, and outbound WASI HTTP
    reflectionRuntime discovery, reflected JSON packing, caller-defined static clients, and fully dynamic value invocation
    tool-coreHost-neutral tool descriptors, schemas, canonical input handling, and error model
    toolOrdinary tool registry, dispatch, help rendering, and ambient typed RPC client runtime
    tool-middlewareHost-neutral middleware registry, opaque invocation carriers, typed/universal underlying capabilities, and ownership enforcement
    interface/WIT-generated bindings (Golem host APIs, WASI interfaces)
    gen/WIT-generated WASM export glue code

    #Code Generation

    This SDK is designed to be used with golemcloud/golem_sdk_tools, which generates the boilerplate code that connects your agent definitions to the Golem runtime:

    • golem_reexports.mbt — re-exports WASM entry points from the SDK
    • golem_agents.mbt — agent registration, constructor deserialization, method dispatch
    • golem_derive.mbt — serialization impls for #derive.golem_schema types
    • golem_clients.mbt — RPC client stubs for agent-to-agent calls
    • golem_tools.mbt — tool descriptors, custom error schemas, registration, and invocation dispatch
    • golem_tool_clients.mbt — typed clients for invoking tools and nested subcommands
    • golem_tool_middlewares.mbt — monomorphic/universal middleware adapters, descriptors, typed underlying wrappers, and registration

    #Requirements

    • MoonBit toolchain (moon)
    • Golem CLI (golem) version 1.6.0+
    • wasm-tools for component model linking
    • golemcloud/golem_sdk_tools for code generation

    #Documentation

    #License

    Apache-2.0