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

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

```scala
import golem.reflection.Reflection

val target = Reflection.getAgentType("CounterAgent").flatMap(
_.toRight(golem.reflection.GolemReflectError.Discovery("CounterAgent is unavailable"))
)
val method = target.flatMap(
_.method("add").toRight(golem.reflection.GolemReflectError.Discovery("add is unavailable"))
)
```

Agent type names are unique in an environment. An `AgentType` exposes its
current component ID, lifecycle mode, constructor
`SchemaRef`, and method `SchemaRef`s. `SchemaRef` validates `SchemaValue`, packs
and unpacks canonical `zio.blocks.schema.json.Json`, and renders JSON Schema.
`getAgentType` returns `Right(None)` for a missing type and reserves `Left` for
discovery or decoding failures.

## Use the three reflected/value invocation paths

JSON convenience automatically packs and unpacks:

```scala
val invocation = client.method("add").flatMap { add =>
// The returned Future contains either a reflection error or JSON invocation.
Right(add.invokeJson(Json.Object("by" -> Json.Number(BigDecimal(5)))))
}
```

For explicit reflected packing, call `method.definition.input.packJson`, then
`invokeValue`; after awaiting, call the output `SchemaRef.unpackJson`.

For direct values, bind `agentId.dynamicClient` and manually construct the
positional record:

```scala
import golem.schema.SchemaValue

val call = agentId.dynamicClient.map(
_.method("add").invokeValue(
SchemaValue.RecordValue(List(SchemaValue.U32Value(5)))
)
)
```

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.

All reflected and direct methods support awaited, trigger, and scheduled calls
through `invokeValue`, `triggerValue`, and `scheduleValue`. Reflected live
streams are supported by awaited value calls. Trigger and scheduled reflected
or caller-codec calls reject methods whose input or output schema contains a
stream.

## Define a caller-codec typed contract

`AgentClientDefinition` does not discover remote schemas. `InputRecordCodec`
and `OutputCodec` are the caller's schema authority; the environment-unique
type name is used only to resolve current implementation identity metadata:

```scala
import golem.reflection._
import golem.runtime.{InputRecordCodec, OutputCodec}

val contract = AgentClientDefinition(
"CounterAgent",
InputRecordCodec.single[String]("name")
)
val add = contract.method(
"add",
InputRecordCodec.single[Int]("by"),
OutputCodec.single[Int]
)

val counter = contract.client.get("main")
val result = counter.map(_.method(add).invoke(5))
```

Use `agentId.client(contract)` to bind the same caller-owned codecs to an
existing durable identity. Both creation and binding fail when the named type
is not registered in the current environment.

## Lifecycle attempts

- Use a supplied `AgentId` directly or inspect it with `parts`.
- Use `AgentId.create` for schema-free durable, known-phantom, or newly
generated phantom identities.
- Reflected and caller-codec factories provide `get`, `getPhantom`, and
`newPhantom`.
- Use `DynamicAgentClient.ephemeral(componentId, typeName, constructorValue)`
for a raw ephemeral invocation address.

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.
110 changes: 110 additions & 0 deletions golem-skills/skills/scala/golem-agent-reflection-scala/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
name: golem-agent-reflection-scala
description: "Discovering and calling Golem agents through runtime reflection in Scala. Use when agent types or methods are selected dynamically, schemas must be inspected at runtime, caller-owned codecs are needed, or SchemaValue calls must avoid discovery."
---

# Calling Agents with Runtime Reflection (Scala)

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

```scala
import golem.reflection.Reflection

val target = Reflection.getAgentType("CounterAgent").flatMap(
_.toRight(golem.reflection.GolemReflectError.Discovery("CounterAgent is unavailable"))
)
val method = target.flatMap(
_.method("add").toRight(golem.reflection.GolemReflectError.Discovery("add is unavailable"))
)
```

Agent type names are unique in an environment. An `AgentType` exposes its
current component ID, lifecycle mode, constructor
`SchemaRef`, and method `SchemaRef`s. `SchemaRef` validates `SchemaValue`, packs
and unpacks canonical `zio.blocks.schema.json.Json`, and renders JSON Schema.
`getAgentType` returns `Right(None)` for a missing type and reserves `Left` for
discovery or decoding failures.

## Use the three reflected/value invocation paths

JSON convenience automatically packs and unpacks:

```scala
val invocation = client.method("add").flatMap { add =>
// The returned Future contains either a reflection error or JSON invocation.
Right(add.invokeJson(Json.Object("by" -> Json.Number(BigDecimal(5)))))
}
```

For explicit reflected packing, call `method.definition.input.packJson`, then
`invokeValue`; after awaiting, call the output `SchemaRef.unpackJson`.

For direct values, bind `agentId.dynamicClient` and manually construct the
positional record:

```scala
import golem.schema.SchemaValue

val call = agentId.dynamicClient.map(
_.method("add").invokeValue(
SchemaValue.RecordValue(List(SchemaValue.U32Value(5)))
)
)
```

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.

All reflected and direct methods support awaited, trigger, and scheduled calls
through `invokeValue`, `triggerValue`, and `scheduleValue`. Reflected live
streams are supported by awaited value calls. Trigger and scheduled reflected
or caller-codec calls reject methods whose input or output schema contains a
stream.

## Define a caller-codec typed contract

`AgentClientDefinition` does not discover remote schemas. `InputRecordCodec`
and `OutputCodec` are the caller's schema authority; the environment-unique
type name is used only to resolve current implementation identity metadata:

```scala
import golem.reflection._
import golem.runtime.{InputRecordCodec, OutputCodec}

val contract = AgentClientDefinition(
"CounterAgent",
InputRecordCodec.single[String]("name")
)
val add = contract.method(
"add",
InputRecordCodec.single[Int]("by"),
OutputCodec.single[Int]
)

val counter = contract.client.get("main")
val result = counter.map(_.method(add).invoke(5))
```

Use `agentId.client(contract)` to bind the same caller-owned codecs to an
existing durable identity. Both creation and binding fail when the named type
is not registered in the current environment.

## Lifecycle attempts

- Use a supplied `AgentId` directly or inspect it with `parts`.
- Use `AgentId.create` for schema-free durable, known-phantom, or newly
generated phantom identities.
- Reflected and caller-codec factories provide `get`, `getPhantom`, and
`newPhantom`.
- Use `DynamicAgentClient.ephemeral(componentId, typeName, constructorValue)`
for a raw ephemeral invocation address.

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.
58 changes: 58 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 @@ -132,3 +132,61 @@ steps:
equals: "reflected value"
- path: "$.direct_value"
equals: "direct value"

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

- id: "add-scala-reflected-rpc"
only_if:
language: "scala"
prompt: >
In the Scala 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 define a discovery-free caller-codec contract using
`InputRecordCodec` and `OutputCodec`, and invoke `echo` with
`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 dynamic client to the same AgentId,
manually construct the positional `SchemaValue.RecordValue`, and invoke
`echo` with `direct value` without schemas.

Return a schema-derived record containing `listed`, `typeName`,
`methodName`, `jsonValue`, `callerCodec`, `reflectedValue`, and
`directValue`. Make sure the project builds successfully.
expectedSkills:
- "golem-agent-reflection-scala"
verify:
build: true
deploy: true

- id: "verify-scala-reflected-rpc"
only_if:
language: "scala"
invoke_json:
agent: 'ReflectionCaller("main")'
method: "run"
expect:
result_json:
- path: "$.listed"
equals: true
- path: "$.typeName"
equals: "ReflectionTarget"
- path: "$.methodName"
equals: "echo"
- path: "$.jsonValue"
equals: "hello"
- path: "$.callerCodec"
equals: "caller codec"
- path: "$.reflectedValue"
equals: "reflected value"
- path: "$.directValue"
equals: "direct value"
Loading
Loading