Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/content/next/how-to-guides.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ Practical, step-by-step guides for building with Golem. Each guide covers a spec
<Cards.Card title="Rust (40)" href="how-to-guides/rust" />
<Cards.Card title="TypeScript (40)" href="how-to-guides/ts" />
<Cards.Card title="Scala (40)" href="how-to-guides/scala" />
<Cards.Card title="MoonBit (39)" href="how-to-guides/moonbit" />
<Cards.Card title="MoonBit (40)" href="how-to-guides/moonbit" />
</Cards>
1 change: 1 addition & 0 deletions docs/src/content/next/how-to-guides/moonbit.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Guides specific to developing Golem agents in MoonBit.
<Cards.Card title="Annotating Agent Methods (MoonBit)" href="moonbit/golem-annotate-agent-moonbit" />
<Cards.Card title="Atomic Blocks and Durability Controls (MoonBit)" href="moonbit/golem-atomic-block-moonbit" />
<Cards.Card title="Calling Agents from External MoonBit Applications" href="moonbit/golem-call-from-external-moonbit" />
<Cards.Card title="Calling Agents with Runtime Reflection (MoonBit)" href="moonbit/golem-agent-reflection-moonbit" />
<Cards.Card title="Calling Another Agent (MoonBit)" href="moonbit/golem-call-another-agent-moonbit" />
<Cards.Card title="Configuring Agent Durability (MoonBit)" href="moonbit/golem-configure-durability-moonbit" />
<Cards.Card title="Configuring CORS for MoonBit HTTP Endpoints" href="moonbit/golem-add-cors-moonbit" />
Expand Down
1 change: 1 addition & 0 deletions docs/src/content/next/how-to-guides/moonbit/_meta.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default {
"golem-annotate-agent-moonbit": "Annotating Agent Methods (MoonBit)",
"golem-atomic-block-moonbit": "Atomic Blocks and Durability Controls (MoonBit)",
"golem-call-from-external-moonbit": "Calling Agents from External MoonBit Applications",
"golem-agent-reflection-moonbit": "Calling Agents with Runtime Reflection (MoonBit)",
"golem-call-another-agent-moonbit": "Calling Another Agent (MoonBit)",
"golem-configure-durability-moonbit": "Configuring Agent Durability (MoonBit)",
"golem-add-cors-moonbit": "Configuring CORS for MoonBit HTTP Endpoints",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Calling Agents with Runtime Reflection (MoonBit)

Use generated clients when the complete target definition is available at
compile time. Otherwise choose one authority and invocation path explicitly:
caller-authored codecs, runtime-reflected schemas, or direct schema-free
`SchemaValue` calls.

## Discover and inspect schemas

```moonbit
let available = @reflection.get_all_agent_types()
guard @reflection.get_agent_type("CounterAgent") is Some(counter_type) else {
raise @reflection.ReflectError::Discovery("CounterAgent is unavailable")
}
guard counter_type.find_method("add") is Some(add) else {
raise @reflection.ReflectError::Discovery("add is unavailable")
}
```

Agent type names and reflection identity strings are environment-scoped.
`AgentType` exposes the currently implementing component as deployment
metadata, plus its lifecycle mode, constructor `SchemaRef`, and method schemas.
Reflection clients do not pin that component ID. `SchemaRef::pack_json`
converts canonical JSON into a schema-native value; `unpack_json` performs the
awaited conversion back.

## Use the three Level 3 invocation paths

JSON convenience automatically packs and unpacks:

```moonbit
let counter = counter_type.get_json(Json::object({
"name": Json::string("main"),
}))
let result = counter.invoke_json(
"add",
Json::object({ "by": Json::number(5.0) }),
)
```

For explicit reflected packing, call `add.input.pack_json`, invoke through
`invoke_value`, await the result, and call the output `SchemaRef::unpack_json`.

For direct values, manually construct the positional record and use a dynamic
client:

```moonbit
let result = dynamic.invoke_value(
"add",
@model.SchemaValue::Record([@model.SchemaValue::U32(5U)]),
)
```

Direct clients never discover or validate schemas. Constructor and method
record fields must be packed in declaration order; the runtime authoritatively
accepts or rejects the attempt.

Both reflected and direct clients support awaited, trigger, and scheduled
calls through `invoke_value`, `trigger_value`, and `schedule_value`. Awaited
calls use the asynchronous host invocation path and can carry live streams.
Reflected trigger and scheduled calls reject methods whose input or output
schema contains a stream.

## Define a caller-codec typed contract

`CallerCodecClient` does not discover payload schemas. The caller's
`IntoSchema` and `FromSchema` implementations are the schema authority:

```moonbit
#derive.golem_schema
struct CounterId { name : String }
#derive.golem_schema
struct AddInput { by : UInt }
#derive.golem_schema
struct AddOutput { value : UInt }

let counter = @reflection.CallerCodecClient::create(
"CounterAgent",
CounterId::{ name: "main" },
)
let result : @reflection.Invocation[AddOutput] = counter.invoke(
"add",
AddInput::{ by: 5U },
)
```

The type name is resolved to its current implementing component in the
environment; callers do not pin component metadata. Existing IDs can be bound
with `CallerCodecClient::bind(agent_id, "CounterAgent")`, which strictly
checks the parsed type name. Caller codecs remain authoritative for payload
encoding and decoding.

For trigger or scheduled caller-codec calls, pass an output type tag so the
SDK can reject live streams in either direction:

```moonbit
counter.trigger(
"reset",
ResetInput::{},
(@schema.type_tag() : @schema.TypeTag[Unit]),
)
```

## Raw lifecycle attempts

- Use a supplied `ParsedAgentId` with `DynamicAgentClient::from_agent_id`, or
inspect it with `ParsedAgentId::parts`.
- Use `DynamicAgentClient::lookup` for a manually packed durable identity
without a phantom UUID.
- Use `resume_phantom` with a known UUID.
- Use `new_phantom` to generate a UUID and receive the client, reusable
`ParsedAgentId`, and phantom UUID together.
- Use `DynamicAgentClient::ephemeral` for a raw invocation address built from
the type name and manually packed constructor values.

Raw lifecycle helpers perform no discovery, schema validation, lifecycle-mode
verification, or local factory checks. The runtime is authoritative.

An ephemeral address has no guaranteed reusable pre-invocation identity. The
final identity comes from invocation metadata and must not be treated as a
resumable durable identity.

## Choose an invocation path

| Situation | Use |
|---|---|
| Generated definition available | Generated agent client |
| Typed contract owned by the caller | `CallerCodecClient` resolved by environment type name |
| Runtime-selected method with automatic JSON conversion | `invoke_json` |
| Explicit runtime-schema packing | `SchemaRef::pack_json`, `invoke_value`, `unpack_json` |
| Schema-free infrastructure with Golem values | `DynamicAgentClient` |
136 changes: 136 additions & 0 deletions golem-skills/skills/moonbit/golem-agent-reflection-moonbit/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
name: golem-agent-reflection-moonbit
description: "Discovering and calling Golem agents through runtime reflection in MoonBit. Use when agent types or methods are selected dynamically, schemas must be inspected at runtime, caller-authored codecs are needed, or SchemaValue calls must avoid discovery."
---

# Calling Agents with Runtime Reflection (MoonBit)

Use generated clients when the complete target definition is available at
compile time. Otherwise choose one authority and invocation path explicitly:
caller-authored codecs, runtime-reflected schemas, or direct schema-free
`SchemaValue` calls.

## Discover and inspect schemas

```moonbit
let available = @reflection.get_all_agent_types()
guard @reflection.get_agent_type("CounterAgent") is Some(counter_type) else {
raise @reflection.ReflectError::Discovery("CounterAgent is unavailable")
}
guard counter_type.find_method("add") is Some(add) else {
raise @reflection.ReflectError::Discovery("add is unavailable")
}
```

Agent type names and reflection identity strings are environment-scoped.
`AgentType` exposes the currently implementing component as deployment
metadata, plus its lifecycle mode, constructor `SchemaRef`, and method schemas.
Reflection clients do not pin that component ID. `SchemaRef::pack_json`
converts canonical JSON into a schema-native value; `unpack_json` performs the
awaited conversion back.

## Use the three Level 3 invocation paths

JSON convenience automatically packs and unpacks:

```moonbit
let counter = counter_type.get_json(Json::object({
"name": Json::string("main"),
}))
let result = counter.invoke_json(
"add",
Json::object({ "by": Json::number(5.0) }),
)
```

For explicit reflected packing, call `add.input.pack_json`, invoke through
`invoke_value`, await the result, and call the output `SchemaRef::unpack_json`.

For direct values, manually construct the positional record and use a dynamic
client:

```moonbit
let result = dynamic.invoke_value(
"add",
@model.SchemaValue::Record([@model.SchemaValue::U32(5U)]),
)
```

Direct clients never discover or validate schemas. Constructor and method
record fields must be packed in declaration order; the runtime authoritatively
accepts or rejects the attempt.

Both reflected and direct clients support awaited, trigger, and scheduled
calls through `invoke_value`, `trigger_value`, and `schedule_value`. Awaited
calls use the asynchronous host invocation path and can carry live streams.
Reflected trigger and scheduled calls reject methods whose input or output
schema contains a stream.

## Define a caller-codec typed contract

`CallerCodecClient` does not discover payload schemas. The caller's
`IntoSchema` and `FromSchema` implementations are the schema authority:

```moonbit
#derive.golem_schema
struct CounterId { name : String }
#derive.golem_schema
struct AddInput { by : UInt }
#derive.golem_schema
struct AddOutput { value : UInt }

let counter = @reflection.CallerCodecClient::create(
"CounterAgent",
CounterId::{ name: "main" },
)
let result : @reflection.Invocation[AddOutput] = counter.invoke(
"add",
AddInput::{ by: 5U },
)
```

The type name is resolved to its current implementing component in the
environment; callers do not pin component metadata. Existing IDs can be bound
with `CallerCodecClient::bind(agent_id, "CounterAgent")`, which strictly
checks the parsed type name. Caller codecs remain authoritative for payload
encoding and decoding.

For trigger or scheduled caller-codec calls, pass an output type tag so the
SDK can reject live streams in either direction:

```moonbit
counter.trigger(
"reset",
ResetInput::{},
(@schema.type_tag() : @schema.TypeTag[Unit]),
)
```

## Raw lifecycle attempts

- Use a supplied `ParsedAgentId` with `DynamicAgentClient::from_agent_id`, or
inspect it with `ParsedAgentId::parts`.
- Use `DynamicAgentClient::lookup` for a manually packed durable identity
without a phantom UUID.
- Use `resume_phantom` with a known UUID.
- Use `new_phantom` to generate a UUID and receive the client, reusable
`ParsedAgentId`, and phantom UUID together.
- Use `DynamicAgentClient::ephemeral` for a raw invocation address built from
the type name and manually packed constructor values.

Raw lifecycle helpers perform no discovery, schema validation, lifecycle-mode
verification, or local factory checks. The runtime is authoritative.

An ephemeral address has no guaranteed reusable pre-invocation identity. The
final identity comes from invocation metadata and must not be treated as a
resumable durable identity.

## Choose an invocation path

| Situation | Use |
|---|---|
| Generated definition available | Generated agent client |
| Typed contract owned by the caller | `CallerCodecClient` resolved by environment type name |
| Runtime-selected method with automatic JSON conversion | `invoke_json` |
| Explicit runtime-schema packing | `SchemaRef::pack_json`, `invoke_value`, `unpack_json` |
| Schema-free infrastructure with Golem values | `DynamicAgentClient` |
59 changes: 59 additions & 0 deletions golem-skills/tests/harness/scenarios/rpc-5-runtime-reflection.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,62 @@ steps:
equals: "reflected value"
- path: "$.directValue"
equals: "direct value"

- id: "create-moonbit-project"
only_if:
language: "moonbit"
create_project:
name: test-app
verify:
build: true

- id: "add-moonbit-reflected-rpc"
only_if:
language: "moonbit"
prompt: >
In the MoonBit test-app project, add a durable `ReflectionTarget` agent
identified by a string `name`, with an `echo` method accepting and
returning a string. Add a durable `ReflectionCaller` whose async `run`
method discovers `ReflectionTarget`, inspects `echo`, and invokes it
through the JSON convenience path with `hello`.

Also invoke the same target through a discovery-free
`CallerCodecClient` with caller-authored record codecs and the message
`caller codec`. Explicitly pack JSON through the reflected method
`SchemaRef`, invoke its value API, await the result, and unpack it as
`reflected value`. Finally bind a `DynamicAgentClient` to the same
`AgentId`, manually construct a positional `SchemaValue::Record`, and
invoke `echo` with `direct value` without schemas.

Return a schema-derived record containing `listed`, `type_name`,
`method_name`, `json_value`, `caller_codec`, `reflected_value`, and
`direct_value`. Make sure the project builds successfully.
expectedSkills:
- "golem-agent-reflection"
- "golem-agent-reflection-moonbit"
verify:
build: true
deploy: true

- id: "verify-moonbit-reflected-rpc"
only_if:
language: "moonbit"
invoke_json:
agent: 'ReflectionCaller("main")'
method: "run"
expect:
result_json:
- path: "$.listed"
equals: true
- path: "$.type_name"
equals: "ReflectionTarget"
- path: "$.method_name"
equals: "echo"
- path: "$.json_value"
equals: "hello"
- path: "$.caller_codec"
equals: "caller codec"
- path: "$.reflected_value"
equals: "reflected value"
- path: "$.direct_value"
equals: "direct value"
2 changes: 2 additions & 0 deletions sdks/moonbit/golem_sdk/README.mbt.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ Use `golem build` and `golem deploy` with a `golem.yaml` application manifest. S
- **Agent registry** — register multiple agent types in a single component via `#derive.agent`
- **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 `SchemaValue`s
- **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]`
Expand All @@ -380,6 +381,7 @@ Use `golem build` and `golem deploy` with a `golem.yaml` application manifest. S
| `logging` | Structured logging with named loggers and level filtering |
| `context` | Span-based tracing and invocation context |
| `rpc` | Agent-to-agent RPC helpers |
| `reflection` | Runtime discovery, reflected JSON packing, caller-codec clients, and schema-free value invocation |
| `tool-core` | Host-neutral tool descriptors, schemas, canonical input handling, and error model |
| `tool` | Ordinary tool registry, dispatch, help rendering, and ambient typed RPC client runtime |
| `tool-middleware` | Host-neutral middleware registry, opaque invocation carriers, typed/universal underlying capabilities, and ownership enforcement |
Expand Down
Loading
Loading