Skip to Content
Next — You're reading the unreleased preview docs. APIs and pages may change before the next release.Switch to the current docs →
How-To GuidesMoonBitCalling Agents from External MoonBit Applications

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 directory

The 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 --yes

This 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/main

Server 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 on moonbitlang/async
  • runtime/ — the bundled runtime package (<agent-client>/runtime): HTTP transport, the schema-native value model, codecs, and configuration
  • client/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 native and uses moonbitlang/async for HTTP communication; all constructors and methods are async
  • The generated module is self-contained: it bundles its runtime and only depends on moonbitlang/async and the MoonBit core library
  • Keep application metadata in moon.mod; leave the generated bridge’s moon.mod unchanged
  • Resolve the generated module through moon.work, then run moon check --target native