Calling Agents from External MoonBit Applications
Overview
Golem can generate typed MoonBit client libraries (bridge SDKs) for calling agents from any external MoonBit application — a CLI tool, a background job, or any native MoonBit program. The generated client communicates with the Golem server’s REST API and provides a fully typed interface matching the agent’s methods.
The bridge target language is independent of the agent’s source language, so this works for agents written in any language, including MoonBit.
Step 1: Enable Bridge Generation
Add a bridge section to golem.yaml:
bridge:
moonbit:
external:
agents: "*" # Generate for all agents
# Or list specific agents:
# agents:
# - MyAgent
# - my-app:billing
outputDir: ./bridge-sdk/moonbit # Optional custom output directoryThe agents field accepts "*" (all agents), or a list of agent type names or component names (namespace:name).
Step 2: Generate the Bridge SDK
The recommended approach is to declare the bridge in golem.yaml (as shown above) and let golem build produce the SDK as part of the normal build:
golem build --yesThis produces a MoonBit module per agent type (e.g., my-agent-client/) under
bridge-sdk/moonbit/ (or golem-temp/bridge-sdk/moonbit/ when outputDir is omitted). Re-running
golem build after agent changes keeps the generated client in sync automatically.
Avoid invoking golem generate-bridge manually — it exists as a low-level escape hatch, but the manifest-driven flow above is the supported way to keep bridges configured, reproducible, and up to date.
Step 3: Add the Generated Module to a Workspace
The generated my-agent-client/ directory is a self-contained module with a moon.mod manifest.
Do not edit the generated manifest. The external application also uses moon.mod.
Create the external application in external-client/. Add external-client/moon.work with both
modules to resolve the generated client locally:
members = [
".",
"../bridge-sdk/moonbit/my-agent-client",
]Then import the generated module and moonbitlang/async in the application’s moon.mod, using the
exact generated name/version and the async version declared by the bridge’s moon.mod:
name = "my-org/external-client"
preferred_target = "native"
import {
"my-agent-client@0.0.1",
"moonbitlang/async@0.21.2",
}The workspace resolves my-agent-client locally. Run moon check --target native from
external-client; current MoonBit commands fetch registry dependencies automatically, so do not
use the old project-dependency meaning of moon install.
Step 4: Use the Generated Client
Import the generated client package, the bundled runtime package, and moonbitlang/async in your moon.pkg. The moonbitlang/async import is required by MoonBit to use an async fn main entry point, even if you never reference @async directly:
import {
"moonbitlang/async" @async,
"my-agent-client/client" @client,
"my-agent-client/runtime" @runtime,
}Then call the agent. All generated constructors and methods are async and run on the moonbitlang/async event loop:
async fn main {
// Configure the Golem server connection
@client.MyAgent::configure(@runtime.Local, "my-app", "local")
// Get or create an agent instance
let agent = @client.MyAgent::get("my-instance")
// Call methods — fully typed parameters and return values
let result = agent.do_something("input")
println(result)
}Build and run the native binary:
moon build --target native
moon run --target native src/mainServer Configuration
@runtime.GolemServer has three variants:
// Local development server (http://localhost:9881)
@runtime.Local
// Golem Cloud
@runtime.Cloud("your-api-token")
// Custom deployment
@runtime.Custom("https://my-golem.example.com", "your-token")configure(server, appName, envName) sets the connection shared by every generated client in the module.
Reconnect window of streaming calls
A streaming call reconnects with a growing delay (50 ms up to 3 s) while the server accepts neither its start nor a resume; HTTP 429 and 5xx on the upgrade and retryable rejections are retried too. The optional reconnect argument of configure bounds the time without progress: @runtime.Within(milliseconds) (the default is @runtime.default_reconnect_policy, 5 minutes) or @runtime.Unlimited. Past the window the call raises @runtime.Unreachable(endpoint~, window_ms~, last_cause~), a case of @runtime.BridgeError. Cancelling the task that runs the call stops it earlier.
@client.MyAgent::configure(@runtime.Local, "my-app", "local", reconnect=@runtime.Within(30000))Phantom Agents
To create multiple agent instances that share the same constructor parameters, use phantom agents. The phantom id is the last argument, after the constructor parameters:
let agent = @client.MyAgent::get_phantom("my-instance", "shared-phantom-id")Or generate a random phantom id automatically:
let agent = @client.MyAgent::new_phantom("my-instance")Triggering and Scheduling
In addition to the awaiting call, each method has fire-and-forget and scheduled variants:
// Fire-and-forget: enqueue the invocation without waiting for a result
agent.trigger_do_something("input")
// Schedule the invocation for a future time: the RFC 3339 timestamp comes
// first, followed by the method parameters
agent.schedule_do_something("2026-01-01T00:00:00Z", "input")Overriding Configuration
If the agent declares locally overridable configuration, the generated client
also exposes *_with_config constructor variants. Each declared config value
becomes an optional parameter (named config_<path>) appended after the
constructor parameters; pass Some(..) to override a value or None to leave it
at its default:
// Durable: override config when getting/creating the agent
let agent = @client.MyAgent::get_with_config("my-instance", Some("override-value"))
// With an explicit phantom id (the phantom id comes before the config overrides)
let p = @client.MyAgent::get_phantom_with_config("my-instance", "phantom-id", Some("v"))
// With a fresh random phantom id
let np = @client.MyAgent::new_phantom_with_config("my-instance", None)These variants are only generated when the agent declares overridable (non-secret) configuration.
Multimodal Methods
When a method takes or returns multimodal input (a sequence of mixed-modality
items, such as text and images), the parameter or return type is an
Array[Multimodal<N>], where Multimodal<N> is a generated enum with one case
per modality:
let reply = agent.analyze([
Multimodal0::Text("describe this"),
Multimodal0::Image(@runtime.BinaryUrl("https://example.com/cat.png")),
])Unstructured Text and Binary
Schema values explicitly role-marked as unstructured text or unstructured binary map to the ergonomic
@runtime.UnstructuredText and @runtime.UnstructuredBinary wrappers, each of
which is either inline content or a URL reference:
// Inline text with an optional BCP-47 language code, or a URL
@runtime.TextInline("hello", Some("en"))
@runtime.TextUrl("https://example.com/note.txt")
// Inline bytes with an optional MIME type, or a URL
@runtime.BinaryInline(bytes, Some("image/png"))
@runtime.BinaryUrl("https://example.com/cat.png")These wrappers do not represent arbitrary bare text or binary schema values. Bare binary maps to
@runtime.AgentBinary { bytes, mime_type }, the same shape as @schema.AgentBinary in the MoonBit
SDK. When a role-marked
value restricts the allowed language codes or MIME types, the generated
client validates returned values against the allowed set and raises a
@runtime.BridgeError on a disallowed code.
Text and Quantity
Bare text maps to @runtime.Text[S] and a quantity maps to @runtime.Quantity[U], the same
shapes as @schema.Text[S] and @schema.Quantity[U] in the MoonBit SDK. The generated client
declares the marker types: one UnitXxx per quantity unit (for example UnitKg) and one
TextSpec<N> per set of text restrictions.
let weight : @runtime.Quantity[UnitKg] = @runtime.Quantity::new(1500L, 3, "kg")
let caption : @runtime.Text[TextSpec0] = @runtime.Text::new("hello", language="en")Quantity::new raises a @runtime.BridgeError when the unit is not one the schema allows. The
client checks text restrictions and quantity units and bounds against the schema when it sends
and receives a value.
Generated Module Layout
Each agent type gets its own moon module directory containing:
moon.mod— module metadata, depending onmoonbitlang/asyncruntime/— the bundled runtime package (<agent-client>/runtime): HTTP transport, the schema-native value model, codecs, and configurationclient/client.mbt— the generated, fully typed client package (<agent-client>/client): the agent handle, its constructors and method wrappers, and the generated MoonBit types and codecs for all custom parameter and return types
Key Points
- Bridge generation runs during
golem build— agents must be built first so their type information is available - The generated code is fully typed — method parameters and return types map to MoonBit types, and all custom types (records, variants, enums, flags, unions, multimodal, unstructured text/binary) are generated as corresponding MoonBit types
- The client targets
nativeand usesmoonbitlang/asyncfor HTTP communication; all constructors and methods areasync - The generated module is self-contained: it bundles its runtime and only depends on
moonbitlang/asyncand the MoonBit core library - Keep application metadata in
moon.mod; leave the generated bridge’smoon.modunchanged - Resolve the generated module through
moon.work, then runmoon check --target native