diff --git a/docs/src/content/next/app-manifest.mdx b/docs/src/content/next/app-manifest.mdx index e86df3b2b2..b67bd1da33 100644 --- a/docs/src/content/next/app-manifest.mdx +++ b/docs/src/content/next/app-manifest.mdx @@ -43,7 +43,7 @@ The _Golem CLI_ commands that use _Application Manifest_ start by searching for After resolving relative paths in the documents they are merged, then _component selection_ happens: this can be either explicit, by using `--component-name` CLI flags, or implicit, in which case only components defined in the directory - including subdirectories - from where the _Golem CLI_ was executed are used. -Application Manifest documents can also be explicitly passed to the CLI, using the `--app` flag. Note that when using explicit documents the `includes` field is not used, it is expected that all relevant documents are provided for the CLI. +An Application Manifest can also be selected explicitly with `--app-manifest-path` (`-A`). This chooses the root manifest instead of searching parent directories; its `includes` are still resolved. Use `--disable-app-manifest-discovery` (`-X`) to run without manifest discovery. ## Template variables and functions diff --git a/docs/src/content/next/cli/_meta.js b/docs/src/content/next/cli/_meta.js index 1527c22ae7..13b1b44637 100644 --- a/docs/src/content/next/cli/_meta.js +++ b/docs/src/content/next/cli/_meta.js @@ -4,6 +4,7 @@ export default { components: "Components", agents: "Agents", permissions: "Permissions", + "account-usage": "Account Usage and Limits", plugins: "Plugins", "shell-completion": "Shell Completion", "install-from-source": "Install from Source", diff --git a/docs/src/content/next/cli/account-usage.mdx b/docs/src/content/next/cli/account-usage.mdx new file mode 100644 index 0000000000..5368aeacfa --- /dev/null +++ b/docs/src/content/next/cli/account-usage.mdx @@ -0,0 +1,95 @@ +import { Callout } from "nextra/components" + +# Account usage and limits + + + These commands operate against the selected server. Results depend on its metering configuration, + your account plan, and your permission to view or change the selected account. + + +Golem reports account usage by UTC calendar month. Usage reporting is separate from enforcement: usage answers what was measured, while per-agent storage and linear-memory limits constrain allocations. The monthly included-memory setting is an allowance for accounting; it does not stop agents when consumption reaches that value. + +## Current and monthly usage + +Show the current UTC month: + +```shell copy +golem account usage show +``` + +Show a particular month in `YYYY-MM` form: + +```shell copy +golem account usage show --period 2026-09 +``` + +The response includes an `as-of` timestamp and four dimensions: + +| Dimension | Unit | Meaning | +| --- | --- | --- | +| Compute | GCU | Wasmtime fuel divided by 1,000,000 | +| Memory | GB-seconds | Allocated WebAssembly linear memory over time | +| Durable storage | GB-month | Durable filesystem allocated-byte-time | +| Ephemeral storage | GB-month | Ephemeral filesystem allocated-byte-time | + +Storage uses byte-seconds divided by `1024³ × 730 × 3600`. Both storage fields come from the shared filesystem meter, separated by durable and ephemeral agent usage. Memory measures allocated linear memory, not process RSS or host memory. + +Each dimension also reports whether metering was `enabled`, `disabled`, or `unknown`. A zero with metering enabled means no usage was measured. `unknown` means no usage producer has reported that meter's state for the period. + +### Closed-period history + +```shell copy +golem account usage history +golem account usage history --last 12 +``` + +History returns closed UTC months newest first and defaults to six periods. It is **sparse**: months without a stored usage record are omitted. Historical rows use the same units and metering states, but do not snapshot historical plan or limit metadata. + +Use `--account ` or `--account-id ` on usage commands when authorized to inspect another account. + +## Effective limits and overrides + +Show the effective resource limits, their plan defaults, optional overrides, ceilings, and whether the account may configure them: + +```shell copy +golem account limits show +``` + +The three override dimensions are: + +- maximum filesystem storage per agent, in bytes +- maximum WebAssembly linear memory per agent, in bytes +- monthly included linear-memory allowance, in GB-seconds; this is not a runtime cap + +Set one override per command: + +```shell copy +golem account limits set 10737418240 +golem account limits set --max-memory-per-agent 536870912 +golem account limits set --monthly-memory-gb-seconds 1000000 +``` + +The storage value is positional. Choose values after inspecting `limits show`: memory and monthly-memory overrides must be between the plan default and ceiling, inclusive, while storage overrides must not exceed their ceiling. The effective value is the override or plan default capped by that ceiling. The service rejects setting an override when its dimension is not user-configurable. + +Clear one override per command: + +```shell copy +golem account limits unset --storage +golem account limits unset --max-memory-per-agent +golem account limits unset --monthly-memory-gb-seconds +``` + +For storage, `golem account limits unset` without a flag is also accepted, but the explicit `--storage` form is clearer. + +The CLI creates non-expiring overrides. The REST API also accepts an expiry timestamp, but setting an expiry requires an administrator token. Once an override expires, it no longer participates in the effective limit; the plan default and ceiling apply again. + +## Usage, quotas, and operator metering + +These concepts are independent: + +- **Account usage** is the monthly, user-facing billing measurement shown by `golem account usage`. +- **Resource limits** include the enforced per-agent storage and linear-memory caps shown by `golem account limits`; the same command also shows the monthly included-memory allowance. +- **Application quotas** are capability-style concurrency or resource controls used by agents. See [Quotas](/next/develop/quotas). +- **Operator metering switches** configure which worker-executor dimensions a deployment measures and exports. They do not enable enforcement. See [Resource metering](/next/operate/resource_metering). + +Self-hosted operators can enforce filesystem and memory controls while usage metering is disabled. Conversely, an enabled meter reports consumption; it does not by itself establish a quota or resource limit. diff --git a/docs/src/content/next/cli/app-manifest.mdx b/docs/src/content/next/cli/app-manifest.mdx index 48694ca53c..34112005a8 100644 --- a/docs/src/content/next/cli/app-manifest.mdx +++ b/docs/src/content/next/cli/app-manifest.mdx @@ -1,248 +1,154 @@ # Golem Application Manifest -The _Golem Application Manifest_ document format is used by `golem`, usually stored in files named `golem.yaml`, and they are the intended way to define, build, deploy and manage Golem Applications and Environments. +The Golem Application Manifest, normally named `golem.yaml`, defines how the CLI builds, deploys, and manages an application and its environments. See the [Application Manifest reference](/next/app-manifest) for its complete schema. -The Application Manifest uses _YAML_ format, see the [reference](/next/app-manifest) for more information on the schema and for the field reference. +## Manifest discovery -# Application Manifest Quickstart +For manifest-aware commands, the CLI searches the current directory and its parents for the root `golem.yaml`. It then loads the files selected by that manifest's `includes` and merges the documents. Path bases are field-specific; for example, component artifact paths are resolved from the component directory. -Application manifest documents can be explicitly passed as `golem` arguments, but the recommended way to use them is with _auto discovery_ mode: when `golem` is called with an application manifest compatible command it keeps searching for `golem.yaml` documents in current working directory and parent directories. Once it found the top level `golem.yaml` document, more documents are searched using the [includes](/next/app-manifest#fields_includes) field. +Select a different root explicitly with: -Once all manifest documents are found, the paths in them are resolved based on their directory, and then the documents are merged. For the field specific merge rules see the [field reference](/next/app-manifest#field-reference). +```shell copy +golem --app-manifest-path path/to/golem.yaml +``` + +Use `--disable-app-manifest-discovery` when you intentionally want profile-only operation without a discovered application. + +## Create an application from templates + +List current templates, or filter them by language or name: + +```shell copy +golem templates +golem templates rust +``` + +Create a Rust application with the default Rust agent template: + +```shell copy +golem new myapp --template rust --component-name myapp:counter +``` + +For a single-component Rust application, the generated application has this shape: + +```text +myapp/ +├── Cargo.toml +├── golem.yaml +└── src/ + ├── counter_agent.rs + └── lib.rs +``` -## Using component templates +The generated Rust source uses the current `golem-rust` macros: + +```rust +use golem_rust::{agent_definition, agent_implementation, endpoint}; + +#[agent_definition(mount = "/counters/{name}")] +pub trait CounterAgent { + fn new(name: String) -> Self; + + #[endpoint(post = "/increment")] + fn increment(&mut self) -> u32; +} +``` -Golem projects can be created with `golem new` command. This creates a new application that may consist of multiple components. To add a new component to an existing application, run `golem new` again from within the application directory with the `--component-name` option. E.g.: let's add a new _rust_ and _ts_ component in a new and empty directory: +To add another component, run `golem new` in the existing application: -```shell -golem new --template rust --component-name app:component-a myapp +```shell copy cd myapp -golem new --template ts --component-name app:component-b . +golem new . --template ts --component-name myapp:frontend ``` -When using the `golem new` command, it will create: +When a second component is added to an application whose first component lives at the root, the CLI proposes a multi-component layout upgrade. For Rust, it moves that component's `Cargo.toml`, `src`, and existing `target` directory into the component directory before generating the new component. In this example the original component moves under `counter/`. Review and confirm the plan before the files are moved. -- common directory for the given language (`common-rust` and `common-ts`): -- this directory contains the languages specific _Application Manifest Template_, which defines how to -build the components -- can be used for shared subprojects -- might contain other shared configurations -- directory for components for the given language (`components-rust` and `components-ts`) +The default Rust and TypeScript templates both declare `CounterAgent`, and duplicate agent-type names across components are rejected. Before building this two-component example, rename the Rust type in `counter/src/counter_agent.rs` by updating the trait and its implementation together: -Let's rename one of the generated agents, so they are unique for the deployment. In `components-ts/app-component-b/src/main.ts` change: +```diff +-pub trait CounterAgent { ++pub trait OtherCounterAgent { +``` ```diff -- class CounterAgent extends BaseAgent { -+ class OtherCounterAgent extends BaseAgent { +-impl CounterAgent for CounterImpl { ++impl OtherCounterAgent for CounterImpl { ``` -Now that we added our components, let's use the `golem` command list our project metadata: - -```shell -$ golem - -Golem Command Line Interface - -Usage: golem [OPTIONS] - -Commands: - new Create a new application, component, or agent - templates List or search application templates - build Build all or selected components in the application - generate-bridge Generate bridge SDK(s) for the selected agent(s) - repl Start REPL for a selected component - deploy Deploy application - clean Clean all components in the application or by selection - update-agents Try to automatically update all existing agents of the application to the latest version - redeploy-agents Redeploy all agents of the application using the latest version - list-agent-types List all the deployed agent types - exec Execute custom, application manifest defined commands - environment Manage environments - component Manage components - agent Invoke and manage agents - api Manage API gateway objects - plugin Manage plugins - profile Manage global CLI profiles - server Run and manage the local Golem server - cloud Manage Golem Cloud accounts and projects - agent-secret Manage Agent Secrets - retry-policy Manage Retry Policies - resource Manage quota resource definitions - completion Generate shell completion - help Print this message or the help of the given subcommand(s) - -Options: - -F, --format - Output format, defaults to text, unless specified by the selected profile - -E, --environment - Select Golem environment by name - -L, --local - Select "local" environment from the manifest, or "local" profile - -C, --cloud - Select "cloud" environment from the manifest, or "cloud" profile - -A, --app-manifest-path - Custom path to the root application manifest (golem.yaml) - -X, --disable-app-manifest-discovery - Disable automatic searching for application manifests - -P, --preset - Select custom component presets - --profile - Select Golem profile by name - --config-dir - Custom path to the config directory (defaults to $HOME/.golem) - -Y, --yes - Automatically answer "yes" to any interactive confirm questions - --show-sensitive - Disables filtering of potentially sensitive use values in text mode (e.g. component environment variable values) - --dev-mode - Enable experimental, development-only features - --template-group - Switch to experimental or development-only template groups - -v, --verbose... - Increase logging verbosity - -q, --quiet... - Decrease logging verbosity - -h, --help - Print help - -V, --version - Print version - -Application environments: - cloud - Selected: no - Server: cloud - builtin (https://release.api.golem.cloud) - Presets: release - - local - Selected: yes - Server: local - builtin (http://localhost:9881) - Presets: debug - -Application components: - app:component-a - Selected: yes - Source: /Users/noise64/workspace/golem-demo/myapp/components-rust/app-component-a/golem.yaml - Layers: rust, rust[debug], app:component-a - - app:component-b - Selected: yes - Source: /Users/noise64/workspace/golem-demo/myapp/components-ts/app-component-b/golem.yaml - Layers: ts, app:component-b - -Application API definitions: - app-component-a-api@0.0.1 - app-component-b-api@0.0.1 - -Application API deployments for environment local: - subdomain: myapp -> myapp.localhost:9006 - app-component-a-api - app-component-b-api - -Application custom commands: - cargo-clean - ts-npm-install +Also rename the `CounterAgent` key to `OtherCounterAgent` under `httpApi.deployments.local[].agents` in the generated root `golem.yaml`. + +## Inspect the merged application + +Run `golem` without a subcommand inside an application to show the selected application, environment, server, components, HTTP and MCP deployments, and custom commands after manifest merging and directory-based component selection. + +Use source tracing when you need to see which manifest layer supplied a component property: + +```shell copy +golem component manifest-trace ``` -Deployment domains shown by the CLI include the manifest intent and the resolved concrete domain. For example, an HTTP API manifest entry such as `subdomain: myapp` is displayed as `subdomain: myapp -> myapp.localhost:9006` with the default local server port. +The current command prints the property trace for every component. -## Starting the local development server +## Start the local server -The `golem` CLI also includes a local development server that can be used to quickly develop, deploy, test and debug Golem applications locally. -To start the server use: -```shell +```shell copy golem server run ``` -The above command will quickly boot the server, and then it will keep logging to the terminal, so let's switch to another terminal to continue with building and deploying our application.. - -## Building and deploying the application - -Because the _ts_ components use npm, we have to use `npm install` before building the components. We can also see that this has a wrapper custom command in the manifest called `npm-install`, and `golem` itself will automatically run it before a build. Let's see an example for both: - -```sh -$ golem exec npm-install -<..> -$ golem build -Collecting sources - Found sources: /Users/<...>/app-demo/common-rust/golem.yaml, /Users/<...>/app-demo/common-ts/golem.yaml, /Users/<...>/app-demo/components-rust/app-component-a/golem.yaml, /Users/<...>/app-demo/components-ts/app-component-b/golem.yaml, /Users/<...>/app-demo/golem.yaml -Collecting components - Found components: app:component-a, app:component-b -Resolving application wit directories - Resolving component wit dirs for app:component-a (/Users/<...>/app-demo/components-rust/app-component-a/wit, /Users/<...>/app-demo/components-rust/app-component-a/wit-generated) - Resolving component wit dirs for app:component-b (/Users/<...>/app-demo/components-ts/app-component-b/wit, /Users/<...>/app-demo/components-ts/app-component-b/wit-generated) -Selecting profiles, no profile was requested - Selected default profile debug for app:component-a using template cpp - Selected default build for app:component-b using template ts -<...> -Linking RPC - Copying app:component-a without linking, no static WASM RPC dependencies were found - Copying app:component-b without linking, no static WASM RPC dependencies were found +The built-in local environment normally connects to this server. Manifest `localServer` settings can change its ports, data directory, and local executor options; direct `server run` flags take precedence. + +## Build and deploy + +Build all components selected by the current directory and manifest: + +```shell copy +golem build ``` -Then we can check that the components are built: +Select build components explicitly with positional names when needed: -```sh -$ ls golem-temp/components -app_component_a_debug.wasm app_component_b.wasm +```shell copy +golem build myapp:counter myapp:frontend ``` -To deploy the application, including all the components, we can use +Deployment always plans the application as a whole: -```sh copy +```shell copy +golem deploy --plan golem deploy ``` -in the application root folder, and the CLI will apply all changes required. - -Note that `golem deploy` will also always implicitly check for source changes, and based on that will automatically build any changed components, so usually we can just run `golem deploy`. - -If we want to only build some components, we can do so by explicitly selecting them with the `--component-name` flag, or we can implicitly select them by changing our current working directory, e.g.: - -```shell -$ cd components-rust -$ golem -<...> -Components: - app:component-a - Selected: yes - Source: /Users/noise64/workspace/examples/app-demo/components-rust/app-component-a/golem.yaml - Template: cpp - Profiles: debug, release - app:component-b - Selected: no - Source: /Users/noise64/workspace/examples/app-demo/components-ts/app-component-b/golem.yaml - Template: ts -<...> -``` +In text format, the plan is a human-readable diff of changed entries. Add `--full-diff` to include unchanged deployment and environment-setup entries. Structured formats can emit a sequence of typed documents; consumers should branch on each document's `$type` rather than assume one fixed JSON object. + +`golem deploy` checks source changes and builds affected components automatically before applying the deployment. + +### Existing agents -Notice how only app:component-a is selected in the above example, the same selection logic is used when building. -On the other hand, deploying _will always deploy the whole application atomically_, so it is ensured that all our agents -are up-to-date. +A normal deployment does not move existing agents to the latest component revision. Choose at most one post-deployment action: -When developing locally, it is often useful to drop all our existing durable agents and have a fresh clean environment -for testing. For this, use: +```shell copy +# Attempt a durable update using the selected mode +golem deploy --update-agents auto -```shell +# Delete and recreate existing agents; state is lost +golem deploy --redeploy-agents + +# Deploy, then delete existing agents of the deployed components; their state is lost golem deploy --reset ``` -## Targeting Golem Cloud, or other environments -By default, most `golem` commands will target the local Golem server. +Both `--redeploy-agents` and `--reset` are destructive. Review the selected environment before confirming them. -To access Golem's own hosted _Golem Cloud_ (or other custom installations), we can simply use the same commands as before -but select the cloud environment with one of the following ways: -```shell -# Explicitly selecting the cloud (or a custom) environment -golem --environment cloud deploy -golem -E cloud deploy +## Select an environment -# Using the convenience cloud flag: +Templates normally define `local` and `cloud` environments. Select by name, convenience flag, or environment variable: + +```shell copy +golem --environment cloud deploy golem --cloud deploy -golem -C build -# Or by using environment variables, which can be useful in CI/CD pipelines: export GOLEM_ENVIRONMENT=cloud golem deploy ``` -For more information see the next seection about [Environments and Profiles](/next/cli/envs_and_profiles). +Without an explicit selection, the manifest's default environment applies. See [Environments and Profiles](/next/cli/envs_and_profiles) for application-qualified and account-qualified environment references and profile-only operation. diff --git a/docs/src/content/next/cli/components.mdx b/docs/src/content/next/cli/components.mdx index 3cd29e9818..5ad4b936c0 100644 --- a/docs/src/content/next/cli/components.mdx +++ b/docs/src/content/next/cli/components.mdx @@ -14,7 +14,7 @@ To create a new component in an existing application directory (previously creat golem new --template --component-name ``` -To see all the available component templates, just run the command without providing one. +List available templates with `golem templates`, or filter them by language, for example `golem templates rust`. This command only modifies the source code of the application, does not create anything on the server. @@ -32,7 +32,13 @@ To build the whole application, use the `build` command without specifying a com golem build ``` -Both commands accept a `--build-profile ` argument. Some of the built-in templates define a separate `release` profile which creates a more optimized version of the components. Build profiles can be fully customized by editing the _application manifest files_. +Select a component preset with `--preset` (`-P`). For example, built-in Rust templates provide a `release` preset for optimized builds: + +```shell copy +golem --preset release build +``` + +Presets can be customized in the application manifest. To deploy **all components** of an application, use: @@ -42,75 +48,60 @@ golem deploy ## Component search -### Using component name and the latest version +### Using a component name and the currently deployed revision -If you want to get the latest version of the component using its name you can you can use the `component get` command. +To get the current deployed revision of a component by name, use the `component get` command. ```shell copy golem component get example:component ``` -### Using component name and specific version +### Using a component name and specific revision -To get a specific version of a component, just pass the desired version number as well: +To get a specific component revision, pass its revision number as well: ```shell copy golem component get example:component 2 ``` -### Getting component list +### Getting the component list -To get all component versions for specific component name you can use `component list` command with a given component name. Note if you are in a component's source directory, the command will automatically list that component's versions. - -```shell copy -golem component list example:component -``` - -To get all component available you can use `component list` command this way: +List components in the selected environment's current deployment: ```shell copy golem component list ``` - - If you want to restrict component search to some specific project on Golem Cloud you can specify - project via `--project` or `--project-name` option. It works for all commands that accept - `--component-name` parameter. - - ## Updating component To update a component just run the `golem deploy` command again. -If you want to trigger an update all agents to the new latest version right after creating this version you can use `--update-agents` option. - -It is possible to change the **component's type** (from durable to ephemeral, or from ephemeral to durable) when -updating the component by changing the manifest file. +To update existing agents after deployment, pass an explicit update mode, for example `golem deploy --update-agents auto`. This is mutually exclusive with `--redeploy-agents` and `--reset`. ## Updating agents -If you want to update all agents you can use `component update-agents` command. +Use `component update-agents` to update durable agents that use an older revision than the selected component's currently deployed revision. -This command gets all agents for the component that are not using the latest version and triggers an update for them one by one: +It triggers updates one by one for the eligible agents: ```shell copy golem component update-agents example:component ``` -The update request is enqueued and processed by the agents asynchronously, `golem` cannot await for the update to finish. +By default, update requests are enqueued and processed asynchronously. Pass `--await` to wait for completion, or `--disable-wakeup` to leave suspended agents asleep until they next wake. Note that automatic agent update is not guaranteed to succeed, if the updated component differs too much from the previous one. -You can use URL or `--component-name` instead. +Omit the positional component name to use directory-based component selection. ## Redeploying agents -During the development of a Golem Component, it is often necessary to quickly rebuild the code, update the component and just restart all the test agents from scratch to test the changes. +During development, it is often useful to deploy new component code and then restart its test agents from scratch. -This is different from updating the agents as they will loose their state, but it can speed up the feedback loop during development. +This is different from updating agents because they lose their state, but it can speed up the feedback loop during development. This workflow is supported by the `component redeploy-agents` command: @@ -118,6 +109,6 @@ This workflow is supported by the `component redeploy-agents` command: golem component redeploy-agents example:component ``` -This command deletes all agents that are not using the latest version of component and re-creates them with the same name, parameters and environment variables. +This command does not build or deploy code. It deletes and recreates **all** agents of the selected component using the currently deployed revision. Agent IDs, environment variables, and configuration are preserved, but agent state is lost. -You can use URL or `--component-name` instead. +Omit the positional component name to use directory-based component selection. diff --git a/docs/src/content/next/cli/envs_and_profiles.mdx b/docs/src/content/next/cli/envs_and_profiles.mdx index e52c948160..33183f09aa 100644 --- a/docs/src/content/next/cli/envs_and_profiles.mdx +++ b/docs/src/content/next/cli/envs_and_profiles.mdx @@ -1,7 +1,7 @@ # Golem CLI Environments and Profiles There are two main ways to interact with _Golem CLI_ in a context: -- using it in a directory that defines a _Golem Application_ along with its _Environments_ using the [Application Manifest](/next/cli/app-manifest) (also see the ) +- using it in a directory that defines a _Golem Application_ and its _Environments_ with the [Application Manifest](/next/cli/app-manifest) - or using it without a manifest, relying on _profiles_ and CLI flags which will specify the _target server_ and _environment_ to use. @@ -21,7 +21,7 @@ can be defined in the `environments` property. ![Golem Application Environments](/images/golem-app-env-deploy.png) Our template will include the built-in `local` and `cloud` environments. The `local` one is configured to be used -wuth `golem server run`, while the `cloud` one is configured to be used with _Golem Cloud_. +with `golem server run`, while the `cloud` one is configured to be used with _Golem Cloud_. Custom environments with custom target servers can also be defined, see the [reference](/next/app-manifest) for more information. @@ -29,7 +29,7 @@ In this mode the following global flags and environment variable can be used to ```shell # Explicitly selecting an environment by name -golem --enviroment +golem --environment golem -E # Using the convenience cloud and local flags: @@ -45,75 +45,37 @@ export GOLEM_ENVIRONMENT= ## Profiles -The _Golem CLI_ can also be used without an Application Manifest, which is useful for supporting dev-ops use cases. -The CLI has built-in profiles for accessing the _local_ and _cloud_ servers, and without a manifest, the `-local` and -`-cloud` flags will select these profiles. +The _Golem CLI_ can also be used without an Application Manifest, which is useful for operational and automation workflows. +The CLI has built-in profiles for accessing the _local_ and _cloud_ servers, and without a manifest, the `--local` and +`--cloud` flags select these profiles. -For commands that also require and _Application Environment_ the longer forms of the `--environment` flag can be used. -Note that the CLI will tell you, if such an environment reference is needed, and about the possible accepted formats, e.g.: - -```shell -$ golem component list -Selected app: -, env: -, server: http://localhost:9881/, profile: local -error: The requested command requires an environment from an application manifest or via flags or environment variables. - -Accepted environment and profile flags and environment variables: - - --environment , -E : - Selects an environment from the current application manifest. - The application is searched based on the current directory. - - If no explicit flags or environment variables are used then the default environment - is selected from the manifest. - - If there is no available application, then a more specific from has to be used. - - - --environment /, -E /: - Selects a server environment from a specific application. The used server is the - current selected manifest environment or the selected profile if the CLI is used - without a manifest. - - - --environment //, -E //: - Selects a server environment from a specific application owned by another account. - The used server is the current selected manifest environment or the selected profile - if the CLI is used without a manifest. - - - --local, -L: - When used with an application manifest then the environment is selected from the - manifest. Without it it selects the built-in local profile. - - - --cloud, -C: - When used with an application manifest then the environment is selected from the - manifest. Without it it selects the built-in cloud profile. - - - --profile: - Selects a different profile then the default one. Only effective when used without - an application manifest. +An unqualified environment name selects an environment from the discovered application manifest: +```shell copy +golem --environment production component list +``` - GOLEM_ENVIRONMENT environment variable: - Alternative to the --environment flag. +Do not combine an unqualified `--environment` with `--local` or `--cloud`. +Qualified references identify a remote application environment, while the profile selects the server used to reach it: - GOLEM_PROFILE environment variable: - Alternative to the --profile flag. +```shell copy +golem --profile cloud --environment myapp/production component list +golem --profile cloud --environment owner@example.com/myapp/production component list ``` +The accepted forms are `APPLICATION/ENVIRONMENT` and `ACCOUNT_EMAIL/APPLICATION/ENVIRONMENT`. Select the profile explicitly when the default profile does not point at the intended server. + ## Logging in to Golem Cloud Golem Cloud requires authentication to access our cloud servers. This happens automatically the first time you run any command that requires authentication, but if you want to trigger authentication, the following command can be used: -```shell -golem cloud account get --cloud -```` +```shell copy +golem --disable-app-manifest-discovery --profile cloud account get +``` -Note that the `--cloud` is required to make sure that the authentication happens against the Golem Cloud servers. +This selects the built-in Cloud profile without manifest discovery. It starts browser authentication when needed and reuses valid cached credentials otherwise. An already selected Cloud profile does not require another selection flag. ## Profile management @@ -131,7 +93,7 @@ Manage global CLI profiles Usage: golem profile [OPTIONS] Commands: - new Create a new global profile, call without for interactive setup + new Create a new global profile, call without for interactive setup list List global profiles switch Set the active global default profile get Show global profile details diff --git a/docs/src/content/next/cli/permissions.mdx b/docs/src/content/next/cli/permissions.mdx index 65302ce459..d8e42f23e1 100644 --- a/docs/src/content/next/cli/permissions.mdx +++ b/docs/src/content/next/cli/permissions.mdx @@ -1,101 +1,146 @@ import { Callout } from "nextra/components" -# Golem CLI Permissions +# Permissions -This page only applies to the hosted **Golem Cloud**. + + These commands operate against the selected server or profile and require the corresponding + account permissions. + + +Golem has three related permission-management surfaces: + +- **API tokens** authenticate automation that calls Golem APIs. +- **Permission shares** grant bounded card authority from one account to another. +- **Cards** carry authority in account and agent wallets. -## Tokens +For the underlying model, see [Permissions and cards](/next/concepts/permissions). -Tokens are API keys that allow accessing **Golem Cloud** APIs from external services. The `golem` CLI allows managing these tokens. -To manage them programmatically, check [the token API](/next/rest-api/token). +## API tokens -### Listing existing tokens +### List tokens + +```shell copy +golem api-token list +``` -The following command lists all the tokens associated with your account: +### Create a token ```shell copy -golem cloud token list +golem api-token new ``` -### Creating a new token +The command prints the token secret exactly once, including in structured output. Store it securely and do not put it in source control or logs. + +By default, the token uses `2100-01-01T00:00:00Z` as its expiry. Set an explicit RFC 3339 timestamp with a UTC offset when a shorter lifetime is appropriate: + +```shell copy +golem api-token new --expires-at 2026-12-31T23:59:59Z +``` + +Timezone-less timestamps and relative durations such as `30d` are not accepted. + +### Delete a token + +Use the token ID shown by `list` or `new`: + +```shell copy +golem api-token delete +``` + +Deleting a token prevents future API authentication with that secret. It does not revoke permission cards. + +## Account permission shares + +A permission share is owned by one account and targets another account by email address. Its lower positive and negative grants define the authority shared with the target. Golem materializes the share as a managed permission-share card. -To create a new token, use the following command: +Permission strings use `class(owner) @ recipient : verb : resource`. For example, this grant lets the recipient view the owner's `shop` application: + +### Create a share ```shell copy -golem cloud token new +golem account permission-share new recipient@example.com deployers \ + --lower-positive 'application(owner@example.com/shop) @ recipient@example.com : view :' ``` +The positional arguments are the target account email and the share name. A share grant's recipient must be `*` or exactly the target account's email. Repeat either grant option to add more than one pattern. Use `--account ` or `--account-id ` to act on another account when your credentials allow it. + +### List and inspect shares + +List shares owned by the selected account: + +```shell copy +golem account permission-share list ``` -New token created with id 08bc0eac-5c51-40a5-8bc6-5c8928efb475 and expiration date 2100-01-01 00:00:00 UTC. -Please save this token secret, you can't get this data later: -64cf566c-ed72-48e5-b786-b88aa0298fb4 + +List shares received by it instead: + +```shell copy +golem account permission-share list --received ``` -Optionally, an expiration date can be specified with `--expires-at`. If not specified, default expiration date is `2100-01-01 00:00:00 UTC`. +Get a share by ID or by its owner-scoped name: -### Deleting a token +```shell copy +golem account permission-share get +golem account permission-share get-by-name deployers +``` -Each token has a **token ID**. Use the `token delete` command to remove a token using it's identifier: +### Update or delete a share ```shell copy -golem cloud token delete 08bc0eac-5c51-40a5-8bc6-5c8928efb475 +golem account permission-share update \ + --name deployment-operators \ + --lower-positive '' + +golem account permission-share delete ``` -## Project sharing +On update, omitting `--name`, `--lower-positive`, or `--lower-negative` preserves that existing value. Supplying a grant option replaces the corresponding list; repeat it to provide the complete replacement list. Updates and deletes use the share's current revision and fail on a concurrent modification rather than overwriting it. + +Every successful update creates a replacement managed card and revokes the previous card and all its descendants, even when only the share name changes. Deleting the share revokes its managed card and descendants without replacement. + +## Inspect and revoke cards -On **Golem Cloud** components are organized into **projects**. +List all managed card kinds owned by the selected account: -### Listing projects +```shell copy +golem card list +``` -Existing projects can be listed using the `project list` subcommand: +When any include flag is present, only the selected kinds are returned: ```shell copy -golem project list +golem card list --include-root --include-permission-shares +golem card list --include-environment-defaults --include-agent-initials ``` -### Adding projects +The account scope flags `--account ` and `--account-id ` are mutually exclusive. -A new project can be created using `project add`. The command expects a project name and description: +To inspect an agent's active wallet instead, use an agent reference. This activates the agent if necessary: ```shell copy -golem project add --project-name "Golem Demo" --project-description "A new project for demonstrating the project feature" +golem card list --agent ``` -When creating components or agents, the project can be specified with the `--project-name` flag. Every user has a **default project** which is used when no explicit project is specified. +Agent wallet listing cannot be combined with account scope or include flags. -### Sharing projects with other accounts +Inspect one card: -Projects can be shared among multiple Golem Cloud accounts. +```shell copy +golem card get +``` -To share a project, use the `share` subcommand: +Revoke a card and all cards derived from it: ```shell copy -golem share --project-name "Golem Demo" --recipient-account-id 08bc0eac-5c51-40a5-8bc6-5c8928efb475 --project-actions ViewWorker --project-actions ViewComponent +golem card revoke ``` -This example shares the "Golem Demo" project with the account identified by `08bc0eac-5c51-40a5-8bc6-5c8928efb475` and grants component and agent **view** permissions for it. +Revocation is destructive and prompts for confirmation unless the global `-Y`/`--yes` option is set. The output lists the card IDs revoked by the cascade. - - Alternatively it is possible to create and manage **project policies** using the `project-policy` - subcommand, and refer to these policies in the `share` command later. - +Permission-share cards, environment-default cards, and system cards cannot be revoked directly. Delete or update a permission share to revoke its managed authority; change the managing environment configuration for an environment-default card. -The following table lists all the **actions** that can be granted to a project. The REST API and permission action names still use "worker" for backwards compatibility. - -| Action | Description | -| --------------------- | --------------------------------------------- | -| `ViewComponent` | List, download and get metadata of components | -| `CreateComponent` | Create new components | -| `UpdateComponent` | Update existing components | -| `DeleteComponent` | Delete components | -| `ViewWorker` | List and get metadata of agents | -| `CreateWorker` | Create new agents | -| `UpdateWorker` | Update existing agents | -| `DeleteWorker` | Delete agents | -| `ViewProjectGrants` | List existing project grants | -| `CreateProjectGrants` | Grant more access for the project | -| `DeleteProjectGrants` | Revoke access for the project | -| `ViewApiDefinition` | View API definitions | -| `CreateApiDefinition` | Create new API definitions | -| `UpdateApiDefinition` | Update existing API definitions | -| `DeleteApiDefinition` | Delete API definitions | + + The CLI does not derive, transfer, or install cards. Applications perform those operations through + the `golem:permissions` guest interfaces. Do not substitute API tokens for runtime permission cards. + diff --git a/docs/src/content/next/concepts.mdx b/docs/src/content/next/concepts.mdx index e824479d09..f862f0e6bc 100644 --- a/docs/src/content/next/concepts.mdx +++ b/docs/src/content/next/concepts.mdx @@ -93,7 +93,9 @@ Idle agents scale to zero. The scheduler spreads active agents across the cluste + + diff --git a/docs/src/content/next/concepts/_meta.js b/docs/src/content/next/concepts/_meta.js index a4ead63268..5da3a5c391 100644 --- a/docs/src/content/next/concepts/_meta.js +++ b/docs/src/content/next/concepts/_meta.js @@ -1,8 +1,11 @@ export default { reliability: "Reliability", agents: "Agents", + "golem-native-tools": "Golem-Native Tools", "worker-gateway": "API Gateway", + "http-routers": "HTTP Routers and Files", "agent-to-agent-communication": "Agent to Agent Communication", "api-definitions": "API Definitions", + permissions: "Permissions and Cards", plugins: "Plugins", } diff --git a/docs/src/content/next/concepts/golem-native-tools.mdx b/docs/src/content/next/concepts/golem-native-tools.mdx new file mode 100644 index 0000000000..3466ecba9f --- /dev/null +++ b/docs/src/content/next/concepts/golem-native-tools.mdx @@ -0,0 +1,110 @@ +import { Callout } from "nextra/components" + +# Golem-Native Tools + +Golem-native tools are typed, separately deployable operations that agents can call through Golem's runtime. Use them when an agent must act on the world under **operator-controlled permissions** and inside an **enforceable capability sandbox**. Unlike an in-process function or framework hook, the runtime can authorize every call, constrain what the implementation can reach, and apply middleware the caller cannot bypass. + + + Here, **Golem-native** describes the tool model. A **native tool** more narrowly means a privileged implementation compiled into the Golem host. Most tools are ordinary WebAssembly components. Golem currently ships no production inventory of built-in or host-native tools. + + +## Why use a tool? + +A tool creates two independent security boundaries: + +1. **Agent permissions** decide whether the caller has authority to perform the operation. +2. **WebAssembly capabilities** decide what the tool implementation can physically access. + +This is stronger than asking agent code to follow a convention. An untrusted or generated agent cannot remove runtime middleware, mint a permission card, add a WIT import, or escape its component sandbox. + +Use a Golem tool when you need one or more of these properties: + +- fine-grained, revocable authority attached to an agent; +- filesystem, network, environment, configuration, or secret access limited by the component's imports; +- durable approval, audit, redaction, quota, or policy middleware; +- a typed client across language and component boundaries; +- a reusable operation published independently of the agent; +- a safe agent-facing projection of an existing CLI or MCP server. + +For a small trusted helper that needs exactly the agent's lifecycle and privileges, a normal library function is simpler. + +## Exposure and authorization are separate + +A call succeeds only when the tool is both **exposed** and **authorized**: + +| Condition | Controlled by | +| --- | --- | +| The tool is visible to the agent | Component, environment, and agent tool bindings in `golem.yaml` | +| The agent may invoke that command | A matching permission card in the agent's wallet | + +A binding does not grant authority, and a permission does not install a tool. This separation lets an operator expose a common catalog while granting each agent only the commands it needs. See [Tool authoring and lifecycle](/next/develop/tool-authoring) for bindings and [Application Manifest](/next/app-manifest) for the complete schema. + +Do not confuse runtime permission cards with **tool release grants**. A release grant lets an environment consume a published tool artifact. A permission-card grant authorizes a running agent operation. A registry-backed tool can require both, in addition to an agent binding. + +## Tool grants and resource grants + +Permissions are opaque, host-maintained cards held in an agent's wallet. Two layers commonly apply to one call: + +- A **tool grant** authorizes invocation of a command, including its command path, options, flags, and positionals. +- A **resource grant** authorizes the operation the implementation performs, such as reading a filesystem path or acting on another agent. + +Authorizing `files read /reports/q3.csv` does not automatically authorize the underlying filesystem read. Conversely, filesystem authority alone does not authorize calling the tool. Tool and middleware instances act with the calling agent's wallet, narrowed by their binding; they do not receive independent ambient wallets. + +Cards can be derived, shared, revoked, and transferred, but derivation is attenuation-only: a child card can narrow authority and expiry, never broaden it. Runtime checks use the invocation's pinned wallet view so recovery and replay reproduce the same authorization decision. + +## Isolation and capability scoping + +Tool and middleware identity is owned per `(calling agent, entity name)` pair and is never shared between agents. Each component-tool invocation runs in a fresh isolated instance under that owner. Calls between an agent, middleware, and a leaf tool cross runtime-managed instance boundaries. + +The component's **WIT imports** are its structural capability declaration: + +- without `wasi:filesystem`, it cannot access a filesystem; +- without socket or HTTP imports, it cannot access the network directly through those WASI interfaces; +- without the Golem agent host import, it cannot read agent configuration; +- without the secret-reveal import, it cannot materialize secret plaintext. + +When an import is present, the host aliases the owning agent's corresponding view and may narrow it with tool or middleware binding policy. Current bindings can narrow configuration and secret-key access and allow or deny filesystem access. Path- and destination-specific authorization belongs to resource permissions; per-tool path and host binding allowlists are not currently available. A binding cannot grant access the agent did not have. The effective capability is always an intersection, never a union. + +This structural boundary complements middleware. Imports answer “can this component perform this class of effect at all?” Middleware and permission cards answer “may this invocation perform this particular operation?” + +### Privileged host tools + +A host-native tool is the exception to WIT-import scoping because its implementation runs inside the host. It still uses the same typed metadata, permission checks, bindings, middleware chain, durable call model, streaming contract, and narrowing policy. Host implementation is not permission bypass: calls through Golem's configuration and resource helpers are checked against the effective agent scope. + +## Non-bypassable, durable middleware + +Tool middleware is configured by the operator and traversed by the runtime at the `tool-rpc` boundary. Agent code cannot enumerate, reorder, remove, or call around the effective chain. Middleware can: + +- reject or rewrite unsafe inputs; +- require human approval; +- apply rate, cost, or tenant limits; +- audit calls and redact outputs; +- adapt one typed tool surface to another. + +When invoked by a durable agent, middleware participates in that agent's durable execution. A human-in-the-loop approval can suspend for hours and survive suspension or executor restart without application-managed checkpoints. Ephemeral owners do not provide this recovery guarantee. Durability does not replace least privilege: approval middleware is an additional guardrail, not authority to exceed the agent wallet or component capabilities. + +Universal middleware wraps every tool without changing its schema. Monomorphic middleware wraps a known tool shape and may adapt the presented shape. In both cases the runtime owns traversal; calling the next inner layer requires an invocation-scoped handle that cannot skip layers. + +## Secrets remain opaque + +Tool schemas can carry secrets as resource handles rather than strings. Agents and pass-through middleware can forward a handle without seeing plaintext. For example, the external Durable Streams API accepts an opaque handle for authentication without revealing it to guest code. Revealing plaintext requires the reveal WIT import, the applicable binding policy, secret-reveal permission, and an audited host call. + +While a value remains opaque, its serialized representation carries identity rather than plaintext. After reveal, application code must avoid logging or persisting the returned value. See [Configuration and Secrets](/next/develop/config-and-secrets). + +## Typed clients and adapters + +One tool definition describes its command tree, inputs, outputs, declared errors, and streams. Golem uses that metadata for discovery, help, runtime validation, and generated or definition-owned typed clients. A tool authored in one supported language can be called from another without reducing its contract to untyped JSON. + +The command model is intentionally CLI-shaped: nested commands, global options, flags, fixed and variadic positionals, stdin, stdout, structured results, and exit-coded errors. You can therefore wrap `git`, `curl`, or another executable behind a typed, permission-aware surface. MCP imports are projected into the same registry and called through the same typed runtime boundary; the generated client does not embed the MCP endpoint or credentials. + +## Choose the right boundary + +| Boundary | Choose it when | Trade-off | +| --- | --- | --- | +| Direct library call | Code is trusted, local, and should share the agent's process and privileges | No independent binding, permission, sandbox, publication, or runtime middleware boundary | +| Agent RPC | The callee has identity, durable state, or a lifecycle of its own | Heavier stateful abstraction; use agents for entities, not stateless utilities | +| Component tool | You need typed reuse, per-agent isolation, capability scoping, publication, or middleware | Requires a tool component and deployment binding | +| MCP-imported tool | The implementation already exists behind MCP | MCP transport remains external; Golem projects and governs the exposed surface | +| Host-native tool | The operation must use privileged runtime internals | Trusted platform code; not protected by guest WIT imports | + +The common pattern is: keep pure computation in a library, keep durable business entities as agents, and place externally effective operations behind tools. Continue with [Tool authoring and lifecycle](/next/develop/tool-authoring). diff --git a/docs/src/content/next/concepts/http-routers.mdx b/docs/src/content/next/concepts/http-routers.mdx new file mode 100644 index 0000000000..3d20af71be --- /dev/null +++ b/docs/src/content/next/concepts/http-routers.mdx @@ -0,0 +1,434 @@ +import { Callout } from "nextra/components" + +# HTTP routers and file serving + +Use a custom HTTP router when an application needs to own HTTP routing, status codes, +headers, or streaming bodies. Use [code-first endpoints](/next/invoke/making-custom-apis) +when Golem should map HTTP inputs and outputs to typed agent methods. Both can share +a domain. This page describes the behavior and guarantees of routers and file +serving. For SDK examples and deployment instructions, see +[Using HTTP routers and files](/next/invoke/http-routers). + +The [API Gateway](/next/concepts/worker-gateway) (`golem-worker-service`) can serve +these requests in three ways: + +| Behavior | Owner | What runs on a request | +| --- | --- | --- | +| Custom handler | Parameterless ephemeral router agent | A streaming handler invocation | +| Immutable static file | Router's read-only initial files | Gateway reads shared blob storage; no executor call | +| Live file | Ordinary durable agent | Agent initialization/restoration if needed, then a serialized filesystem read | + +## Router identity and mounts + +A router has an application-wide unique agent type name, `http-router` kind, +an empty constructor, ephemeral mode, no snapshots, and one **literal** mount. +`/` is a valid mount; constructor captures such as `/users/{id}` are not router mounts. +Configuration, secrets, dependencies, and initial files use the normal agent-type +provisioning mechanisms. A router may call ordinary agents through generated clients. + +A router can declare at most one streaming handler and one OpenAPI provider, or +neither. Static-only routers need no dummy handler. Provider-only and empty routers +return 404 for ordinary traffic. Handler and provider names are SDK-defined or +configurable, not reserved platform method names. Routers are not ordinary callable +agent clients: use their mounted HTTP interface. Operational inspection can still +show their worker identities. + +The handler binding uses `Any`: a fallback matcher, **not** an HTTP method to send +on the wire. It covers the mount root and descendants. A mount `/app` covers `/app`, +`/app/`, and `/app/x`, but not `/application`. + +## Request routing precedence + +Reserved host endpoints, including OIDC callbacks, webhooks, and domain OpenAPI, +keep their own bindings. For other requests: + +1. A complete code-first route for the actual HTTP method wins. Its results, + including 404 and authentication failures, are final. +2. Otherwise Golem selects the most-specific eligible mount. Routers are eligible + for supported methods; live filesystem mounts only for GET and HEAD. Literal + segments beat parameters, and a longer matching mount beats its prefix. +3. Golem applies the **selected mount's** authentication/session policy and CORS + before any file lookup, agent activation, or handler invocation. +4. For GET/HEAD, it tries that mount's matching file mappings in declaration order. + Only an absent target advances to another mapping. +5. If all mappings miss, a router invokes its handler, if present. Otherwise the + result is 404. Golem does not search a parent mount afterward. + +An exact mapping does not jump ahead of an earlier subtree mapping. File permissions, +symlinks, directories, initialization failures, and storage failures never trigger +fallback. Unmatched methods return 404, not an automatically generated 405. +There is no implicit code-first GET-to-HEAD alias; a framework inside a handler may +provide one. + +## Ordered file mappings + +Both static files and live files use the same `(source, target)` grammar: + +| Source | Target | Meaning | +| --- | --- | --- | +| `/` | `/public/index.html` | Explicit file at the mount root | +| `/logo.svg` | `/public/brand.svg` | Exact public path | +| `/assets/*` | `/public/assets/$1` | Descendants below a filesystem root | +| `/*` | `/public/$1` | All suffixes below this mount | + +Sources are mount-relative paths. The only wildcard is one terminal `/*`; it also +matches an empty suffix. Targets are absolute canonical filesystem paths, not URLs, +and are not percent-decoded. A subtree target must end in `/$1`. Golem compiles +these declarations into structural mappings; `$1` is not a runtime string replacement. + +For example, these declarations first look in an optional override directory: + +```text +1. /assets/* → /overrides/$1 +2. /assets/* → /public/$1 +3. / → /public/index.html +``` + +At mount `/site`, `/site/assets/logo.svg` tries `/overrides/logo.svg`, then +`/public/logo.svg` only if the first target is absent. Repeating a source with a +different target is allowed. Duplicate compiled source-and-target pairs are rejected, +including equivalent percent spellings of a source. + +### Paths and exposure boundaries + +Golem splits the raw path on `/`, then decodes each segment **once**. Malformed +escapes, invalid UTF-8, encoded separators, backslashes, NUL/control characters, +`.`/`..` segments, and interior repeated slashes are rejected as 400 rather than +normalized. Query strings do not participate in file lookup. `+` is a literal plus +in a path; `%252e%252e` names the literal segment `%2e%2e`, not a parent directory. + +One trailing slash is significant. Both `/site` and `/site/` match a root mapping. +Away from the root, `/site/logo.svg/` does not match an exact `/logo.svg` mapping. +A subtree trailing-slash lookup requests a directory: an existing target is forbidden, +and an absent target is a miss. There is **no directory listing, implicit index.html, +or automatic slash redirect**. Map `/` explicitly to serve an index file. + + + Mapping `/*` to `/public/$1` exposes only `/public`, not the rest of the agent + filesystem. It does expose every eligible file below that root. Keep secrets + outside it and protect the mount when files are private. Symlinks are forbidden + even if they point back inside the exposed root. + + +## Immutable router files + +Provision assets as **read-only initial files** for the router type. The manifest +and SDK examples are in [Serving static files](/next/invoke/http-routers#serving-static-files). +Read-write initial files and files created by a handler are not immutable assets. + +Deployment compiles an index scoped to the environment, router type, and exact +component revision, using the upload-time file size and BLAKE3 digest. The gateway +streams a static hit directly from shared initial-file blob storage. It does not +create a router agent or contact an executor, including for HEAD and misses. +An absent index entry can fall through. A referenced blob that cannot be read is a +storage failure, not a miss. See [shared storage configuration](/next/invoke/http-routers#configuring-shared-storage). + +Immutable does not mean the URL is content-addressed. The default is +`Cache-Control: no-cache`, allowing storage with revalidation, not a long-lived +freshness policy. The strong ETag is `"blake3-"`. +No Last-Modified header is emitted. + +## Live durable-agent files + +Use `exposeFiles` (or the SDK's equivalent annotation) on an ordinary, durable, +non-phantom agent's HTTP mount. Every constructor parameter must occur exactly once +as a path capture. Header, query, body, or caller identity cannot choose the agent ID. +For example, `/documents/{owner}` can select the agent whose constructor takes `owner`. + +The first GET/HEAD can create and initialize that agent. Constructor-created files +are immediately serveable; a nonresident agent is restored. File serving invokes +no exported method, but initialization and replay can execute guest code. Concurrent +first requests use the normal single-agent initialization lifecycle. Initialization +failure is a server error, not a missing file. + +A read waits for initialization and is serialized against invocations, mutations, +updates, reverts, and unloads. Metadata and bytes belong to one filesystem generation; +a concurrent overwrite cannot mix old and new bytes in a response. The guard lasts +until completion, error, or consumer drop; slow downloads can delay that agent's +other work. HEAD releases after metadata. Cancelling a read does not roll back or +cancel shared initialization. + +The selected deployment controls **what paths are exposed**, while the agent's normal +current lifecycle revision supplies the contents. Serving does not roll the agent +back to the deployment revision. Later requests can see an updated file; the policy +does not automatically expand after an agent update. + +Live files use `Cache-Control: no-store` and emit neither ETag nor Last-Modified. +Golem does not hash mutable files into strong validators. See the +[timeout configuration](/next/invoke/http-routers#setting-timeouts-and-resource-budgets) for the +serving deadline and current resource-limit behavior. + +## File HTTP reference + +Golem handles file response headers and preconditions for you. These rules apply +to both static and live mappings unless noted otherwise. + +Full GET returns 200 with Content-Type (from the target extension), Content-Length, +`Accept-Ranges: bytes`, and `X-Content-Type-Options: nosniff`. Unknown extensions use +`application/octet-stream`. File serving does not sniff contents, negotiate formats, +or automatically compress them. HEAD applies the same preconditions and metadata +but sends no body and ignores Range/If-Range. + +Preconditions run only after a file is found and authorized: + +| Request header | Immutable file | Live file | +| --- | --- | --- | +| `If-Match` | Strong tag comparison; failure is 412 | `*` succeeds; a tag list fails with 412 | +| `If-None-Match` | Weak comparison; a match, including `*`, returns 304 | `*` returns 304; a tag list does not match | +| Date preconditions | Ignored | Ignored | +| `If-Range` with Range | Only an exact strong ETag match permits 206 | Any If-Range makes the response a full 200 | + +If-Match runs before If-None-Match, then Range. Malformed entity-tag lists return +400. A file 304 includes Cache-Control and the immutable ETag when available, with +no body or Content-Length. A 412 has an empty body and Content-Length: 0. + +GET supports a single byte range: `bytes=2-5` (inclusive), `bytes=2-`, or +`bytes=-4`. A satisfiable range returns 206 with Content-Range and the selected +Content-Length. Oversized end offsets and suffix lengths are clipped to file length. + +An empty file, a start at or beyond EOF, or a zero suffix is unsatisfiable: 416, +`Content-Range: bytes */`, and an empty body. Malformed/overflowing ranges, +end-before-start, unknown units, and multiple ranges or Range fields are ignored, +returning full 200. Multipart byte ranges are not supported. + +## Streaming handler reference + +SDK adapters implement this contract. Use it when writing a raw handler, integrating +a framework, or diagnosing framing and status errors. + +The language-neutral request contains `method`, `scheme`, `authority`, `path`, +optional `query`, ordered byte-valued `headers`, and a stream of byte chunks as +`body`. The response contains a 16-bit `status`, the same header representation, +and a streaming body. + +- The method is case-sensitive, including extension methods. `Any` is only metadata. +- The path is the original full public path, not a mount-stripped or decoded path. + Query excludes `?`; absent query and empty query are distinct. Repeated keys, + ordering, `+`, and percent escapes are preserved. +- Header names are lowercase. Every field occurrence and byte value is preserved, + with order preserved among occurrences of the same name. Cross-name ordering is + not guaranteed. In particular, do not comma-fold Set-Cookie. +- Bodies are transfer-decoded but not content-decoded. Chunk boundaries are not + semantic; empty chunks are not EOF. Stream failure and cancellation are not EOF. +- Host/authority, forwarding, hop-by-hop headers, framing, and Expect handling belong + to the host. Do not generate transfer framing in the handler. A supplied + Content-Length must be one valid length and match the emitted bytes. + +Backpressure is applied through bounded stream/session credits in both directions. +Do not collect a body simply to forward it or calculate its length. A handler can +return a response before upload EOF. Dropping unused input stops its delivery without +cancelling the response. The host bounds abandoned uploads rather than waiting forever. + +Only final statuses 200–599 are accepted. HEAD and statuses 204/205/304 dispose the +response producer without polling body items. Content-Length is removed for 204, +forced to zero for 205, and may describe the corresponding representation for HEAD +or 304. Disposing an unused body is not cancellation of a successful invocation. + +Body EOF alone is not invocation success. Golem waits for explicit session success +before completing the public response. It withholds the terminal streaming frame, +or the last byte of a positive Content-Length response; for HEAD/bodyless/zero-length +responses it delays head commitment. This prevents a later invocation failure from +appearing as a successful complete response. + +### Cancellation and ephemeral recovery + +An external disconnect/reset cancels the active invocation and its streams. +Cancellation is idempotent and does **not** roll back effects already performed, +including calls to durable agents. Request EOF, normal response completion, and +host disposal of an unused upload are not client cancellation. + +After internal transport loss, Golem can reattach the **same** surviving invocation +using its identity, execution epoch, acknowledgements, and output cursors. It +deduplicates transport frames; it does not rerun the handler. Recovery is bounded +by retained state and the [retry budget](/next/invoke/http-routers#setting-timeouts-and-resource-budgets). + +Loss of the ephemeral executor or the serving gateway fails the HTTP exchange. +Golem never transparently creates a replacement invocation, even for GET. A client +retry is a new request and may repeat effects. Cancellation is best effort if the +executor is unavailable. Do not rely on language finalizers running after a host-level +Wasm interruption. + +### TypeScript Web adapter limitations + +TypeScript Web Request/Response/Headers are normalized views, not a lossless replacement +for the canonical envelope. Web APIs may normalize methods and URLs, combine fields, +or reject methods/statuses. Use `implementRaw` for the canonical request/response, +the Web handler's context for its original request, and `withRawHeaders` for ordered +byte-valued response fields. See [Working with headers](/next/invoke/http-routers#working-with-headers). + +### Effect adapter lifecycle and limitations + +The Effect adapter runs an application factory inside each request's scope and +keeps that scope open through response consumption. Acquired resources are released +on completion, failure, or local Effect interruption. HEAD and 204/205/304 do not +start body streams, so cleanup for eagerly acquired resources must belong to the +request scope, not only to a stream's `ensuring` finalizer. + +Effect's framework request uses a mount-relative URL. `HttpRouter.request` exposes +the original canonical envelope, including the full public path and the distinction +between absent and empty query strings. Both views share one body reader. The +`text`, `json`, and `arrayBuffer` accessors buffer under Effect's +`HttpIncomingMessage.MaxBodySize`; using the request stream does not collect it. + +Effect `Respondable` failures retain their HTTP responses, including `HttpApi` +validation errors. Router misses and request parsing failures become 404 and 400 +responses. Other application failures remain invocation failures. The adapter +does not support response `FormData`, non-`Uint8Array` raw bodies, or WebSocket upgrades. + + + Local Effect interruption runs and awaits finalizers, but host-level Wasm + interruption does not deliver cooperative cancellation to JavaScript. The runtime + also cannot interrupt a response producer while its first request for a body + chunk is still pending. Do not assume Effect finalizers run on every client + disconnect or host interruption. + + +## Authentication, CORS, and failures + +Declare authentication/session and CORS on the selected mount. A child mount does +not inherit protection from its parent, and a handler cannot override host CORS. +Golem authenticates before looking up files or activating an agent; an auth failure +cannot fall through to a less-protected mount. Unsafe paths are rejected even before +route selection/authentication. Do not treat CORS as access control for non-browser clients. + +Browser preflight (`OPTIONS` with Origin and Access-Control-Request-Method) uses host +CORS for the requested method/path and never invokes a handler or opens a file. +Plain OPTIONS uses normal routing. Mount CORS also applies to host-generated outcomes. +The separately hosted [domain OpenAPI endpoints](#openapi-generation) are public; +mount protection does not make the API description private. + +| Outcome | Status before response commitment | +| --- | --- | +| Unsafe path, malformed head/framing or entity-tag list, invalid constructor input | 400 | +| Authentication/session failure | Selected security policy's rejection or login redirect; no fallback | +| File permission failure, symlink, directory/non-regular target | 403 | +| Unknown domain, no matching behavior, exhausted mappings without handler | 404 | +| Failed If-Match | 412 | +| Unsatisfiable single byte range | 416 | +| Unsupported Expect value | 417 | +| Initialization, host, or storage failure (including missing indexed blob) | 500 | +| CONNECT, TRACE, upgrade request, unsupported transfer coding | 501 | +| Guest trap, invalid response head, stream/session failure, failed recovery | 502 | +| Serving exchange deadline | 504 | + +After commitment, a failure aborts/resets the stream (or closes HTTP/1); it cannot +send a second status, append an error JSON body, or manufacture successful EOF. +Request/response bodies must not be logged. Keep diagnostics free of secrets and +internal filesystem paths. + +Custom routers do not support WebSockets, tunnels/upgrades, guest informational +responses, response trailers, or server push. File mappings do not support directory +listing, implicit index lookup, symlinks, multipart ranges, or mutable strong validators. + +## OpenAPI generation + +A router can contribute an OpenAPI **3.1.0** document through a parameterless, +non-streaming provider method. Golem validates the document, prefixes its paths with +the router mount, and merges it with code-first operations for the same domain. +The document describes routes; it does not install them in the handler. See +[Publishing OpenAPI](/next/invoke/http-routers#publishing-openapi) for provider examples. + +Providers run lazily when the domain description is requested, not at deployment or +on ordinary application requests. They receive internal system authorization and +an anonymous agent principal, not the requester's headers, session, or identity. +Normal router configuration, dependencies, and secrets remain available. + + + The domain's JSON and YAML OpenAPI endpoints are public host endpoints. Mount + authentication does not protect them. Never include secrets or private operational + data in a provider document. + + +### Accepted documents + +Check framework-generated descriptions against this subset before returning them +from a provider. + +Providers return a UTF-8 JSON string with `openapi`, valid `info`, and `paths`. +Optional root fields are `components`, `tags`, `security`, `servers`, `externalDocs`, +and `x-*` extensions. Golem validates then discards provider `info` and root `servers`: +it owns the merged document's identity, and the final server URL uses the deployment's +configured domain and scheme, not request forwarding headers. + +The supported subset excludes: + +- Path- and operation-level `servers`, even empty arrays; root `webhooks` and + `jsonSchemaDialect`. +- Callbacks, callback operations, component `pathItems`, and Path Item `$ref`. +- External/relative references, fragment anchors, dangling pointers, and example + `externalValue`. Golem never fetches referenced documents. +- Schema `$id`, `$anchor`, `$dynamicAnchor`, `$dynamicRef`, and nested `$schema`. + Schema Objects use the default OpenAPI 3.1 dialect. + +References must be local JSON Pointers rooted at `#/components/` or `#/paths/`, +resolve within that provider document, and have the expected target kind. Recursive +schemas are allowed without expansion. `operationRef` must point to a concrete local +operation; discriminator mappings must be explicit local schema pointers. These +rules apply at semantic keyword positions, not arbitrary data in examples, defaults, +const/enum values, or extensions. + +Operations may use get, put, post, delete, options, head, patch, and trace. There is +no OpenAPI `any` operation; describing trace does not enable the unsupported TRACE +transport method. Supported component kinds are schemas, responses, parameters, +examples, requestBodies, headers, securitySchemes, and links. + +### Paths, security, and conflicts + +At mount `/site`, a provider path `/echo` becomes `/site/echo`, and `/` becomes +`/site`. At mount `/`, paths are unchanged. Non-root trailing slashes stay distinct. +Golem rewrites local path references and `operationRef` pointers, including those +inside components, but does not prefix or rename component names. + +Root provider security is applied to operations without their own security. +An explicit `security: []` removes the provider requirement, **not** the mount's +requirement. Golem combines mount requirements with each operation's alternatives, +so a provider cannot weaken host authentication. The merged document has no root +security requirement that could leak across providers. + +Generated operations are merged first, then providers ordered by mount, router type +name, and component ID. There is no last-writer-wins behavior, automatic renaming, +or partial result. These conflicts fail the entire generation: + +- Duplicate method and equivalent path, even with identical definitions. +- Equivalent path templates with different parameter names, such as `/a/{x}` and + `/a/{y}`, even when methods differ. +- Conflicting path-item summary, description, or extensions. Shared path-item + parameter lists must be deeply equal, treating absent and empty as equal. +- Conflicting components under the same kind/name, tags under the same name, or + root extensions/externalDocs. Equal values are deduplicated. +- Empty or duplicate `operationId` values. IDs are optional, but any present ID + must be globally unique across generated and supplied operations. Link IDs must resolve. + +Object key order does not affect equality; array order does. Output object keys are +sorted and arrays retain their semantic order. + +### Caching and limits + +| Limit | Value | +| --- | --- | +| Provider UTF-8 JSON text | 1 MiB | +| JSON nesting | 64 containers | +| Merged compact JSON | 8 MiB | +| Concurrent provider calls per generation | 8 | +| Cached documents per gateway | 256, LRU eviction | + +Exactly-at-limit sizes are accepted. Invalid JSON, duplicate keys, invalid Unicode, +non-object roots, and unsupported semantics fail validation. + +Concurrent JSON/YAML requests for the same input snapshot share one generation. +The cache fingerprint includes the configured origin, deployment/environment, +routes, relevant method/provider descriptors, and security, but not requester identity. +Successful documents have **no time-based expiry**; failures are not cached. +Changed route snapshots get a new key. An admitted request may finish with its old +snapshot, and caches are process-local. Mutable secret changes alone do not provide +a document freshness guarantee. Providers should describe the deployed schema +rather than changing application data. + +There is no provider-specific or whole-generation timeout or global generation +admission limit. A disconnected HTTP waiter detaches without cancelling the shared +generation. The HTTP session exchange deadline does not apply to providers, and +ingress timeouts alone do not bound their execution. + +Provider invocation, output, validation, size, reference, security, or merge failures +return **502** at the description endpoint. Normal handler and file traffic does +not invoke the provider and remains available. diff --git a/docs/src/content/next/concepts/permissions.mdx b/docs/src/content/next/concepts/permissions.mdx new file mode 100644 index 0000000000..ed3d6739e4 --- /dev/null +++ b/docs/src/content/next/concepts/permissions.mdx @@ -0,0 +1,83 @@ +import { Callout } from "nextra/components" + +# Permissions and cards + +Golem uses **permission cards** to carry authority between accounts, environments, and agents. A card is a durable capability: it describes which operations its holder may perform, who the authority belongs to, which recipients may use it, and which resources it covers. + +Cards are different from [API tokens](/next/cli/permissions#api-tokens). An API token authenticates a client to Golem's management API. A permission card authorizes an operation inside Golem's permission model. + +## Cards, permissions, and wallets + +A card contains permission patterns. Each pattern has five parts: + +- a permission class, such as an account, environment, agent, or filesystem operation +- the **owner** whose resource is being accessed +- the **recipient** allowed to exercise or receive the authority +- a verb describing the operation +- a resource pattern restricting what the operation applies to + +Positive and negative grants form the card's lower and upper bounds. Derivation may narrow authority, but cannot create authority outside its parents' bounds. + +A **wallet** is the set of cards currently held by an account, application, or agent. For an agent invocation, Golem pins the wallet version and card IDs used by that invocation. This gives durable execution a stable authority snapshot even if the live wallet changes later. + + + For resources exposed through deployment bindings, such as tools, exposure and permission are + separate requirements. A binding does not grant permission, and permission does not create a + missing binding. + + +## Ownership and recipients + +The **holder** identifies a wallet: supported holder kinds are accounts, applications, and agents. Wallet membership is separate from ownership of a guest handle; deriving a card returns a handle without installing the card in a wallet. The holder and the permission owner are also different concepts: an agent can hold a card granting access to a resource owned by another account. + +Recipient patterns restrict where authority can flow. They can identify: + +- an account +- all environments or agents in an account +- all environments in an application, or one environment +- all agents in an application, environment, or component +- a particular agent type +- a component's external-tool owner +- any recipient (`*`) + +For installation, at least one lower-positive, upper-positive, or upper-negative grant must cover the target agent. Only grants whose recipient pattern covers the holder contribute to that holder's effective permissions; matching lower-negative grants alone do not make a card installable. + +## Where cards come from + +Golem manages several kinds of cards: + +- **account root cards** establish an account's root authority +- **permission-share cards** represent authority shared with another account +- **environment-default cards** supply default authority for an environment +- **component or agent-initial cards** are produced from deployment and agent-type configuration +- **runtime-derived cards** are created durably by an agent from authority already available to it + +The CLI can filter account root, permission-share, environment-default, and initial cards when listing an account. Runtime-derived cards retain their parent IDs, so their authority and revocation lineage remain auditable, but account card listing does not enumerate them. + +## Derivation, transfer, and installation + +Agents use the `golem:permissions` guest interfaces to work with cards: + +1. **Derive** a persistent child with `derive` from one explicit parent, or with `derive-from-wallet` from one compatible card selected from the active wallet. `derive-scope` instead creates an invocation-local, in-memory card from one or more persistent or scope parents. Every child must be no more permissive than its parent authority. +2. **Transfer** the resulting owned card handle through a component-model call. Permission-card handles are affine: transferring a handle consumes the sender's handle, so it cannot be copied and reused. +3. **Install** a persistent card into a target agent's wallet. Installation currently supports agent targets only and consumes the supplied handle. + +Deriving a card does not install it. Transfer moves an owned handle between components; installation changes the target agent's wallet. Installing a concrete card that is already in the caller's wallet removes it from that source wallet before delivery. Installing a polymorphic card materializes a child and retains the template in the source wallet. + +Scope cards cannot be persisted, installed, or transferred as ordinary persistent card snapshots. A persistent derivation is recorded by Golem before the guest receives its handle. + +## Revocation and expiry + +Cards may expire, and expired cards fail validation. Revoking a card revokes its descendants as well, because a child cannot outlive the authority from which it was derived. You can revoke eligible cards from an agent through the guest API or administratively with [`golem card revoke`](/next/cli/permissions#inspect-and-revoke-cards). System cards and cards managed by permission shares or environment defaults cannot be revoked directly; change their managing resource instead. + +A new attempt to revoke an already or concurrently revoked card returns `card-revoked`, even though the desired state already holds. Replaying a completed revocation reproduces its recorded result. An invocation already pinned to a wallet still revalidates card status where the permission operation requires it; revocation is not a way to rewrite previously recorded execution. + +## Replay and failures + +Persistent derivation, installation, and revocation participate in Golem's durable execution protocol: mutation results and audit events are written to the operation log. During replay, Golem reproduces the recorded result instead of performing the external mutation again, and verifies that the replayed card and wallet generation match the recorded event. Ordinary handle transfer travels in the surrounding durable call's value; it is not a separate audited card mutation. + +Permission operations return typed failures for conditions such as invalid grants or recipients, insufficient parent authority, a missing or changed wallet parent, unsupported installation targets, recipient mismatch, and expired, missing, or revoked cards. A failed installation does not partially add a card to the target wallet. Runtime and infrastructure failures can still fail the invocation according to the normal [durability and retry model](/next/concepts/reliability). + +## Administration + +Use the [permissions CLI guide](/next/cli/permissions) to manage API tokens and account permission shares, inspect account or agent cards, and revoke a card. Card derivation, transfer, and installation are application operations exposed through the guest permission APIs, not card-creation CLI commands. diff --git a/docs/src/content/next/concepts/plugins.mdx b/docs/src/content/next/concepts/plugins.mdx index 3059a7d9f9..37748f1169 100644 --- a/docs/src/content/next/concepts/plugins.mdx +++ b/docs/src/content/next/concepts/plugins.mdx @@ -16,14 +16,14 @@ The built-in `golem-otlp-exporter` plugin is an example of a production oplog pr ## Plugin configuration -Plugins can have **configuration** in the form of key-value pairs, which are customizable for each **plugin installation**. Golem sends these configuration values to the plugins when they are invoked. +Plugins can have **configuration** in the form of key-value pairs, which are customizable for each **plugin installation**. Golem sends these configuration values to the plugin when it is invoked. ## Plugin lifecycle -Plugins are first **defined** using Golem's Plugin API (using the REST API, CLI or the Console). Each defined plugin is identified by a _name_ and a _version_. In _Golem Cloud_ plugins are defined per account. +Plugins are first **registered** through the REST API, CLI, or Console. Each registered plugin has a globally unique ID and an account-scoped _name_ and _version_. -Defining a plugin does not immediately make that plugin used by Golem. To make use of a plugin, it must be **installed** to a **component**. This can be done either when a component is created, or later using the Component API. In _Golem Cloud_ plugins can also be installed to _projects_, in which case each new component created in the given project will have those plugins installed. +Registering a plugin does not make it active. Before installation, the plugin must be granted to the target environment, even when the plugin and environment belong to the same account. Declare plugin installations on components or agent types in `golem.yaml`; `golem deploy` reconciles those installations. -Installing a plugin to a component creates a new **component version**, similarly how updating a component's WASM file does. This guarantees that already running agents are not affected by the plugin installation process. To make an existing agent use of an installed plugin, the agent must be **updated** to the new component version. +Changing a component's plugin installations creates a new component revision, just like updating its Wasm. Existing agents are not changed automatically; update them to the new revision when they should use the new installation set. Oplog processor plugins can also be **activated** and **deactivated** on a running agent dynamically using the Agent API. This is an advanced feature which allows the user to temporarily pause an effect of a plugin for a given agent. diff --git a/docs/src/content/next/deploy.mdx b/docs/src/content/next/deploy.mdx index 16cb739a19..1e157a09b2 100644 --- a/docs/src/content/next/deploy.mdx +++ b/docs/src/content/next/deploy.mdx @@ -18,21 +18,15 @@ The services are using the following storage backends: The following sections provide a description of each service. -### Cloud Service +### Registry Service -[cloud-service](https://github.com/golemcloud/golem/tree/main/cloud-service) is responsible for storing entities like projects and accounts in RDB. +[golem-registry-service](https://github.com/golemcloud/golem/tree/main/golem-registry-service) manages accounts, applications, environments, components, plugins, tools, policies, and related registry data in the relational database and blob storage. -See also: [configuration](https://github.com/golemcloud/golem/blob/main/cloud-service/config/cloud-service.toml), [environment variables](https://github.com/golemcloud/golem/blob/main/cloud-service/config/cloud-service.sample.env), [docker image](https://hub.docker.com/r/golemservices/cloud-service) - -### Component Service - -[golem-component-service](https://github.com/golemcloud/golem/tree/main/golem-component-service) is a component registry/management service. The service is using RDB and Blob storage as data storage. - -See also: [configuration](https://github.com/golemcloud/golem/blob/main/golem-component-service/config/component-service.toml), [environment variables](https://github.com/golemcloud/golem/blob/main/golem-component-service/config/component-service.sample.env), [docker image](https://hub.docker.com/r/golemservices/golem-component-service) +See also: [configuration](https://github.com/golemcloud/golem/blob/main/golem-registry-service/config/registry-service.toml), [environment variables](https://github.com/golemcloud/golem/blob/main/golem-registry-service/config/registry-service.sample.env), [docker image](https://hub.docker.com/r/golemservices/registry-service) ### Worker Service -[golem-worker-service](https://github.com/golemcloud/golem/tree/main/golem-worker-service) provides APIs and [API Gateway](/next/concepts/worker-gateway) functionality for agents and acts as a routing service for worker executors. The service uses an RDB as data storage. +[golem-worker-service](https://github.com/golemcloud/golem/tree/main/golem-worker-service) provides APIs and [API Gateway](/next/concepts/worker-gateway) functionality for agents and acts as a routing service for worker executors. It retrieves gateway definitions from the registry service, uses blob storage for initial agent files, and stores gateway sessions in Redis or SQLite. See also: [configuration](https://github.com/golemcloud/golem/blob/main/golem-worker-service/config/worker-service.toml), [environment variables](https://github.com/golemcloud/golem/blob/main/golem-worker-service/config/worker-service.sample.env), [docker image](https://hub.docker.com/r/golemservices/golem-worker-service) diff --git a/docs/src/content/next/develop.mdx b/docs/src/content/next/develop.mdx index 4dc4db1be9..3a7fb8a0d2 100644 --- a/docs/src/content/next/develop.mdx +++ b/docs/src/content/next/develop.mdx @@ -1,88 +1,89 @@ import { Cards, Steps } from "nextra/components" -# Develop an application Golem +# Develop an application on Golem -Developing an application on golem consists of two major steps: +A Golem application contains one or more WebAssembly components, their agents and tools, and optional HTTP or MCP API deployments. The application manifest keeps these parts and their environments in one deployable definition. -- Writing one or more **Golem components** in one of the supported programming languages. -- Defining external HTTP endpoints +## Create an application -This page summarizes the workflow of developing an application on Golem, with links to more specific guides for each step. - -## Creating an application -The primary tool for developing an application on Golem is the [Golem CLI](/next/cli). - -To create a new application, use the `golem new` command, passing the name of the application and it's default programming language (note: it is possible to add components using different languages later). +The primary development tool is the [Golem CLI](/next/cli). List the current templates, optionally filtered by language: ```shell copy -golem new my-app +golem templates +golem templates rust ``` -## Writing the code -The application is just a project directory that can contain multiple components. To learn how to add a new component and implement it, check the specific guides in this chapter. - -### Iterations -During development the whole application can be built using +Create an application by selecting a template: ```shell copy -golem build +golem new my-app --template +cd my-app ``` -and every component can be deployed using +In non-interactive use, provide at least one `--template`. The template determines the language and generates a component name when you do not pass `--component-name`. + +Add another component from the application directory: ```shell copy -golem deploy +golem new . --template --component-name my:component ``` -This is going to create a new **version** of each component. If there are agents created already, those are not going to be updated automatically to these new versions. Check the [agents page](/next/concepts/agents) for more information about updating agents. +See [Defining components](/next/develop/defining-components) and the language-specific SDK pages for implementation guidance. -## Defining APIs + + + + + -Most applications require a public HTTP API (but this is not mandatory - you can always use Golem's invocation API to directly communicate with your Golem application). +## Build and deploy -### Guidelines + +### Build locally -Check the [defining custom APIs](/next/invoke/making-custom-apis) page as a starting point for learning how to define custom APIs. +Build every selected component from the application manifest: -The recommended way to manage these custom APIs is to create a single API definition YAML in the application's root directory. Future Golem versions will integrate API definitions into the _application manifest_ itself. +```shell copy +golem build +``` -### Iterations +From a component directory, manifest discovery selects that component. You can also select components explicitly with positional names, for example `golem build my:component my:other-component`. -Use the `golem api` commands to iterate on your APIs. +### Review a deployment - -### Incrementing the version +Preview the deployment without staging or applying changes: -Every time you make changes, the API's version must be incremented in the YAML file. +```shell copy +golem deploy --plan +``` -### Uploading +Use `--full-diff` when you also want unchanged entries in the displayed deployment and environment-setup diff. -Upload the new API definition using the `golem api upload` command: +### Deploy the application ```shell copy -golem api definition update api.yaml +golem deploy ``` -(Use `golem api definition new api.yaml` the first time) +Deployments reconcile the application manifest as one application-level operation. Changed source is built automatically. Existing agents remain on their current component revision unless you explicitly request an update, redeployment, or reset. -### Deleting the previous deployment - -Before deploying the new API version, the previous deployment must be deleted using the `golem api deployment delete` command: + -```shell copy -golem api deployment delete my-definition/0.0.1 -``` +See [Deployment](/next/cli/app-manifest#build-and-deploy) for environment selection and destructive post-deployment options. -### Deploying +## Expose an API -To try out the actual API, you also have to **deploy** it using the `golem api deploy` command: +Define HTTP endpoints in agent code, then configure HTTP and MCP deployments in `golem.yaml` using `httpApi.deployments` and `mcp.deployments`. `golem deploy` deploys them alongside the application's components, so you do not need a separate manual delete-and-recreate cycle for each version. -```shell copy -golem api deployment deploy my-definition/0.0.2 -``` +Start with [Defining custom APIs](/next/invoke/making-custom-apis), then see the [application manifest reference](/next/app-manifest) for the deployment fields. -### Breaking the component APIs +Direct invocation remains available when an application does not need a public endpoint. See [Invoke](/next/invoke) and [Agent-to-agent communication](/next/concepts/agent-to-agent-communication). -When an API is using a component's **exported interface**, it is not possible to deploy a new version of that component if it is breaking that used interface (`golem deploy` will fail). To resolve this, delete the deployment first as shown above. +## Next steps - + + + + + + diff --git a/docs/src/content/next/develop/_meta.js b/docs/src/content/next/develop/_meta.js index 6efedbe863..25666c13f3 100644 --- a/docs/src/content/next/develop/_meta.js +++ b/docs/src/content/next/develop/_meta.js @@ -11,6 +11,8 @@ export default { type: "separator", title: "Golem SDK", }, + "tool-authoring": "Tool Authoring and Lifecycle", + "durable-streams": "External Durable Streams", http: "HTTP client", websocket: "WebSocket client", durability: "Durability", diff --git a/docs/src/content/next/develop/additional.mdx b/docs/src/content/next/develop/additional.mdx index 1ddfc88266..38dfb4f837 100644 --- a/docs/src/content/next/develop/additional.mdx +++ b/docs/src/content/next/develop/additional.mdx @@ -10,12 +10,22 @@ It is guaranteed that this idempotency key will always be the same (per occurren To generate an idempotency key: - + ```typescript import { generateIdempotencyKey } from "@golemcloud/golem-ts-sdk"; const key = generateIdempotencyKey(); +``` + + +```typescript +import { Effect } from "effect" +import { Durability } from "@golemcloud/effect-golem" + +const key = Effect.gen(function* () { + return yield* Durability.generateIdempotencyKey +}) ``` @@ -43,7 +53,7 @@ let key = @api.generate_idempotency_key() It is possible to query **metadata** for Golem agents. This metadata is defined by the `AgentMetadata` interface: - + ```typescript /** @@ -57,7 +67,7 @@ It is possible to query **metadata** for Golem agents. This metadata is defined /** Environment variables seen by the agent */ env: [string, string][]; /** Configuration variables seen by the agent */ - configVars: [string, string][]; + config: [string, string][]; /** The current agent status */ status: AgentStatus; /** The component revision the agent is running with */ @@ -74,6 +84,16 @@ It is possible to query **metadata** for Golem agents. This metadata is defined - `getSelfMetadata()` returns the metadata for the current agent - `getAgentMetadata(agentId: AgentId)` returns the metadata for a specific agent given by its `AgentId`, or `undefined` if it does not exist + + + Effect TypeScript re-exports the host `AgentMetadata` type as `Agents.AgentMetadata`. Query metadata through Effect values: + + ```typescript + import { Agents } from "@golemcloud/effect-golem" + + const self = Agents.getSelfMetadata + const other = (agentId: Agents.AgentId) => Agents.getAgentMetadata(agentId) + ``` ```rust @@ -86,7 +106,7 @@ It is possible to query **metadata** for Golem agents. This metadata is defined /// Environment variables seen by the agent pub env: Vec<(String, String)>, /// Configuration variables seen by the agent - pub config_vars: Vec<(String, String)>, + pub config: Vec<(String, String)>, /// The current agent status pub status: AgentStatus, /// The component revision the agent is running with @@ -100,7 +120,7 @@ It is possible to query **metadata** for Golem agents. This metadata is defined There are two exported functions to query agent metadata: - - `get_self_metadata()` returns the metadata for the current agent + - `get_self_metadata()` returns `Result` for the current agent - `get_agent_metadata(agent_id: &AgentId)` returns `Some(AgentMetadata)` for a specific agent given by its `AgentId`, or `None` if it does not exist @@ -113,7 +133,7 @@ It is possible to query **metadata** for Golem agents. This metadata is defined agentId: HostApi.AgentIdLiteral, args: List[String], env: Map[String, String], - configVars: Map[String, String], + config: Map[String, String], status: HostApi.AgentStatus, componentRevision: BigInt, retryCount: BigInt, @@ -139,12 +159,12 @@ It is possible to query **metadata** for Golem agents. This metadata is defined agent_id : @rpcTypes.AgentId args : Array[String] env : Array[(String, String)] - config_vars : Array[(String, String)] + config : Array[(String, String)] status : @api.AgentStatus component_revision : UInt64 retry_count : UInt64 environment_id : @api.EnvironmentId - } derive(Show, Eq) + } ``` There are two exported functions to query agent metadata: @@ -166,7 +186,7 @@ Agent enumeration is a feature of Golem available both through the public HTTP A The following example demonstrates how to use the agent enumeration API: - + ```typescript import { @@ -206,9 +226,19 @@ while ((batch = getter.getNext()) !== undefined) { ``` + +```typescript +import { Stream } from "effect" +import { Agents } from "@golemcloud/effect-golem" + +const enumerateAgents = (componentId: Agents.ComponentId) => + Agents.getAgents({ componentId, precise: true }).pipe(Stream.runCollect) +``` + + ```rust -use golem_rust::bindings::golem::api::host::{ +use golem_rust::{ AgentAllFilter, AgentAnyFilter, AgentMetadata, AgentPropertyFilter, AgentStatus, AgentStatusFilter, ComponentId, FilterComparator, GetAgents, }; @@ -228,7 +258,7 @@ let getter = GetAgents::new(component_id, Some(&filter), true); let mut agents: Vec = vec![]; -while let Some(batch) = getter.get_next() { +while let Some(batch) = getter.get_next()? { agents.extend(batch); } ``` @@ -295,11 +325,11 @@ while true { -The third parameter enables `precise` mode. In this mode, Golem will calculate the latest metadata for each returned agent; otherwise, it uses only the last cached values. +The `precise` parameter or option enables precise mode. In this mode, Golem calculates the latest metadata for each returned agent; otherwise, it uses only the last cached values. ### Update an agent - + To trigger an update for a given agent from one component version to another, use the `updateAgent` function: @@ -319,16 +349,34 @@ updateAgent(agentId, targetRevision, "automatic") ``` + +To update an agent from Effect TypeScript, pass the target revision and mode to `Agents.updateAgent`: + +```typescript +import { Effect } from "effect" +import { Agents } from "@golemcloud/effect-golem" + +const updateSelf = Effect.gen(function* () { + const metadata = yield* Agents.getSelfMetadata + yield* Agents.updateAgent({ + agentId: metadata.agentId, + targetRevision: 1n, + mode: "automatic", + }) +}) +``` + + To trigger an update for a given agent from one component version to another, use the `update_agent` function: ```rust -use golem_rust::bindings::golem::api::host::{get_self_metadata, update_agent, UpdateMode}; +use golem_rust::{get_self_metadata, update_agent, UpdateMode}; -let agent_id = get_self_metadata().agent_id; +let agent_id = get_self_metadata()?.agent_id; let target_component_revision = 1u64; -update_agent(&agent_id, target_component_revision, UpdateMode::Automatic); +update_agent(&agent_id, target_component_revision, UpdateMode::Automatic)?; ``` @@ -399,7 +447,7 @@ The resource is affine: `finish` consumes it, and it cannot be completed twice. The request, response, operation name, and observational host calls are intended to support observability. They may appear in oplog inspection, logs, and metrics. Use useful schemas and names, but redact credentials and personal data, and avoid recording unnecessary or unbounded payloads. -Use an SDK combinator instead of handling the live resource manually. Rust provides `golem_rust::durability::Durability::run`, `run_async`, `run_infallible`, and `run_infallible_async`; TypeScript provides `durable`; Scala provides `DurabilityApi.durable` and `durableAsync`; MoonBit provides `@api.durable` and `durable_async`. These combinators own begin/replay/finish/drop and evaluate the operation body only for a live invocation. +Use an SDK combinator instead of handling the live resource manually. Rust provides `golem_rust::durability::Durability::run`, `run_async`, `run_infallible`, and `run_infallible_async`; TypeScript provides `durable`; Effect TypeScript provides `Durability.wrap` and `wrapInfallible`; Scala provides `DurabilityApi.durable` and `durableAsync`; MoonBit provides `@api.durable` and `durable_async`. These combinators own begin/replay/finish/drop and evaluate the operation body only for a live invocation. ### Invocation context @@ -407,7 +455,7 @@ Golem associates an **invocation context** with each invocation, which contains The spans are not automatically sent to any tracing system but they can be reconstructed from the oplog, for example using **oplog processor plugins**, to provide real-time tracing information. - + To get the current invocation context, use the `currentContext` host function, imported from `golem:api/context@1.5.0`: @@ -458,6 +506,24 @@ and then use the `Span` class's methods: If `finish` is not explicitly called on the span, it is going to be finished when the runtime drops the span object. + + + +Effect TypeScript installs a Golem-backed tracer automatically. Use `Tracing.currentContext` for an immutable snapshot of the host context and normal Effect spans for custom work: + +```typescript +import { Effect } from "effect" +import { Tracing } from "@golemcloud/effect-golem" + +const operation = Effect.gen(function* () { + const context = yield* Tracing.currentContext + return context.traceId +}).pipe(Effect.withSpan("my-operation")) +``` + +Use `Tracing.traceContextHeaders`, `withForwardedHeaders`, and `withInvocationParent` for explicit propagation. + +These Effects are intended to run inside an Effect agent handler, where the dispatcher installs the required host services. @@ -467,7 +533,7 @@ To get the current invocation context, use the `current_context` host function, /** * Invocation context support */ -fn current_context(): InvocationContext; +pub fn current_context() -> InvocationContext; ``` The `InvocationContext` resource exposes various methods for querying attributes of the invocation context: @@ -602,9 +668,9 @@ To export traces, logs, and metrics to an external system, see the [Observabilit Although Golem agents can store their state completely in their own memory, it is possible to use the `wasi:keyvalue` interface to store key-value pairs in a Golem managed key value storage. -This can be useful if state needs to be shared between different agents or if the size of this state is too large to be stored in memory. The keys are accessible for every agent of an _application_ - no matter which component they are defined in, or which agent type they belong to. +This can be useful if state needs to be shared between different agents or if the size of this state is too large to be stored in memory. Storage is shared across agents and components within the same environment. - + There are two primary modules for using the key-value store: - `wasi:keyvalue/eventual@0.1.0` defines an API for an eventually consistent key-value store @@ -654,45 +720,15 @@ This can be useful if state needs to be shared between different agents or if th ```typescript export class OutgoingValue { static newOutgoingValue(): OutgoingValue; - /** - * Writes the value to the output-stream asynchronously. - * If any other error occurs, it returns an `Err(error)`. - * @throws Error - */ - outgoingValueWriteBodyAsync(): OutgoingValueBodyAsync; - /** - * Writes the value to the output-stream synchronously. - * If any other error occurs, it returns an `Err(error)`. - * @throws Error - */ - outgoingValueWriteBodySync(value: OutgoingValueBodySync): void; + outgoingValueWriteBodyAsync(data: AsyncIterable): void; + outgoingValueWriteBodySync(value: Uint8Array): void; } export class IncomingValue { - /** - * Consumes the value synchronously and returns the value as a list of bytes. - * If any other error occurs, it returns an `Err(error)`. - * @throws Error - */ - incomingValueConsumeSync(): IncomingValueSyncBody; - /** - * Consumes the value asynchronously and returns the value as an `input-stream`. - * If any other error occurs, it returns an `Err(error)`. - * @throws Error - */ - incomingValueConsumeAsync(): IncomingValueAsyncBody; - /** - * The size of the value in bytes. - * If the size is unknown or unavailable, this function returns an `Err(error)`. - * @throws Error - */ + incomingValueConsumeSync(): Uint8Array; + incomingValueConsumeAsync(): AsyncIterable; incomingValueSize(): bigint; } - - export type OutgoingValueBodyAsync = OutputStream; - export type OutgoingValueBodySync = Uint8Array; - export type IncomingValueAsyncBody = InputStream; - export type IncomingValueSyncBody = Uint8Array; ``` The streaming variants of setting and consuming the values may be used by the underlying implementation to directly @@ -700,6 +736,26 @@ This can be useful if state needs to be shared between different agents or if th a `Uint8Array`. + + Effect TypeScript provides scoped byte and schema-typed bucket APIs: + + ```typescript + import { Effect, Option } from "effect" + import { KeyValue } from "@golemcloud/effect-golem" + + const program = Effect.scoped( + Effect.gen(function* () { + const bucket = yield* KeyValue.openBucket("shared") + yield* bucket.set("greeting", new TextEncoder().encode("hello")) + const value = yield* bucket.get("greeting") + return Option.map(value, (bytes) => new TextDecoder().decode(bytes)) + }), + ) + ``` + + Buckets also provide batch operations, `keys`, and `forSchema`. Atomic increment/compare-and-swap and cache APIs are not currently exposed. + + There are two primary modules for using the key-value store: - `golem_rust::bindings::wasi::keyvalue::eventual` defines an API for an eventually consistent key-value store @@ -778,13 +834,13 @@ This can be useful if state needs to be shared between different agents or if th * If any other error occurs, it returns an `Err(error)`. * @throws Error */ - pub fn incoming_value_consume_sync(&self) -> Result; + pub fn incoming_value_consume_sync(&self) -> Result, Error>; /** * Consumes the value asynchronously and returns the value as an `input-stream`. * If any other error occurs, it returns an `Err(error)`. * @throws Error */ - pub fn incoming_value_consume_async(&self) -> Result; + pub fn incoming_value_consume_async(&self) -> Result, Error>; /** * The size of the value in bytes. * If the size is unknown or unavailable, this function returns an `Err(error)`. @@ -793,10 +849,6 @@ This can be useful if state needs to be shared between different agents or if th pub fn incoming_value_size(&self) -> Result; } - pub type OutgoingValueBodyAsync = OutputStream; - pub type OutgoingValueBodySync = Vec; - pub type IncomingValueAsyncBody = InputStream; - pub type IncomingValueSyncBody = Vec; ``` The streaming variants of setting and consuming the values may be used by the underlying implementation to directly @@ -809,6 +861,8 @@ This can be useful if state needs to be shared between different agents or if th The primary interface to work with key-value pairs is the `Bucket` class: + The following declarations summarize the public API shape; they are not definitions to copy into your application. + ```scala import golem.wasi.KeyValue @@ -869,7 +923,9 @@ This can be useful if state needs to be shared between different agents or if th - The generated `wasi:keyvalue` bindings are exposed through the `golemcloud/golem_sdk/interface/wasi/keyvalue/eventual`, `golemcloud/golem_sdk/interface/wasi/keyvalue/eventualBatch`, and `golemcloud/golem_sdk/interface/wasi/keyvalue/types` packages. Assuming they are imported as `@eventual`, `@eventualBatch`, `@types`, `@streams`, and `@wasiKeyvalueError`: + The generated `wasi:keyvalue` bindings are exposed through the `golemcloud/golem_sdk/interface/wasi/keyvalue/eventual`, `golemcloud/golem_sdk/interface/wasi/keyvalue/eventualBatch`, and `golemcloud/golem_sdk/interface/wasi/keyvalue/types` packages. Assuming they are imported as `@eventual`, `@eventualBatch`, and `@types`, with `golemcloud/golem_sdk/async-core` imported as `@async-core` and the key-value error package as `@wasiKeyvalueError`: + + The declarations below summarize the generated API shape; they are not definitions to copy into your application. The primary interface to work with the key-value pairs consists of the four basic operations: @@ -919,20 +975,24 @@ This can be useful if state needs to be shared between different agents or if th ```moonbit pub fn Bucket::open_bucket(String) -> Result[Self, @wasiKeyvalueError.Error_] - pub(all) struct OutgoingValue(Int) derive(Eq, Show) + pub(all) struct OutgoingValue(Int) pub fn OutgoingValue::new_outgoing_value() -> Self pub fn OutgoingValue::outgoing_value_write_body_sync( Self, FixedArray[Byte], ) -> Result[Unit, @wasiKeyvalueError.Error_] + pub fn OutgoingValue::outgoing_value_write_body_async( + Self, + @async-core.Stream[Byte], + ) -> Result[Unit, @wasiKeyvalueError.Error_] - pub(all) struct IncomingValue(Int) derive(Eq, Show) + pub(all) struct IncomingValue(Int) pub fn IncomingValue::incoming_value_consume_sync( Self, ) -> Result[FixedArray[Byte], @wasiKeyvalueError.Error_] pub fn IncomingValue::incoming_value_consume_async( Self, - ) -> Result[@streams.InputStream, @wasiKeyvalueError.Error_] + ) -> Result[@async-core.Stream[Byte], @wasiKeyvalueError.Error_] pub fn IncomingValue::incoming_value_size( Self, ) -> Result[UInt64, @wasiKeyvalueError.Error_] @@ -944,9 +1004,9 @@ This can be useful if state needs to be shared between different agents or if th ### The WASI Blob Store interface -The `wasi:blobstore` interface provides a way to store and retrieve large binary data. This can be useful for storing large files or other binary data that is too large to be stored in the agent's memory. The blobs are accessible for every agent of an _application_ - no matter which component they are defined in, or which agent type they belong to. +The `wasi:blobstore` interface provides a way to store and retrieve large binary data. This can be useful for storing large files or other binary data that is too large to be stored in the agent's memory. Storage is shared across agents and components within the same environment. - + The Blob Store API organizes blobs identified by _object names_ into **containers**. The `wasi:blobstore/blobstore` module exports functions to create, get and delete these containers by name: @@ -1022,7 +1082,7 @@ The `wasi:blobstore` interface provides a way to store and retrieve large binary * returns list of objects in the container. Order is undefined. * @throws Error */ - listObjects(): StreamObjectNames; + listObjects(): AsyncIterable; /** * deletes object. * does not return error if object did not exist. @@ -1052,7 +1112,26 @@ The `wasi:blobstore` interface provides a way to store and retrieve large binary } ``` - `IncomingValue` can consume blob data either synchronously as a `Uint8Array` or asynchronously as an `InputStream`. To write blob data, create an `OutgoingValue` and obtain its `OutputStream` via `outgoingValueWriteBody()`. + `IncomingValue` can consume blob data either synchronously as a `Uint8Array` or asynchronously as an `AsyncIterable`. To write blob data asynchronously, pass an `AsyncIterable` to `outgoingValueWriteBody(data)`. + + + + Effect TypeScript provides scoped containers with byte and schema-typed operations: + + ```typescript + import { Effect } from "effect" + import { Blobstore } from "@golemcloud/effect-golem" + + const program = Effect.scoped( + Effect.gen(function* () { + const container = yield* Blobstore.getOrCreateContainer("documents") + yield* container.writeData("hello.txt", new TextEncoder().encode("hello")) + return yield* container.getData("hello.txt") + }), + ) + ``` + + Containers also support listing, metadata, deletion, ranges, and `forSchema`. @@ -1115,7 +1194,7 @@ The `wasi:blobstore` interface provides a way to store and retrieve large binary /** * returns list of objects in the container. Order is undefined. */ - pub fn list_objects(&self) -> Result + pub fn list_objects(&self) -> Result, Error> /** * deletes object. * does not return error if object did not exist. @@ -1140,14 +1219,17 @@ The `wasi:blobstore` interface provides a way to store and retrieve large binary } ``` - `IncomingValue` can be consumed either synchronously as a `Vec` or asynchronously as an `InputStream`. To write blob data, create an `OutgoingValue` and obtain its `OutputStream` via `outgoing_value_write_body()`. + `IncomingValue` can be consumed either synchronously as a `Vec` or asynchronously as a `StreamReader`. To write blob data asynchronously, pass a `StreamReader` to `outgoing_value_write_body` and drive the retained writer. The Scala SDK exposes the underlying `wasi:blobstore` modules through the `golem.wasi.Blobstore` wrapper: + The following declarations summarize the public API shape; they are not definitions to copy into your application. + ```scala import golem.wasi.Blobstore + import scala.concurrent.Future object Blobstore { final case class ObjectId(container: String, name: String) @@ -1165,6 +1247,7 @@ The `wasi:blobstore` interface provides a way to store and retrieve large binary ```scala import golem.wasi.Blobstore + import scala.concurrent.Future object Blobstore { final class Container { @@ -1172,7 +1255,7 @@ The `wasi:blobstore` interface provides a way to store and retrieve large binary def info(): ContainerMetadata def getData(objectName: String, start: Long, end: Long): Array[Byte] def writeData(objectName: String, data: Array[Byte]): Unit - def listObjects(): List[String] + def listObjects(): Future[List[String]] def deleteObject(name: String): Unit def deleteObjects(names: List[String]): Unit def hasObject(name: String): Boolean @@ -1186,7 +1269,9 @@ The `wasi:blobstore` interface provides a way to store and retrieve large binary - The generated `wasi:blobstore` bindings are exposed through the `golemcloud/golem_sdk/interface/wasi/blobstore/blobstore`, `golemcloud/golem_sdk/interface/wasi/blobstore/container`, and `golemcloud/golem_sdk/interface/wasi/blobstore/types` packages. Assuming they are imported as `@blobstore`, `@container`, `@types`, and `@streams`: + The generated `wasi:blobstore` bindings are exposed through the `golemcloud/golem_sdk/interface/wasi/blobstore/blobstore`, `golemcloud/golem_sdk/interface/wasi/blobstore/container`, and `golemcloud/golem_sdk/interface/wasi/blobstore/types` packages. Assuming they are imported as `@blobstore`, `@container`, and `@types`, with `golemcloud/golem_sdk/async-core` imported as `@async-core`: + + The declarations below summarize the generated API shape; they are not definitions to copy into your application. The Blob Store API organizes blobs identified by _object names_ into **containers**. The `@blobstore` package exports functions to create, get and delete these containers by name: @@ -1225,7 +1310,7 @@ The `wasi:blobstore` interface provides a way to store and retrieve large binary A `Container` resource in the `@container` package provides read-write access for the blobs in it: ```moonbit - pub(all) struct Container(Int) derive(Show, Eq) + pub(all) struct Container(Int) /// returns container name pub fn Container::name(self : Container) -> Result[String, String] @@ -1252,7 +1337,7 @@ The `wasi:blobstore` interface provides a way to store and retrieve large binary ) -> Result[Unit, String] /// returns list of objects in the container. Order is undefined. - pub fn Container::list_objects(self : Container) -> Result[@container.StreamObjectNames, String] + pub fn Container::list_objects(self : Container) -> Result[@async-core.Stream[String], String] /// deletes object. /// does not return error if object did not exist. @@ -1277,6 +1362,6 @@ The `wasi:blobstore` interface provides a way to store and retrieve large binary pub fn Container::clear(self : Container) -> Result[Unit, String] ``` - `IncomingValue` can be consumed either synchronously as a `FixedArray[Byte]` or asynchronously as an `@streams.InputStream`. To write blob data, create an `OutgoingValue` with `@types.OutgoingValue::new_outgoing_value()` and obtain its `@streams.OutputStream` via `outgoing_value_write_body()`. + `IncomingValue` can be consumed either synchronously as a `FixedArray[Byte]` or asynchronously as an `@async-core.Stream[Byte]`. To write blob data, create an `OutgoingValue` with `@types.OutgoingValue::new_outgoing_value()` and pass an `@async-core.Stream[Byte]` to `outgoing_value_write_body`. diff --git a/docs/src/content/next/develop/agent-filesystem.mdx b/docs/src/content/next/develop/agent-filesystem.mdx index 87d8135710..f2d1ed750a 100644 --- a/docs/src/content/next/develop/agent-filesystem.mdx +++ b/docs/src/content/next/develop/agent-filesystem.mdx @@ -9,13 +9,16 @@ There is no way for an agent to access files outside its own filesystem. ## Accessing the filesystem - + For TypeScript agents, use [`node:fs`](https://nodejs.org/api/fs.html) or [`node:fs/promises`](https://nodejs.org/api/fs.html#promises-api) to work with files. In Golem 1.5, the QuickJS runtime provides a comprehensive filesystem API, so standard operations such as reading, writing, checking existence, creating directories, and listing directories are available. + Effect TypeScript agents use [`node:fs`](https://nodejs.org/api/fs.html) or [`node:fs/promises`](https://nodejs.org/api/fs.html#promises-api), just like other TypeScript agents. The Effect template's QuickJS runtime provides these filesystem implementations. + + For Rust agents, use the standard [`std::fs`](https://doc.rust-lang.org/std/fs/index.html) module to work with files. Golem Rust agents compile to WASI, so standard filesystem operations such as `read_to_string`, `read`, `write`, and `read_dir` work out of the box. @@ -26,9 +29,9 @@ There is no way for an agent to access files outside its own filesystem. Standard JVM file I/O APIs such as `java.io.File` and `java.nio.file.*` are not available in Scala.js. Define a small `@JSImport("node:fs", JSImport.Namespace)` facade and access it lazily at runtime. - For MoonBit agents, use the [`moonbitlang/x/fs`](https://mooncakes.io/docs/moonbitlang/x/fs) package to work with files. + For MoonBit agents, add `"golemcloud/golem_sdk/filesystem" @fs` to the `import` block in your component's `moon.pkg`. - Golem MoonBit agents compile to WASI, so helper functions such as `read_file_to_string`, `write_string_to_file`, `path_exists`, and `read_dir` cover standard filesystem operations. + Its async helpers include `read_bytes`, `write_bytes`, and `list_directory`. Obtain the root directory descriptor with `@fs.get_root_dir()` and handle its optional result before passing the descriptor to these helpers. @@ -72,207 +75,11 @@ use the `golem:api/save-snapshot` function to persist the files and later restor ## Externally accessing agent files -It is easy to expose files on the agent filesystem via HTTP using code-first route definitions. - -For example, we can define an agent with a `download` endpoint that serves files: - - - - ```typescript - import { z } from 'zod'; - import { defineAgent, method, http, s } from '@golemcloud/golem-ts-sdk'; - import * as fs from 'node:fs'; - - export const FileServerAgent = defineAgent({ - name: 'FileServerAgent', - id: { name: z.string() }, - http: http.mount('/files/{name}'), - methods: { - downloadFile: method({ - input: { path: z.string() }, - returns: s.unstructuredBinary(), - http: http.get('/download/{path}'), - }), - }, - }); - - export const FileServerAgentImpl = FileServerAgent.implement({ - init: () => ({}), - methods: { - downloadFile({ path }) { - try { - const buffer = fs.readFileSync(`/web/${path}`); - - let contentType = 'application/octet-stream'; - if (path.endsWith('.html')) { - contentType = 'text/html'; - } else if (path.endsWith('.js')) { - contentType = 'application/javascript'; - } - - return { tag: 'inline', val: buffer, mimeType: contentType }; - } catch (err) { - return { - tag: 'inline', - val: new TextEncoder().encode(`Not found! (${err})`), - mimeType: 'text/plain', - }; - } - }, - }, - }); - ``` - - - - ```rust - use std::fs; - - use golem_rust::{agent_definition, agent_implementation, UnstructuredBinary}; - - #[agent_definition(mount = "/files/{name}")] - pub trait FileServerAgent { - fn new(name: String) -> Self; - - #[endpoint(get = "/download/{*path}")] - fn download_file(&self, path: String) -> UnstructuredBinary; - } - - struct FileServerAgentImpl { - _name: String, - } - - #[agent_implementation] - impl FileServerAgent for FileServerAgentImpl { - fn new(name: String) -> Self { - Self { _name: name } - } - - fn download_file(&self, path: String) -> UnstructuredBinary { - match fs::read(format!("/web/{path}")) { - Ok(contents) => { - let content_type = if path.ends_with(".html") { - "text/html" - } else if path.ends_with(".js") { - "application/javascript" - } else { - "application/octet-stream" - }; - UnstructuredBinary::inline(contents, content_type) - } - Err(_) => UnstructuredBinary::inline( - b"Not found!".to_vec(), - "text/plain", - ), - } - } - } - - ``` - - - - ```scala - import golem.runtime.annotations.{agentDefinition, agentImplementation, endpoint} - import golem.{BaseAgent, UnstructuredBinary} - - import scala.annotation.unused - import scala.concurrent.Future - import scala.scalajs.js - import scala.scalajs.js.annotation.JSImport - import scala.scalajs.js.typedarray.Uint8Array - - @js.native - @JSImport("node:fs", JSImport.Namespace) - private object Fs extends js.Object { - def readFileSync(path: String): Uint8Array = js.native - } - - @agentDefinition(mount = "/files/{name}") - trait FileServerAgent extends BaseAgent { - class Id(val name: String) - - @endpoint(get = "/download/{path}") - def downloadFile(path: String): Future[UnstructuredBinary] - } - - @agentImplementation() - final class FileServerAgentImpl(@unused private val name: String) extends FileServerAgent { - override def downloadFile(path: String): Future[UnstructuredBinary] = - Future.successful { - try { - val bytes = Fs.readFileSync(s"/web/$path") - - val contentType = - if path.endsWith(".html") then "text/html" - else if path.endsWith(".js") then "application/javascript" - else "application/octet-stream" - - UnstructuredBinary.inline(toByteList(bytes), contentType) - } catch { - case _: Throwable => - UnstructuredBinary.inline( - "Not found!".getBytes("UTF-8").iterator.map(_.toByte).toList, - "text/plain" - ) - } - } - - private def toByteList(bytes: Uint8Array): List[Byte] = { - val result = List.newBuilder[Byte] - var i = 0 - while i < bytes.length do { - result += bytes(i).toByte - i += 1 - } - result.result() - } - } - - ``` - - - - ```moonbit - #derive.agent(mount="/files/{name}") - struct FileServerAgent { - _name : String - } - - fn FileServerAgent::new(name : String) -> FileServerAgent { - { _name: name } - } - - #endpoint(get="/download/{path}") - pub fn FileServerAgent::download_file(self : Self, path : String) -> UnstructuredBinary { - ignore(self) - let full_path = "/web/\{path}" - - try { - let data = @fs.read_file_to_bytes(full_path) - let content_type = - if path.has_suffix(".html") { - "text/html" - } else if path.has_suffix(".js") { - "application/javascript" - } else { - "application/octet-stream" - } - - UnstructuredBinary::inline(data, content_type) - } catch { - _ => UnstructuredBinary::inline(b"Not found!", "text/plain") - } - } - ``` - - - - - - -With the `@endpoint` / `#[endpoint]` annotations, the HTTP route is automatically registered — no separate API mapping is needed. The agent above will serve files at `GET /files/{name}/download/{path}`. - -For more information about defining code-first HTTP endpoints for agents, [check the dedicated documentation page](/next/invoke/making-custom-apis). +To serve files without writing a download method, use [live filesystem mappings](/next/invoke/http-routers#serving-live-agent-files) +on a durable agent's mount. They support streaming, HEAD, and single byte-range requests, +including files created during initialization or overwritten by later invocations. +For deployment-provisioned read-only assets, use [immutable router files](/next/invoke/http-routers#serving-static-files). -For serving static content, it is good practice to define a separate agent in an **ephemeral component** that is only responsible for serving files. This way the file-serving endpoints will not ever block by waiting for a stateful agent to process their requests. +If a download needs application logic rather than direct file serving, define a +[code-first HTTP endpoint](/next/invoke/making-custom-apis) or a +[streaming router handler](/next/invoke/http-routers#handling-streaming-requests). diff --git a/docs/src/content/next/develop/config-and-secrets.mdx b/docs/src/content/next/develop/config-and-secrets.mdx index 8f4b0e4419..c7f0ec0844 100644 --- a/docs/src/content/next/develop/config-and-secrets.mdx +++ b/docs/src/content/next/develop/config-and-secrets.mdx @@ -6,9 +6,9 @@ Before Golem 1.5, agents could only receive configuration through **environment ## Configuration -Configuration types are defined as records in your agent's source code and can be nested. They are injected via the agent's constructor using the `Config` wrapper type. +Configuration types are defined as records in your agent's source code and can be nested. The SDK makes configuration available to the implementation: Rust and Scala use a constructor-injected `Config` wrapper, while Effect TypeScript uses a service created with `defineConfig`. - + ```typescript import { z } from 'zod'; @@ -43,6 +43,41 @@ Configuration types are defined as records in your agent's source code and can b }); ``` + + ```typescript + import { Effect, Schema } from "effect" + import { defineAgent, defineConfig, method } from "@golemcloud/effect-golem" + + class ExampleConfig extends defineConfig("Example.Config", { + debugLogs: Schema.Boolean, + alias: Schema.optional(Schema.String), + database: Schema.Struct({ + host: Schema.String, + port: Schema.Number, + }), + }) {} + + const ExampleAgent = defineAgent({ + name: "ExampleAgent", + id: { exampleParam: Schema.String }, + config: ExampleConfig, + methods: { + useConfig: method({ input: {}, success: Schema.Void }), + }, + }) + + ExampleAgent.implement({ + init: () => Effect.void, + methods: () => ({ + useConfig: () => + Effect.gen(function* () { + const config = yield* ExampleConfig + if (yield* config.debugLogs) console.log("Debug logs enabled") + }), + }), + }) + ``` + ```rust #[derive(ConfigSchema)] @@ -67,6 +102,13 @@ Configuration types are defined as records in your agent's source code and can b ```scala + import golem.BaseAgent + import golem.config.{AgentConfig, Config} + import golem.runtime.annotations.{agentDefinition, agentImplementation} + import zio.blocks.schema.Schema + + import scala.concurrent.Future + final case class DbConfig(host: String, port: Int) object DbConfig { implicit val schema: Schema[DbConfig] = Schema.derived @@ -82,6 +124,18 @@ Configuration types are defined as records in your agent's source code and can b class Id(val exampleParam: String) def useConfig(): Future[Unit] } + + @agentImplementation() + final class ExampleAgentImpl( + exampleParam: String, + config: Config[ExampleConfig] + ) extends ExampleAgent { + override def useConfig(): Future[Unit] = { + val current = config.value + if current.debugLogs then println("Debug logs enabled") + Future.successful(()) + } + } ``` @@ -118,15 +172,26 @@ agents: ## Secrets -Secrets are a special type of configuration — they are stored per environment and can be updated dynamically without redeploying the agent. To mark a configuration field as a secret, wrap its type in `Secret`: +Secrets are a special type of configuration — they are stored per environment and can be updated dynamically without redeploying the agent. Mark secret fields using your SDK's secret type or schema, as shown below: - + ```typescript // Import the `s` schema helper: import { s } from '@golemcloud/golem-ts-sdk'; password: s.secret(z.string()) ``` + + ```typescript + apiKey: Schema.Redacted(Schema.String) + ``` + + Access the secret through its Effectful handle: + + ```typescript + const apiKey = yield* config.apiKey.get + ``` + ```rust #[config_schema(secret)] @@ -147,7 +212,9 @@ Secrets are a special type of configuration — they are stored per environment ### Accessing secrets -Unlike regular configuration values, secret fields hold an opaque handle until the agent explicitly calls `.get()` on the `Secret` field. The reveal happens at that call site, so secret material is not embedded in the typed config value. Each reveal pins the secret revision it resolved; retries and replay use that pinned revision deterministically, while a fresh `.get()` after a secret update can observe the new value. +Unlike regular configuration values, secret fields hold an opaque handle until the agent explicitly accesses the secret. The reveal happens at that call site, so secret material is not embedded in the typed config value. Each reveal pins the secret revision it resolved; retries and replay use that pinned revision deterministically, while a fresh access after a secret update can observe the new value. + +In Effect TypeScript, evaluate `yield* config.apiKey.get` inside an Effect generator. It returns a `Redacted`; use `Redacted.value(apiKey)` only at the boundary that requires plaintext. ### Managing secrets via CLI diff --git a/docs/src/content/next/develop/defining-components.mdx b/docs/src/content/next/develop/defining-components.mdx index aa38f294ac..283e2d8ced 100644 --- a/docs/src/content/next/develop/defining-components.mdx +++ b/docs/src/content/next/develop/defining-components.mdx @@ -1,4 +1,4 @@ -import { Callout, Tabs } from "nextra/components" +import { Tabs } from "nextra/components" # Defining Golem Components @@ -6,7 +6,7 @@ import { Callout, Tabs } from "nextra/components" Golem's **command line interface** provides a set of predefined, Golem-specific **templates** to choose from as a starting point. - + To get started from scratch, first create a new application using the TypeScript template: @@ -16,6 +16,15 @@ cd my-app ``` + +To get started from scratch, first create a new application using the Effect TypeScript template: + +```shell copy +golem new --template effect --yes my-app +cd my-app +``` + + To get started from scratch, first create a new application using the Rust template: @@ -53,7 +62,7 @@ golem templates Then create a new component using the chosen template: - + ```shell copy golem new . --template ts --component-name my:component --yes @@ -62,6 +71,12 @@ golem new . --template ts --component-name my:component --yes ```shell copy +golem new . --template effect --component-name my:component --yes +``` + + + +```shell copy golem new . --template rust --component-name my:component --yes ``` @@ -106,7 +121,7 @@ checks. Invocation-scoped underlying access still advances the pinned middleware Defining Golem agents is done using the Golem SDK, which is included in all the templates provided by `golem`. - + An agent is declared with `defineAgent(...)` and given its behaviour with `.implement(...)`: @@ -128,6 +143,27 @@ export const MyAgentImpl = MyAgent.implement({ // ... }, }); +``` + + +An Effect agent is declared with `defineAgent(...)` and registered with `.implement(...)`. Methods return Effects: + +```typescript +import { Effect, Schema } from "effect" +import { defineAgent, method } from "@golemcloud/effect-golem" + +export const MyAgent = defineAgent({ + name: "MyAgent", + id: {}, + methods: { + ping: method({ input: {}, success: Schema.String }), + }, +}) + +MyAgent.implement({ + init: () => Effect.void, + methods: () => ({ ping: () => Effect.succeed("pong") }), +}) ``` @@ -199,7 +235,7 @@ Every agent must define a **constructor** with an arbitrary number of parameters For example, an agent working on a particular user's particular request could be defined as: - + ```typescript import { z } from 'zod'; @@ -212,6 +248,20 @@ export const RequestHandler = defineAgent({ // ... }, }); +``` + + +```typescript +import { Schema } from "effect" +import { defineAgent } from "@golemcloud/effect-golem" + +export const RequestHandler = defineAgent({ + name: "RequestHandler", + id: { userId: Schema.String, requestId: Schema.String }, + methods: { + // ... + }, +}) ``` @@ -245,11 +295,7 @@ fn RequestHandler::new(user_id : String, request_id : String) -> RequestHandler -An agent with a constructor like this can be identified by `request-handler("user-123", "request-456")`. - - - Note that the agent name is converted to _kebab-case_ (`request-handler`) when referred to in an agent identifier. This is due to some technical implementation details of the underlying Golem runtime. You can learn more about these mappings on the [name mapping page](/next/name-mapping). - +The Effect agent shown here is identified by `RequestHandler("user-123", "request-456")`; agent identifiers preserve the declared agent name. ### Agent methods @@ -263,7 +309,7 @@ This metadata is stored in the Golem component and can be used when exposing the The following example shows the use of how to attach these metadata to simple agent methods in different languages - + ```typescript import { z } from 'zod'; @@ -291,6 +337,35 @@ export const RequestHandler = defineAgent({ ``` + +```typescript +import { Schema } from "effect" +import { defineAgent, method } from "@golemcloud/effect-golem" + +const TaskDetails = Schema.Struct({ + description: Schema.String, + priority: Schema.Number, +}) +const RequestHandlerStatus = Schema.Struct({ + status: Schema.Literals(["success", "failure"]), + summary: Schema.String, +}) + +export const RequestHandler = defineAgent({ + name: "RequestHandler", + id: { userId: Schema.String, requestId: Schema.String }, + methods: { + addDetails: method({ + input: { details: TaskDetails }, + success: RequestHandlerStatus, + description: "Adds details and returns the current status.", + promptHint: "Enter new details about the task", + }), + }, +}) +``` + + ```rust use golem_rust::{Schema, agent_definition, prompt, description}; @@ -384,18 +459,22 @@ pub fn RequestHandler::add_details( -Agent methods can be both sync or async. +Agent methods can be both sync or async. Effect TypeScript handlers always return an `Effect`, whether the work represented by that Effect is synchronous or asynchronous. ### Supported data types -The SDK supports a large variety of data types to be used in agent constructors and agent methods, both in parameter and return type position. When a type is not supported, it will report an error at compile time. +The SDK supports a large variety of data types in agent constructors and agent methods, both in parameter and return type position. Validation timing is SDK-specific; Effect TypeScript validates whether its schemas have an unambiguous Golem representation during registration and discovery. - + - In TypeScript, most serializable user-defined types work as expected. Prefer named type aliases or interfaces for parameters and return types, and use string literal unions instead of TypeScript enums. See the [page about data type mapping](/next/type-mapping) for more details. + In TypeScript, declare constructor parameters, method inputs, and return values using supported runtime schemas, such as Zod schemas. Use `z.object(...)` for records and `z.enum(...)` for string alternatives; TypeScript types are inferred from these schemas. - In Rust, any data type that implements the `golem_rust::Schema` trait can be used as a constructor parameter or as a parameter or return type of an agent method. For user-defined types, `#[derive(Schema)]` is usually enough to derive the implementation automatically. See the [page about data type mapping](/next/type-mapping) for more details about the supported built-in types. + Effect TypeScript uses Effect `Schema` values for IDs, method inputs, successes, and typed errors. User-defined records use `Schema.Struct`, and variants use schemas such as `Schema.Union`. Ambiguous unions and schemas without a Golem representation are rejected during registration or discovery. + + + + In Rust, any data type that implements the `golem_rust::agentic::Schema` trait can be used as a constructor parameter or as a parameter or return type of an agent method. For user-defined types, `#[derive(Schema)]` is usually enough to derive the implementation automatically. See the [page about data type mapping](/next/type-mapping) for more details about the supported built-in types. Note that parameters must be passed by value to agent methods, not as references (`Schema` is not implemented for `&T`). @@ -409,15 +488,17 @@ The SDK supports a large variety of data types to be used in agent constructors -Check the [page about data type mapping](/next/type-mapping) to learn how each type is mapped to **JSON** when using the invocation API. - ### Library support - + See the [JS APIs](/next/js-apis) page for a detailed overview of all the supported Web Platform and Node.js APIs. Most 3rd party libraries can be installed using `npm` and used in the agent code, as long as they only rely on the supported APIs and do not require native addons. + + Packages compatible with the template's pinned Effect version can be installed with `npm`. They must rely on Web Platform or supported Node.js APIs and must not require native addons. + + Any Rust crate can be added as a dependency if it supports compiling to the `wasm32-wasip2` target. @@ -434,7 +515,7 @@ Check the [page about data type mapping](/next/type-mapping) to learn how each t ### Agent mode - durable or ephemeral By default every agent is **durable**. To define an **ephemeral agent**, override the agent mode in the agent definition: - + ```typescript export const EphemeralAgent = defineAgent({ @@ -448,6 +529,19 @@ By default every agent is **durable**. To define an **ephemeral agent**, overrid ``` + + ```typescript + export const EphemeralAgent = defineAgent({ + name: "EphemeralAgent", + mode: "ephemeral", + id: { name: Schema.String }, + methods: { + // ... + }, + }) + ``` + + ```rust #[agent_definition(ephemeral)] @@ -488,26 +582,33 @@ By default every agent is **durable**. To define an **ephemeral agent**, overrid It is often required to pass _configuration values_ to agents when they are started. -In general Golem supports three different ways of doing this: +Golem supports two ways of doing this: -1. Defining a list of string arguments passed to the agent, available as **command line arguments** -2. Defining a list of key-value pairs passed to the agent, available as **environment variables**. -3. Defining dedicated configuration key-value pairs (experimental). +1. Defining key-value pairs available as **environment variables**. +2. Defining dedicated typed configuration values. -### Command line arguments and environment variables +### Environment variables - + -Command line arguments and environment variables are accessible through the `node:process` module: +Environment variables are accessible through the `node:process` module: ```typescript -import { argv, env } from "node:process"; +import { env } from "node:process"; +``` + + + +Environment variables are available through `node:process`: + +```typescript +import { env } from "node:process" ``` -Command line arguments and environment variables are accessible through the `std::env` module: +Environment variables are accessible through the `std::env` module: ```rust use std::env; let agent_id = env::var("GOLEM_AGENT_ID").unwrap(); @@ -527,9 +628,8 @@ val agentId = env("GOLEM_AGENT_ID") -Command line arguments and environment variables are accessible through the `@environment` module: +Environment variables are accessible through the `@environment` module: ```moonbit -let args = @environment.get_arguments() let env = @environment.get_environment() ``` @@ -541,7 +641,7 @@ Environment variables can be specified when an agent is **explicitly created**, - `GOLEM_AGENT_ID` - the ID of the agent - `GOLEM_AGENT_TYPE` - the agent type (first part of the agent ID) - `GOLEM_COMPONENT_ID` - the ID of the agent's component -- `GOLEM_COMPONENT_VERSION` - the version of the component used for this agent +- `GOLEM_COMPONENT_REVISION` - the revision of the component used for this agent In addition to these, when using [Agent to Agent communication](/next/develop/rpc), agents created by remote calls **inherit the environment variables of the caller**. @@ -562,13 +662,7 @@ components: ### Per-agent configuration -In Golem 1.5, configuration is primarily per-agent rather than per-component. The configuration follows a cascade hierarchy: - -``` -componentTemplates → components → agents → presets -``` - -Each level can define `env` (environment variables) and `files` (initial filesystem), with more specific levels overriding more general ones. +Configuration resolves from the component template and component first. Agent templates are then applied, followed by agent properties, environment presets, and finally custom presets. Each level can define `env` (environment variables) and `files` (initial filesystem), with later, more specific levels overriding earlier ones according to their merge modes. #### Agent-level configuration in golem.yaml diff --git a/docs/src/content/next/develop/durability.mdx b/docs/src/content/next/develop/durability.mdx index 602db85623..42d8cd647f 100644 --- a/docs/src/content/next/develop/durability.mdx +++ b/docs/src/content/next/develop/durability.mdx @@ -10,12 +10,15 @@ The high-level SDKs let applications control idempotence, define atomic regions, All these features are regional - they can be changed for a section of the code within a single exported function. - + In TypeScript, the SDK provides scoped helpers such as `withIdempotenceMode` and `withRetryPolicy`. They run a sync or async callback with the requested setting, then restore the previous settings afterwards. + + In Effect TypeScript, `Durability.withIdempotenceMode` scopes the setting to an Effect and restores it when the Effect completes, fails, or is interrupted. Other durability controls such as `atomically` and `oplogCommit` return Effect values. + The `golem_rust` crate provides scoped helpers such as `with_idempotence_mode`, with `_async` variants for async code. They execute a closure with the requested setting, then restore the @@ -51,13 +54,15 @@ If a live attempt traps before it is finished, recovery retries the **whole cust Requests, responses, operation names, and observational host calls can be visible to oplog search, logs, metrics, and other observability tooling. Record useful names and schemas, but redact secrets and avoid putting unnecessary or unbounded payloads into observable values. -Use the SDK combinator for this lifecycle instead of managing the affine resource directly: `golem_rust::durability::Durability::run*` in Rust, `durable` in TypeScript, `DurabilityApi.durable`/`durableAsync` in Scala, or `@api.durable`/`durable_async` in MoonBit. Each combinator starts the invocation before evaluating the body, skips the body on replay, finishes after the complete live body returns, and drops an unfinished resource if the body fails. +Use the SDK combinator for this lifecycle instead of managing the affine resource directly: `golem_rust::durability::Durability::run*` in Rust, `durable` in TypeScript, `Durability.wrap`/`wrapInfallible` in Effect TypeScript, `DurabilityApi.durable`/`durableAsync` in Scala, or `@api.durable`/`durable_async` in MoonBit. Each combinator starts the invocation before evaluating the body, skips the body on replay, finishes after the complete live body returns, and drops any live resource left unfinished. + +In Effect TypeScript, `Durability.wrap` records both successes and typed failures described by its error schema. Defects, interruption, and response-encoding failures leave the live invocation unfinished. `Durability.wrapInfallible` accepts an Effect with no typed error channel. ### Idempotence mode Golem assumes that HTTP requests are idempotent by default. This means that in case of a failure, if the system cannot determine whether the request successfully reached the target or not, it will be retried. - + This behavior can be changed using the `withIdempotenceMode` function: @@ -70,6 +75,19 @@ Golem assumes that HTTP requests are idempotent by default. This means that in c }) ``` + + Use `Durability.withIdempotenceMode` to scope the setting to an Effect: + + ```typescript + import { Effect } from "effect" + import { Durability } from "@golemcloud/effect-golem" + + const result = Durability.withIdempotenceMode( + false, + Effect.succeed("hello"), + ) + ``` + This behavior can be changed using the `with_idempotence_mode` function: @@ -115,7 +133,7 @@ With disabled idempotence mode, in case Golem cannot determine if the request wa By default, side effects are persisted and retried one by one. It is possible to group them together into atomic regions, in which case the execution is retried for some reason (the agent failed or interrupted within the region), all the side effects will be reexecuted. - + The `golem-ts-sdk` library exports the `atomically` function for using atomic regions: @@ -129,6 +147,22 @@ By default, side effects are persisted and retried one by one. It is possible to }) ``` + + `Durability.atomically` runs an Effect in an atomic region: + + ```typescript + import { Effect } from "effect" + import { Durability } from "@golemcloud/effect-golem" + + const result = Durability.atomically( + Effect.gen(function* () { + const first = yield* firstSideEffect + const second = yield* secondSideEffect(first) + return [first, second] as const + }), + ) + ``` + The `golem_rust` crate exports the `atomically` function for using atomic regions: @@ -174,7 +208,7 @@ By default, side effects are persisted and retried one by one. It is possible to ### Commit oplog - + Wait until the oplog is replicated to a specified number of replicas before continuing: @@ -188,6 +222,15 @@ By default, side effects are persisted and retried one by one. It is possible to Wait until the oplog is replicated to a specified number of replicas before continuing: + ```typescript + import { Durability } from "@golemcloud/effect-golem" + + const committed = Durability.oplogCommit(3) + ``` + + + Wait until the oplog is replicated to a specified number of replicas before continuing: + ```rust use golem_rust::oplog_commit; diff --git a/docs/src/content/next/develop/durable-streams.mdx b/docs/src/content/next/develop/durable-streams.mdx new file mode 100644 index 0000000000..a9003ea73e --- /dev/null +++ b/docs/src/content/next/develop/durable-streams.mdx @@ -0,0 +1,183 @@ +import { Callout, Tabs } from "nextra/components" + +# External Durable Streams + +The guest SDKs can read and write services implementing the [Durable Streams protocol](https://github.com/durable-streams/durable-streams). Golem owns HTTP transport, authentication, and durable host-call recording; your agent owns checkpoints, producer identity, sequence progress, buffering, and retry policy. + +This page covers streams hosted outside the agent invocation protocol. For operating method input/output stream sessions exposed by Golem, see [Operating Durable Stream Sessions](/next/operate/durable_streams). + +## Read and write + +All five SDKs support JSON-message streams and byte streams. JSON appends are batches of complete logical values. Byte streams expose a sequence of bytes; HTTP batch and append boundaries are not preserved. In the examples, `url` identifies an already-created, open `application/json` stream; constructing an SDK reader or writer does not create the provider stream. + + + +```typescript +import { z } from 'zod'; +import { + createDurableJsonWriter, + readDurableJsonStream, +} from '@golemcloud/golem-ts-sdk'; + +const writer = createDurableJsonWriter(z.string(), { url }); +await writer.append(['first', 'second']); +await writer.close(); + +for await (const value of readDurableJsonStream(z.string(), { + url, + offset: '-1', + live: 'long-poll', +})) { + console.log(value); +} +``` + +Use `createDurableByteWriter` and `readDurableByteStream` for bytes. TypeScript serializes operations and resolves a retained request before another append or disposal. `dispose()` does not create a separate close request, but it can complete a retained append-and-close and can fail if that retry fails. + + +```typescript +import { Effect, Schema, Stream } from 'effect'; +import { DurableStreams } from '@golemcloud/effect-golem'; + +const program = Effect.gen(function* () { + const writer = yield* DurableStreams.makeJsonWriter(Schema.String, { url }); + yield* writer.append(['first', 'second'], { close: true }); + return yield* Stream.runCollect( + DurableStreams.readJson(Schema.String, { url, offset: '-1' }), + ); +}).pipe(Effect.scoped); +``` + +This effect runs inside a Golem agent handler, where the dispatcher supplies the host service; `Effect.scoped` supplies resource scope, not that service. Writers are scoped resources. Use `makeByteWriter` and `readBytes` for bytes. Host and protocol failures use `DurableStreams.DurableStreamError`; schema compilation and conversion can fail with separate typed errors. + + +```rust +use golem_rust::durable_streams::{ + DurableStreamWriter, ExternalDurableStream, ReadOptions, WriteOptions, +}; + +let mut writer = DurableStreamWriter::new( + url.clone(), + "application/json", + WriteOptions::default(), +)?; +writer.append_json(&["first", "second"], true).await?; + +let mut reader = ExternalDurableStream::::json(url, ReadOptions::default()); +while let Some(value) = reader.next().await? { + println!("{value}"); +} +``` + +Use `ExternalDurableStream::::bytes` and `append_bytes` for bytes. `reader.checkpoint()` returns the last fully drained checkpoint. `writer.retry_pending()` resolves an uncertain append. + + +```scala +import golem.streams.{DurableStreams, DurableStreamReadOptions} +import scala.concurrent.ExecutionContext + +given ExecutionContext = ExecutionContext.parasitic + +val producer = DurableStreams.newProducer() +val writer = DurableStreams.jsonWriter[String](url, producer) + +for { + _ <- writer.append(Vector("first", "second"), close = true) + values = DurableStreams.json[String](url, DurableStreamReadOptions()) +} yield values +``` + +The result contains a lazy `AgentStream[String]`; it does not collect the messages. `DurableStreams.bytes` and `byteWriter` handle bytes. A writer retains an uncertain append; call `retryPending()` before new data and `dispose()` to release without remotely closing. + + +```moonbit +// In an async function, with golemcloud/golem_sdk/durable_streams imported. +let writer : @durable_streams.Writer[String] = + @durable_streams.json_writer(url) +ignore(writer.append(["first", "second"], close=true)) + +let reader : @durable_streams.Reader[String] = + @durable_streams.json_reader(url, offset="-1") +for ;; { + match reader.read() { + Some(value) => println(value) + None => break + } +} +``` + +Use `byte_reader` and `byte_writer` for bytes. `retry_pending()` retries the retained request exactly; `close()` consumes a sequence for a close-only append. + + + +## Checkpoints and offsets + +A checkpoint contains a server-generated `offset` and optional transport `cursor`. Treat both as opaque strings: + +- `-1` starts from the service's beginning/default position. +- `now` asks the service to resolve the current tail once during catch-up. +- The SDK starts in catch-up mode, advances to the returned checkpoint only after the entire batch is consumed, then switches to long polling or SSE at the tail. +- A pending or partially consumed batch does not promote its checkpoint. A still-live reader may retain the batch, but cancellation or close can discard it, and cancellation need not immediately abort the underlying HTTP request. + +Rust and MoonBit expose the last fully drained checkpoint. TypeScript, Effect, and Scala high-level readers do not expose it; restart those readers from an application-supplied offset. An Effect stream is a reusable description, and each execution creates a new reader from its options. During ordinary durable-agent recovery, Golem reconstructs progress by replay; do not snapshot native resource handles. + +## Producer identity and idempotency + +A writer is identified by `(producer ID, epoch)` and sends a monotonically increasing sequence per append request. Sequence counts requests, not JSON messages or bytes. SDKs generate one durable producer ID when omitted. + +For exactly-once retry deduplication, the remote service must atomically retain producer state with appended data and retain that state throughout the retry window. If a response is lost, retry the **same producer ID, epoch, sequence, payload, and close flag**. SDKs retain that uncertain request; TypeScript resolves it before another append, while the other SDKs require explicit pending-request recovery before different data. + +Golem records each completed host attempt and replays its result, but exactly-once external publication still depends on provider persistence and deduplication retention. With Golem idempotence disabled, recovery of an interrupted append can fail closed rather than risk changing the request. + +## Append and close + +- A JSON append contains zero or more separately encoded complete values; an array-valued application message remains one value inside the protocol's outer batch. +- A byte append contains exact bytes, but readers do not recover append boundaries. +- Empty data is valid only when `close` is true. +- Append-and-close commits data and closure atomically. Close-only also consumes one producer sequence. +- A successful receipt reports the acknowledged epoch, sequence, closed state, and possibly the next offset. Duplicate acknowledgements may omit the offset. +- Dropping a host resource never rolls back an append. Release APIs differ: TypeScript `dispose()` first resolves any retained request, which can complete a pending append-and-close; other SDKs expose different explicit release behavior. + +## Fencing and concurrent use + +A provider-accepted higher epoch at sequence zero fences older epochs for the same stream and producer ID while that producer state remains retained. Use a new epoch only when deliberately taking ownership of a producer ID. `fenced` and `sequence-conflict` are safety failures. `producer-diverged` means the provider acknowledged a sequence ahead of this writer; it does not prove that equal-sequence retries carried equal payloads. + +Each SDK writer serializes appends and permits only one uncertain request. Do not pipeline one writer or operate it concurrently. Independent readers can use independent checkpoints, but each reader object is affine/single-reader. + +Forking a Golem agent copies producer identity and pending state; it does not fork the external stream or allocate another producer. Assign a distinct producer ID in each intended branch. A producer ID generated before the fork is inherited by both branches. Reconcile any inherited uncertain request before moving either branch to a different identity, because the original request may already have been published. + +## Errors and retries + +The common error kinds are: + +| Category | Kinds | Guidance | +| --- | --- | --- | +| Request or protocol | `invalid-request`, `protocol-error`, `payload-too-large` | Fix the request or peer implementation | +| Access or lifecycle | `permission-denied`, `not-found`, `gone`, `closed` | Refresh authorization or stop using that URL | +| Producer safety | `sequence-conflict`, `fenced`, `producer-diverged` | Reconcile producer ownership and retained state | +| Transient | `timeout`, `transport`, `rate-limited`, `unavailable` | Retry with bounded exponential delay and honor `retry-after` | + +SDK defaults retry only transient failures. One HTTP attempt's timeout is separate from each SDK's retry limit; retry-count and wall-clock-budget policies differ by SDK. An error is never EOF; only a batch marked closed ends a reader normally. + +Stream retention and expiry are controlled by the external service, not Golem. An expired or deleted URL typically reports `gone` or `not-found`; `gone` can also mean a checkpoint precedes retained data. There is no separate guest-side keepalive guarantee, although provider-defined reads or writes may refresh a sliding TTL. Persist only provider-supported checkpoints and follow that provider's expiry policy. + +## Authentication + +Every SDK accepts an optional string-valued Golem secret handle for bearer authentication. The host borrows the capability and sends the credential without revealing plaintext to guest code. The descriptor records pinned secret identity, not the borrowed handle or plaintext. The agent still needs network permission, secret `Reveal` authority, and the applicable binding scope. + +## External streams or method streams? + +| Use | External Durable Streams | Agent method streams | +| --- | --- | --- | +| Stream owner | External protocol service | A Golem agent invocation | +| Address | Provider URL | Typed SDK stream values; session and slot URLs for external HTTP clients | +| Progress | Provider offsets/cursors managed by the guest client | Golem session checkpoints and attachment journals | +| Producer identity | Explicit ID, epoch, and sequence | Managed for agent-to-agent SDK streams; external session writers supply producer tuples | +| Best for | Shared logs and ingress/egress services whose lifecycle belongs to another service | Typed agent input/output and streams owned by one invocation, including output after method return | +| Operations | Guest SDK plus provider lifecycle | [Operator session API](/next/operate/durable_streams) and method stream SDK types | + +Use an external stream when the URL and retention lifecycle belong to another service. Use a method stream when the producer or consumer is part of a typed Golem invocation. Bridging between them is valid, but preserve backpressure and cancellation and do not assume their offsets are interchangeable. + + + A stream resource is not a snapshot-safe data structure. Construct it through deterministic agent execution and let replay rebuild it. A checkpoint does not capture a partially consumed batch, and producer ID and epoch alone do not restore a writer: recovery must also account for buffered progress, sequence progress, and any uncertain request. The SDKs do not expose a universal snapshot/restore API for these objects. + diff --git a/docs/src/content/next/develop/forking.mdx b/docs/src/content/next/develop/forking.mdx index 35e679778a..3e8380cb5c 100644 --- a/docs/src/content/next/develop/forking.mdx +++ b/docs/src/content/next/develop/forking.mdx @@ -8,7 +8,9 @@ Golem agents are single threaded. To achieve parallel execution, it is possible A simpler way is to use the **fork API**. The fork API consists of a single host function, defined as the following: - +The declarations below show the public interface shape; use the SDK calls shown in the usage examples rather than copying these declarations into an application. + + ```typescript declare module 'golem:api/host@1.5.0' { @@ -44,6 +46,17 @@ declare module 'golem:api/host@1.5.0' { ``` + +Effect TypeScript exposes the host result type as `Agents.ForkResult`. `Agents.fork` is an Effect because host failures use the Effect error channel: + +```typescript +import type { Agents } from "@golemcloud/effect-golem" + +type ForkResult = Agents.ForkResult +// Agents.fork: Effect.Effect +``` + + `fork` and `ForkResult` can be imported from `golem_rust::fork` and `golem_rust::ForkResult` respectively @@ -66,7 +79,7 @@ pub enum ForkResult { /// The phantom ID of the forked agent is returned in `fork-result` on both sides. /// The newly created agent continues running from the same point, but the return value is going to be different /// in this agent and the forked agent. -pub fn fork() -> ForkResult; +pub fn fork() -> Result; ``` @@ -90,13 +103,13 @@ def fork(): ForkResult = HostApi.fork() -Assuming the `golemcloud/golem_sdk/interface/golem/core/types` package is imported as `@rpcTypes`, the `fork` host function and the related types are available from `golemcloud/golem_sdk/interface/golem/api/host`: +Assuming `golemcloud/golem_sdk/api` is imported as `@api` and `golemcloud/golem_sdk/interface/golem/core/types` as `@rpcTypes`, the facade exposes an unwrapped `fork` function. The related type shapes below are illustrative: ```moonbit /// Details about the fork result pub(all) struct ForkDetails { forked_phantom_id : @rpcTypes.Uuid -} derive(Show, Eq) +} derive(Debug, Eq) /// Indicates which agent the code is running on after `fork`. /// The parameter contains details about the fork result, such as the phantom-ID of the newly @@ -104,7 +117,7 @@ pub(all) struct ForkDetails { pub(all) enum ForkResult { Original(ForkDetails) Forked(ForkDetails) -} derive(Show, Eq) +} derive(Debug, Eq) /// Forks the current agent at the current execution point. /// The new agent gets the same base agent ID but with a new unique phantom ID. @@ -122,7 +135,7 @@ Using this `fork` function from a component that was created from Golem's built- The following code snippet demonstrates calling `fork` and continuing on two different parallel branches based on its result value: - + ```typescript import { z } from 'zod'; @@ -158,6 +171,25 @@ The following code snippet demonstrates calling `fork` and continuing on two dif ``` + ```typescript + import { Effect } from "effect" + import { Agents } from "@golemcloud/effect-golem" + + const run = Effect.gen(function* () { + const result = yield* Agents.fork + + switch (result.tag) { + case "original": + // Continue in the original agent. + break + case "forked": + // Continue in the forked agent. + break + } + }) + ``` + + ```rust use golem_rust::{agent_definition, agent_implementation, fork, ForkResult}; @@ -178,7 +210,7 @@ impl ExampleAgent for ExampleAgentImpl { } fn run(&self) -> Result<(), String> { - match fork() { + match fork().map_err(|err| err.to_string())? { ForkResult::Original(_) => { // ... Ok(()) @@ -256,7 +288,7 @@ The high level idea is the following: The following code snippet demonstrates this pattern: - + ```typescript import { z } from 'zod'; @@ -321,6 +353,29 @@ The following code snippet demonstrates this pattern: Note that **Golem promises** are NOT JavaScript Promises. The SDK wraps waiting for them in async helpers such as `awaitPromise` and `HostApi.awaitPromiseJson`, but the promise itself is still a Golem runtime resource identified by a `PromiseId`. + + + ```typescript + import { Effect } from "effect" + import { Agents } from "@golemcloud/effect-golem" + + const run = Effect.gen(function* () { + const promiseId = yield* Agents.Promises.create + + switch ((yield* Agents.fork).tag) { + case "original": { + const rawResult = yield* Agents.Promises.await(promiseId) + console.log(new TextDecoder().decode(rawResult)) + break + } + case "forked": { + const result = new TextEncoder().encode("Hello from the forked agent") + yield* Agents.Promises.complete(promiseId, result) + break + } + } + }) + ``` ```rust @@ -354,7 +409,7 @@ impl ExampleAgent for ExampleAgentImpl { async fn run(&self) -> Result<(), String> { let promise_id = create_promise(); - match fork() { + match fork().map_err(|err| err.to_string())? { ForkResult::Original(_) => { let local_result = RunResult { message: format!("Hello from original agent with id: {}", self.id), @@ -464,7 +519,7 @@ fn bytes_to_string(bytes : Bytes) -> String { String::from_array(chars) } -pub fn ExampleAgent::run(self : Self) -> Unit { +pub async fn ExampleAgent::run(self : Self) -> Unit { let promise_id = @api.create_promise() match @api.fork() { diff --git a/docs/src/content/next/develop/http.mdx b/docs/src/content/next/develop/http.mdx index c00a98312a..0ed2204193 100644 --- a/docs/src/content/next/develop/http.mdx +++ b/docs/src/content/next/develop/http.mdx @@ -2,7 +2,7 @@ import { Callout, Tabs } from "nextra/components" # HTTP requests - + HTTP requests can be made with the standard `fetch` function. @@ -26,6 +26,36 @@ import { Callout, Tabs } from "nextra/components" ``` + + Effect TypeScript agents can use the standard `fetch` function and wrap its Promise in an Effect: + + ```typescript + import { Effect } from "effect" + + const request = Effect.tryPromise({ + try: () => fetch(`http://localhost:${port}/todos`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + title: "foo", + body: "bar", + userId: 1, + }), + }), + catch: (cause) => new Error("HTTP request failed", { cause }), + }).pipe( + Effect.flatMap((response) => + Effect.tryPromise({ + try: () => response.json(), + catch: (cause) => new Error("Invalid JSON response", { cause }), + }), + ), + ) + ``` + + This constructs a lazy Effect. Return it from an agent method, or evaluate it with `yield* request` inside `Effect.gen`. + + HTTP requests can be made using the `wstd` crate. Make sure to have the crate added to your `Cargo.toml`: @@ -127,7 +157,7 @@ import { Callout, Tabs } from "nextra/components" http = "1.4" progenitor = { git = "https://github.com/golemcloud/progenitor-wasi-http" } serde = { version = "1.0", features = ["derive"] } - wstd = "=0.6.5" + wstd = "=0.5.4" ``` Then expand the client directly into your source file: @@ -175,7 +205,7 @@ import { Callout, Tabs } from "nextra/components" http = "1.4" progenitor-client = { git = "https://github.com/golemcloud/progenitor-wasi-http" } serde = { version = "1.0", features = ["derive"] } - wstd = "=0.6.5" + wstd = "=0.5.4" [build-dependencies] prettyplease = "0.2" @@ -223,10 +253,17 @@ import { Callout, Tabs } from "nextra/components" To generate a self-contained client crate that can be committed and reused independently, use the `cargo-progenitor` CLI: ```bash - cargo install cargo-progenitor + cargo install --git https://github.com/golemcloud/progenitor-wasi-http cargo-progenitor cargo progenitor -i openapi/my-api.json -o ./my-api-client -n my-api-client -v 0.1.0 ``` + In the generated crate's `Cargo.toml`, use the WASI fork of the runtime client and add the `http` dependency used by generated methods: + + ```toml + progenitor-client = { git = "https://github.com/golemcloud/progenitor-wasi-http" } + http = "1.4" + ``` + See the [`progenitor-wasi-http` README](https://github.com/golemcloud/progenitor-wasi-http) and the [`example-build`](https://github.com/golemcloud/progenitor-wasi-http/tree/main/example-build) and [`example-macro`](https://github.com/golemcloud/progenitor-wasi-http/tree/main/example-macro) directories for complete worked examples, including pagination and customizing the generated builder API. @@ -267,73 +304,16 @@ import { Callout, Tabs } from "nextra/components" - HTTP requests can be made using generated `wasi:http` bindings. Assuming your generated `wasi:http/types` bindings are imported as `@http`, your `wasi:http/outgoingHandler` bindings are imported as `@outgoingHandler`, and `moonbitlang/core/json` is imported as `@json`: + The current MoonBit SDK provides an async wrapper around Preview 3 `wasi:http`. Import `golemcloud/golem_sdk/http` as `@http`, construct an `@http.Request` with its generated accessors, and pass it to `@http.send`: ```moonbit - struct ExampleRequest { - title : String - body : String - userId : Int - } derive(ToJson) - - fn string_to_bytes(value : String) -> FixedArray[Byte] { - let chars = value.to_array() - let bytes = FixedArray::make(chars.length(), b'\x00') - for i in 0.. String { - let chars : Array[Char] = [] - for i in 0.. Result[@http.Response, @http.ErrorCode] { + @http.send(request) } - - let request_body = ExampleRequest::{ - title: "foo", - body: "bar", - userId: 1, - } - - let headers = @http.Fields::from_list( - [("Content-Type", string_to_bytes("application/json"))], - ).unwrap() - - let request = @http.OutgoingRequest::outgoing_request(headers) - let _ = request.set_method(@http.Post) - let _ = request.set_scheme(Some(@http.Http)) - let _ = request.set_authority(Some("localhost:\{port}")) - let _ = request.set_path_with_query(Some("/todos")) - - let body = request.body().unwrap() - let output_stream = body.write().unwrap() - output_stream.blocking_write_and_flush( - string_to_bytes(request_body.to_json().stringify()), - ).unwrap() - output_stream.drop() - @http.OutgoingBody::finish(body, None).unwrap() - - let future_response = @outgoingHandler.handle(request, None).unwrap() - let pollable = future_response.subscribe() - pollable.block() - let response = future_response.get().unwrap().unwrap().unwrap() - - let incoming_body = response.consume().unwrap() - let stream = incoming_body.stream().unwrap() - let bytes = stream.blocking_read(1048576UL).unwrap() - stream.drop() - ignore(@http.IncomingBody::finish(incoming_body)) - - let data = @json.parse(bytes_to_string(bytes)) catch { - _ => panic() - } - println("Body: \{data.stringify()}") ``` + This is the exact SDK wrapper signature. Request bodies and response bodies are async byte streams; consume the full response stream before decoding JSON, and use `moonbitlang/core/encoding/utf8` rather than character-to-byte casts. + diff --git a/docs/src/content/next/develop/observability.mdx b/docs/src/content/next/develop/observability.mdx index 3396fc1ad7..6d2d52cc1c 100644 --- a/docs/src/content/next/develop/observability.mdx +++ b/docs/src/content/next/develop/observability.mdx @@ -31,7 +31,7 @@ The following plugin configuration keys are available: ### Custom spans - + Create custom spans using the `golem:api/context` interface: @@ -44,6 +44,18 @@ The following plugin configuration keys are available: span.finish(); ``` + + The Effect SDK installs its Golem-backed tracer automatically. Use normal Effect spans: + + ```typescript + import { Effect } from "effect" + + const operation = Effect.gen(function* () { + yield* Effect.annotateCurrentSpan("env", "production") + // ... do work ... + }).pipe(Effect.withSpan("my-operation")) + ``` + Create custom spans using the `golem::api::context` module: @@ -102,6 +114,19 @@ const result = dc.traceSync( ); ``` +For Effect TypeScript agents, prefer Effect's tracing APIs; the SDK wires them to Golem invocation context automatically. For host context details or W3C propagation headers, use `Tracing.currentContext` or `Tracing.traceContextHeaders`: + +```typescript +import { Effect } from "effect" +import { Tracing } from "@golemcloud/effect-golem" + +const context = Effect.gen(function* () { + const snapshot = yield* Tracing.currentContext + const headers = yield* Tracing.traceContextHeaders + return { snapshot, headers } +}) +``` + ### Logs When log exporting is enabled via the `signals` parameter, `stdout`/`stderr` output and dedicated log calls are forwarded to the OTLP collector. Standard logging APIs work out of the box — `console.log` in TypeScript, `println!` in Rust, and so on. diff --git a/docs/src/content/next/develop/promises.mdx b/docs/src/content/next/develop/promises.mdx index 77739195d8..89daaa028e 100644 --- a/docs/src/content/next/develop/promises.mdx +++ b/docs/src/content/next/develop/promises.mdx @@ -10,7 +10,7 @@ When a promise is completed, an arbitrary byte array can be attached to it as a ### Creating a promise - + To create a promise simply call the `createPromise` function: @@ -23,6 +23,18 @@ When a promise is completed, an arbitrary byte array can be attached to it as a } ``` + + `Agents.Promises.create` is an Effect that creates a promise: + + ```typescript + import { Effect } from "effect" + import { Agents } from "@golemcloud/effect-golem" + + const createPromiseId = Effect.gen(function* () { + return yield* Agents.Promises.create + }) + ``` + To create a promise simply call the `create_promise` function: @@ -61,7 +73,7 @@ When a promise is completed, an arbitrary byte array can be attached to it as a The returned value has the type `PromiseId`, and defined as the following (including the nested types): - + ```typescript export type OplogIndex = bigint @@ -91,6 +103,16 @@ The returned value has the type `PromiseId`, and defined as the following (inclu ``` + + Effect TypeScript re-exports the host `PromiseId` type as `Agents.PromiseId`. Its wire shape is the same Golem agent ID and oplog index structure shown in the TypeScript tab. Prefer the exported type rather than duplicating that structure: + + ```typescript + import type { Agents } from "@golemcloud/effect-golem" + + type PromiseId = Agents.PromiseId + ``` + + ```rust pub type OplogIndex = u64; @@ -187,7 +209,7 @@ The returned value has the type `PromiseId`, and defined as the following (inclu ### Awaiting a promise - + To await a promise, use the async `awaitPromise` function, which returns the promise result as a byte array payload. Here's an example that awaits a promise, then decodes the payload from JSON format: @@ -206,6 +228,24 @@ The returned value has the type `PromiseId`, and defined as the following (inclu ``` + `Agents.Promises.await` returns an Effect containing the raw byte payload: + + ```typescript + import { Effect, Schema } from "effect" + import { Agents } from "@golemcloud/effect-golem" + + const MyPayload = Schema.Struct({ id: Schema.String }) + + const waitForPayload = (promiseId: Agents.PromiseId) => + Agents.Promises.await(promiseId).pipe( + Effect.map((bytes) => new TextDecoder().decode(bytes)), + Effect.flatMap( + Schema.decodeUnknownEffect(Schema.fromJsonString(MyPayload)), + ), + ) + ``` + + To await a promise, either use the blocking `blocking_await_promise` or `blocking_await_promise_json` functions, or in async context, the recommended `await_promise` or `await_promise_json` functions. The JSON variants are decoding the completed promise's payload from JSON format, while the non-JSON variants are returning the raw byte array. @@ -255,7 +295,7 @@ The returned value has the type `PromiseId`, and defined as the following (inclu String::from_array(chars) } - fn wait_for_payload(promise_id : @api.PromiseId) -> MyPayload { + async fn wait_for_payload(promise_id : @api.PromiseId) -> MyPayload { let payload = @api.await_promise(promise_id) let json = @json.parse(bytes_to_string(payload)) catch { _ => panic() @@ -273,7 +313,7 @@ Note that if an agent is only **awaiting** Golem promises (one or more), the age ### Completing a promise from within an agent - + To complete a promise from within an agent, use the `completePromise` function. The following example completes a promise with a value encoded as JSON: @@ -292,6 +332,20 @@ Note that if an agent is only **awaiting** Golem promises (one or more), the age ``` + `Agents.Promises.complete` returns an Effect. It succeeds with `true`, or fails with `PromiseAlreadyCompletedError` if the promise was already completed. + + ```typescript + import { Agents } from "@golemcloud/effect-golem" + + const completeFromAgent = (promiseId: Agents.PromiseId) => { + const payload = new TextEncoder().encode( + JSON.stringify({ id: "value", meta: "data" }), + ) + return Agents.Promises.complete(promiseId, payload) + } + ``` + + To complete a promise from within an agent, use the `complete_promise` (payload is a byte array) or the `complete_promise_json` function. ```rust diff --git a/docs/src/content/next/develop/rdbms.mdx b/docs/src/content/next/develop/rdbms.mdx index 69502d7470..47a07dc614 100644 --- a/docs/src/content/next/develop/rdbms.mdx +++ b/docs/src/content/next/develop/rdbms.mdx @@ -10,9 +10,9 @@ The currently supported databases are: - Apache Ignite 2
- See the full API definition + See selected API types and signatures - + ```ts declare module 'golem:rdbms/types@1.5.0' { @@ -235,6 +235,18 @@ The currently supported databases are: ``` + + Effect TypeScript wraps these WIT interfaces with Effect SQL clients. Import the adapter for the selected database; do not import native Node database drivers: + + ```typescript + import { MySqlClient } from "@golemcloud/effect-golem/mysql" + import { PgClient } from "@golemcloud/effect-golem/postgres" + import { IgniteClient } from "@golemcloud/effect-golem/ignite2" + ``` + + Each adapter implements the canonical `SqlClient` API from `effect/unstable/sql`, including tagged SQL, schema integration, streaming, and `withTransaction`. + + In generated bindings, find the following modules under `golem_rust::bindings::golem::rdbms`: @@ -446,19 +458,19 @@ The currently supported databases are: year : Int month : Byte day : Byte - } derive(Show, Eq) + } derive(Debug, Eq) pub(all) struct Time { hour : Byte minute : Byte second : Byte nanosecond : UInt - } derive(Show, Eq) + } derive(Debug, Eq) pub(all) struct Timestamp { date : Date time : Time - } derive(Show, Eq) + } derive(Debug, Eq) // In `@postgres`: pub(all) suberror Error_ { @@ -467,13 +479,13 @@ The currently supported databases are: QueryExecutionFailure(String) QueryResponseFailure(String) Other(String) - } derive(Show, Eq) + } derive(Debug, Eq) pub(all) struct SparseVec { dim : Int indices : FixedArray[Int] values : FixedArray[Float] - } derive(Show, Eq) + } derive(Debug, Eq) pub(all) enum DbValue { Text(String) @@ -489,7 +501,7 @@ The currently supported databases are: Sparsevec(SparseVec) Null // ... - } derive(Show, Eq) + } derive(Debug, Eq) pub fn DbResultStream::get_columns(self : DbResultStream) -> Array[DbColumn] pub fn DbResultStream::get_next(self : DbResultStream) -> Array[DbRow]? @@ -539,7 +551,7 @@ The currently supported databases are: QueryExecutionFailure(String) QueryResponseFailure(String) Other(String) - } derive(Show, Eq) + } derive(Debug, Eq) pub(all) enum DbValue { Varchar(String) @@ -548,7 +560,7 @@ The currently supported databases are: Json(String) Null // ... - } derive(Show, Eq) + } derive(Debug, Eq) pub fn DbResultStream::get_columns(self : DbResultStream) -> Array[DbColumn] pub fn DbResultStream::get_next(self : DbResultStream) -> Array[DbRow]? @@ -598,10 +610,12 @@ The currently supported databases are: ### Executing SQL statements -To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` resource and call `execute` on it: +To execute an SQL statement with `golem-rdbms`, first create a `db-connection` resource and call `execute` on it: + +The Effect examples below run inside a Golem Effect agent, whose runtime supplies the database host services. Creating an Effect does not execute it. Database failures use the `SqlError` error channel; handle them or map them to the agent method's declared error type. #### MySQL - + ```typescript import { DbConnection } from "golem:rdbms/mysql@1.5.0"; @@ -625,6 +639,26 @@ To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` re + + ```typescript + import { Effect } from "effect" + import { MySqlClient } from "@golemcloud/effect-golem/mysql" + + const createTable = Effect.gen(function* () { + const sql = yield* MySqlClient.make({ + connectionAddress: "mysql://root@localhost:3306/test", + }) + yield* sql` + CREATE TABLE IF NOT EXISTS test_users ( + user_id varchar(25) NOT NULL PRIMARY KEY, + name varchar(255) NOT NULL, + created_on timestamp NOT NULL DEFAULT NOW() + ) + ` + }).pipe(Effect.scoped) + ``` + + ```rust use golem_rust::bindings::golem::rdbms::mysql::*; @@ -683,6 +717,7 @@ To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` re // Connecting to the database called 'test' with user 'root' // Use appropriate error handling in real code let conn = @mysql.DbConnection::open("mysql://root@localhost:3306/test").unwrap() + defer conn.drop() let statement = #|CREATE TABLE IF NOT EXISTS test_users #| ( @@ -692,7 +727,7 @@ To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` re #| PRIMARY KEY (user_id) #| ); - @mysql.DbConnection::execute(conn, statement, []).unwrap() + let _ = @mysql.DbConnection::execute(conn, statement, []).unwrap() ``` @@ -704,7 +739,7 @@ To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` re #### PostgreSQL - + ```typescript import { DbConnection } from "golem:rdbms/postgres@1.5.0"; @@ -728,6 +763,26 @@ To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` re + + ```typescript + import { Effect } from "effect" + import { PgClient } from "@golemcloud/effect-golem/postgres" + + const createTable = Effect.gen(function* () { + const sql = yield* PgClient.make({ + connectionAddress: "postgresql://user@localhost:5432/test", + }) + yield* sql` + CREATE TABLE IF NOT EXISTS test_users ( + user_id varchar(25) NOT NULL PRIMARY KEY, + name varchar(255) NOT NULL, + created_on timestamp NOT NULL DEFAULT NOW() + ) + ` + }).pipe(Effect.scoped) + ``` + + ```rust use golem_rust::bindings::golem::rdbms::postgres::*; @@ -787,6 +842,7 @@ To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` re // Connecting to the database called 'test' with user 'user' // Use appropriate error handling in real code let conn = @postgres.DbConnection::open("postgresql://user@localhost:5432/test").unwrap() + defer conn.drop() let statement = #|CREATE TABLE IF NOT EXISTS test_users #| ( @@ -807,7 +863,7 @@ To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` re #### Apache Ignite - + ```typescript import { DbConnection } from "golem:rdbms/ignite2@1.5.0"; @@ -843,6 +899,35 @@ To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` re + + ```typescript + import { Effect } from "effect" + import { IgniteClient } from "@golemcloud/effect-golem/ignite2" + + const useIgnite = Effect.gen(function* () { + const sql = yield* IgniteClient.make({ + connectionAddress: "ignite://127.0.0.1:10800", + transformResultNames: (name) => name.toLowerCase(), + }) + yield* sql` + CREATE TABLE IF NOT EXISTS test_users ( + user_id varchar(25) NOT NULL PRIMARY KEY, + name varchar(255) NOT NULL + ) WITH "CACHE_NAME=TestUsers" + ` + const rows = yield* sql<{ user_id: string; name: string }>` + SELECT user_id, name FROM test_users WHERE name = ${"hello"} + ` + yield* sql.withTransaction( + sql`INSERT INTO test_users (user_id, name) VALUES (${"1"}, ${"Alice"})`, + ) + return rows + }).pipe(Effect.scoped) + ``` + + Ignite does not support nested `withTransaction` calls because it has no savepoints. + + ```rust use golem_rust::bindings::golem::rdbms::ignite2::*; @@ -888,7 +973,7 @@ To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` re // Connecting to an Apache Ignite 2 instance // Use appropriate error handling in real code val result = - Rdbms.Ignite2.open("ignite://localhost:10800").flatMap { conn => + Rdbms.Ignite.open("ignite://localhost:10800").flatMap { conn => conn.execute( """CREATE TABLE IF NOT EXISTS test_users | ( @@ -902,7 +987,7 @@ To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` re ``` - The functions in `golem.host.Rdbms.Ignite2` are **not async**. This is due to a limitation of the current Golem runtime, and are subject to change in future releases. + The functions in `golem.host.Rdbms.Ignite` are synchronous. @@ -913,6 +998,7 @@ To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` re // Connecting to an Apache Ignite 2 instance // Use appropriate error handling in real code let conn = @ignite.DbConnection::open("ignite://127.0.0.1:10800").unwrap() + defer conn.drop() let statement = #|CREATE TABLE IF NOT EXISTS test_users #| ( @@ -921,7 +1007,7 @@ To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` re #| PRIMARY KEY (user_id) #| ) WITH "CACHE_NAME=TestUsers"; - @ignite.DbConnection::execute(conn, statement, []).unwrap() + let _ = @ignite.DbConnection::execute(conn, statement, []).unwrap() // Querying data let result = @ignite.DbConnection::query( @@ -932,7 +1018,8 @@ To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` re // Using transactions let tx = conn.begin_transaction().unwrap() - tx.execute( + defer tx.drop() + let _ = tx.execute( "INSERT INTO test_users (user_id, name) VALUES (?, ?)", [@ignite.DbValue::DbString("1"), @ignite.DbValue::DbString("Alice")] ).unwrap() @@ -951,5 +1038,5 @@ To execute an SQL statement with `golem-rdbms`, first crete a `db-connection` re Additionally you can: - `query` executes a SQL statement and returns a result -- `query-stream` executs a SQL statement and returns a streaming result +- `query-stream` executes a SQL statement and returns a streaming result where the selected SDK exposes streaming - `begin-transaction` creates a transaction resource on which, in addition to the `query` and `execute` functions, there is also a `commit` and a `rollback` method. diff --git a/docs/src/content/next/develop/read-only-methods.mdx b/docs/src/content/next/develop/read-only-methods.mdx index a3de8e7a04..acf936ee8f 100644 --- a/docs/src/content/next/develop/read-only-methods.mdx +++ b/docs/src/content/next/develop/read-only-methods.mdx @@ -21,8 +21,8 @@ exactly what is and isn't checked. ### Enforced: writes, HTTP, and RPC trap The following all go through Golem's durability layer, and a read-only method **traps immediately** on any of -them — *before* the call runs and *before* any change is persisted — with a typed `ReadOnlyViolation` error that -carries the offending host function. User code can catch it and distinguish it from other failures. +them — *before* the call runs and *before* any change is persisted. The invocation fails with a typed +`ReadOnlyViolation` that identifies the agent method and offending host function. - writing or changing persistent state (key-value & blob storage, databases, promises, worker management, …), - making outgoing HTTP requests, @@ -73,9 +73,10 @@ The cache policy is part of the read-only marker — non-read-only methods canno | `ttl(d)` | The cached result expires after duration `d`, even if no write happened in the meantime. | | `no-cache` | The method runs every time. It is still side-effect-free, but its result is never cached. | -Invalidation for `until-write` happens automatically: any non-read-only invocation, update, restart, or replay on -the agent invalidates all `until-write` entries. `ttl(d)` entries additionally expire by time. `no-cache` never -inserts into the cache. +After a successful non-read-only invocation commits its completion marker, Golem invalidates cached read-only +results for that agent. While a mutation is queued or running, readers can still receive the previous cached +value. Updating or reverting the agent also invalidates its cache; evicting and reloading only the WebAssembly +instance does not. `ttl(d)` entries additionally expire by time. `no-cache` never inserts into the cache. ## Per-principal caching (automatic) @@ -83,20 +84,21 @@ By default a read-only result is **shared across all callers** — the cache key responses are marked `Cache-Control: public` (CDN-friendly). When the result depends on *who* is calling (for example, an authorization-scoped view), add a `Principal` -parameter to the method. The SDK detects it and **automatically** marks the read-only configuration as -principal-dependent (`uses_principal = true`) — there is no manual flag to set. The calling principal then becomes -part of the cache key, so each principal gets its own cached entry, and HTTP responses switch to -`Cache-Control: private` with `Vary: Authorization`. +parameter to the method. Rust, Scala, MoonBit, and Effect TypeScript detect it and **automatically** mark the +read-only configuration as principal-dependent (`uses_principal = true`). Plain TypeScript requires +`readOnly: { usesPrincipal: true }`, as shown below. The calling principal then becomes part of the cache key, so +each principal gets its own cached entry, and HTTP responses switch to `Cache-Control: private`. The response's +`Vary` header names the configured authentication header and any request headers bound as method inputs. -The `Principal` parameter is auto-injected by the runtime: it is not part of the method's input schema and callers -do not pass it (the same `Principal` mechanism used by authenticated HTTP endpoints). +The `Principal` parameter is marked as auto-injected in the method's input schema. The runtime supplies it; +callers do not pass it. ## Read-only and ephemeral are mutually exclusive Read-only methods read the agent's **shared, already-loaded state**. [Ephemeral agents](/next/develop/durability) get a fresh instance per invocation and have no shared state to read from, so a read-only method on an ephemeral -agent has no meaning. The SDKs reject this combination at **compile time**, and the host rejects it again when the -agent registers its types. +agent has no meaning. The host rejects this combination when the agent registers its types; individual SDKs may +also reject it earlier. ## What works in a read-only method @@ -117,7 +119,7 @@ regular (non-read-only) method instead. ## Marking a method read-only - + Mark a method read-only by setting `readOnly: true` on its `method(...)` spec: @@ -176,6 +178,53 @@ regular (non-read-only) method instead. } ``` + + Mark a method read-only with `readOnly: true`. Effect handlers return an `Effect`, and state held in a `Ref` must only be read from the read-only handler: + + ```typescript + import { Effect, Ref, Schema } from "effect" + import { defineAgent, method } from "@golemcloud/effect-golem" + + export const CounterAgent = defineAgent({ + name: "CounterAgent", + id: { name: Schema.String }, + methods: { + increment: method({ input: {}, success: Schema.Number }), + getCount: method({ input: {}, success: Schema.Number, readOnly: true }), + }, + }).implement({ + init: () => Ref.make(0), + methods: (count) => ({ + increment: () => Ref.updateAndGet(count, (value) => value + 1), + getCount: () => Ref.get(count), + }), + }) + ``` + + Use the object form for another cache policy. Principal-aware caching is inferred when the input contains `Principal.PrincipalSchema`: + + ```typescript + import { Principal } from "@golemcloud/effect-golem" + + const recentSummary = method({ + input: {}, + success: Summary, + readOnly: { cache: { ttlNanos: 30_000_000_000n } }, + }) + + const pureCompute = method({ + input: { x: Schema.Number, y: Schema.Number }, + success: Schema.Number, + readOnly: { cache: "no-cache" }, + }) + + const myVisibleItems = method({ + input: { principal: Principal.PrincipalSchema }, + success: Schema.Array(Item), + readOnly: true, + }) + ``` + Use the `#[read_only]` attribute on the method: @@ -319,23 +368,23 @@ cache policy and whether the method is principal-dependent: | Cache policy | Response headers | |---|---| | `no-cache` | `Cache-Control: no-store` | -| `until-write` | `Cache-Control: , no-cache` + `ETag: ":"` | -| `ttl(d)` | `Cache-Control: , max-age=` + `ETag: ":"` | +| `until-write` | `Cache-Control: , no-cache` + an opaque `ETag` | +| `ttl(d)` | `Cache-Control: , max-age=` + an opaque `ETag` | -Where `` is `private` (plus `Vary: Authorization`) when the method is principal-dependent (it takes a -`Principal` parameter), and `public` otherwise. +Where `` is `private` when the method is principal-dependent, and `public` otherwise. `Vary` always +names request headers bound as inputs; for principal-dependent methods it also names the configured authentication +header. -A request that arrives with `If-None-Match: ":"` is revalidated cheaply: if the index still -matches the agent's current state (no non-read-only invocation happened since), Golem returns `304 Not Modified` -**without invoking the executor** — giving a full agent-loading bypass even for cold agents whose state has not -changed. +A request that repeats the returned ETag in `If-None-Match` is revalidated cheaply. If it still matches the +agent's current agent-instance fingerprint and oplog position, Golem returns `304 Not Modified` without executing +the agent method or loading its WebAssembly instance. A response a CDN or browser sees for a `public`, `until-write` method therefore looks like: ```http HTTP/1.1 200 OK Cache-Control: public, no-cache -ETag: "counter-1:42" +ETag: "" Content-Type: application/json {"count":7} @@ -345,15 +394,14 @@ and a later conditional request short-circuits to: ```http GET /counter-1/count HTTP/1.1 -If-None-Match: "counter-1:42" +If-None-Match: "" HTTP/1.1 304 Not Modified Cache-Control: public, no-cache -ETag: "counter-1:42" +ETag: "" ``` Read-only methods bound to non-`GET`/`HEAD` verbs are allowed, but they do not get cache headers — caching a - `POST`/`PUT`/`DELETE` response is not meaningful. The worker-service logs a one-time warning on first - registration in that case. + `POST`/`PUT`/`DELETE` response is not meaningful. Deployment validation emits a warning for each such binding. diff --git a/docs/src/content/next/develop/retries.mdx b/docs/src/content/next/develop/retries.mdx index 15c1e05853..c0e6926694 100644 --- a/docs/src/content/next/develop/retries.mdx +++ b/docs/src/content/next/develop/retries.mdx @@ -4,22 +4,22 @@ import { Callout, Tabs } from "nextra/components" ### Using Golem's retry mechanism -Golem applies a retry mechanism to all agents. In case of a failure, Golem will automatically recover the agent to the point before the failure and retry the operation. An exponential backoff and an upper limit on the number of retries are applied. +Golem applies retry policies to retryable agent failures. The selected policy controls delays and retry limits. Permanent errors, permission failures, stack overflows, and deterministic traps outside atomic regions are not retried. If the maximum number of retries is reached, the agent will be marked as failed and no further invocations will be possible on it. -This mechanism is automatic and applied to all kinds of failures. To rely on it, throw an unhandled exception. +During recovery, Golem replays the oplog and reuses results already recorded for preceding durable operations. Raising an unhandled exception does not cause those earlier operations to execute again. - Golem will retry from the point the unhandled exception was thrown. It is possible that a previous operation's - unwanted result (if it did not end in an exception) has been already persisted, in which case it won't be retried. + Host retry policies govern retryable Golem operations and agent failures. They do not retry an arbitrary typed + failure inside an SDK's local effect abstraction; for example, `Retry.withPolicy` propagates `Effect.fail`. ### Customizing the retry policy The retry policy which controls the maximum number of retries and the exponential backoff is a global configuration of the Golem servers, but it can be customized for each agent. - + The `@golemcloud/golem-ts-sdk` package exports the `withRetryPolicy` function to temporarily install a named retry policy for a block of code: @@ -86,6 +86,28 @@ The retry policy which controls the maximum number of retries and the exponentia }; ``` + + The Effect SDK provides a typed retry-policy DSL and `Retry.withPolicy`, which installs a policy for one Effect. It restores the previous policy with the same name when its scope closes, or removes the temporary policy if none existed: + + ```typescript + import { Duration, Effect } from "effect" + import { Retry } from "@golemcloud/effect-golem" + + const policy = Retry.NamedPolicy.named( + "temporary-policy", + Retry.Policy.exponential(Duration.millis(100), 1.5) + .clamp(Duration.millis(100), Duration.seconds(2)) + .maxRetries(3), + ).priority(1000) + + const program = Retry.withPolicy( + policy, + Effect.succeed("hello"), + ) + ``` + + Use `Retry.setPolicy(policy)` instead when the policy should remain installed on the durable agent until it is overwritten or removed. + The `golem_rust` crate exports the `with_retry_policy` function to temporarily install a named retry policy for a block of code: @@ -209,48 +231,33 @@ The retry policy which controls the maximum number of retries and the exponentia @js.native sealed trait JsNamedRetryPolicy extends js.Object { def name: String = js.native - def priority: Int = js.native + def priority: Double = js.native def predicate: JsRetryPredicate = js.native def policy: JsRetryPolicyTree = js.native } ``` - Assuming the `golemcloud/golem_sdk/api` package is imported as `@api`, use the `with_retry_policy` function to temporarily install a retry policy for a block of code: + Assuming the `golemcloud/golem_sdk/api` package is imported as `@api`, use the high-level policy builders and `with_named_policy`: ```moonbit - let policy : @api.RetryPolicy = @api.RetryPolicy::{ - max_attempts: 3U, - min_delay: 100_000_000UL, // 100 milliseconds - max_delay: 2_000_000_000UL, // 2 seconds - multiplier: 1.5, - max_jitter_factor: None, - } - - let result : String = @api.with_retry_policy(policy, fn() { + let policy = @api.NamedPolicy::named( + "temporary-policy", + @api.Policy::exponential(@api.Duration::millis(100), 1.5) + .clamp(@api.Duration::millis(100), @api.Duration::seconds(2)) + .max_retries(3), + ).priority(1000) + + let result : String = @api.with_named_policy(policy, fn() { // this callback runs with the custom retry policy "hello" - }) - ``` - - The policy passed to `with_retry_policy` is a `RetryPolicy`, which contains the retry limit and backoff settings: - - ```moonbit - pub(all) struct RetryPolicy { - max_attempts : UInt - min_delay : UInt64 - max_delay : UInt64 - multiplier : Double - max_jitter_factor : Double? - } derive(Show, Eq) + }) catch { + error => raise error + } ``` - - MoonBit uses a simpler `RetryPolicy` struct with `max_attempts`, `min_delay`, `max_delay`, `multiplier`, and `max_jitter_factor` fields, rather than the tree-based `NamedRetryPolicy` used by TypeScript, Rust, and Scala. The named retry policy system described below applies to TypeScript, Rust, and Scala only. - - ### Named retry policies in golem.yaml Retry policies can also be defined declaratively in the application manifest (`golem.yaml`). These named retry policies are created in the environment during deployment, making them available to all agents without requiring code changes. @@ -258,9 +265,9 @@ Retry policies can also be defined declaratively in the application manifest (`g ```yaml retryPolicyDefaults: local: - - name: default-retry + default-retry: priority: 10 - predicate: "true" + predicate: true policy: countBox: maxRetries: 3 @@ -270,7 +277,7 @@ retryPolicyDefaults: factor: 2.0 ``` -### Policy types reference +### Manifest policy types reference The following policy types can be used to construct retry policy trees: @@ -291,7 +298,7 @@ The following policy types can be used to construct retry policy trees: | `union` | `[policy1, policy2]` | Union of two policies | | `intersect` | `[policy1, policy2]` | Intersection of two policies | -### Predicates reference +### Manifest predicates reference Predicates control when a retry policy is applied. The following predicate types are available: @@ -305,11 +312,18 @@ Predicates control when a retry policy is applied. The following predicate types | `propLt` / `propLte` | `{ property, value }` | Less than / less or equal | | `propExists` | string | Property exists | | `propIn` | `{ property, values }` | Property in set | -| `propMatches` | `{ property, pattern }` | Regex match | +| `propMatches` | `{ property, pattern }` | Glob match | | `propStartsWith` | `{ property, prefix }` | Starts with prefix | | `propContains` | `{ property, substring }` | Contains substring | | `and` | `[pred1, pred2]` | Logical AND | | `or` | `[pred1, pred2]` | Logical OR | | `not` | predicate | Logical NOT | -Predicate values are typed: `{ text: "..." }`, `{ integer: 42 }`, or `{ boolean: true }`. +Predicate values in the manifest are strings, integers, or booleans. For example: + +```yaml +predicate: + propEq: + property: status-code + value: 503 +``` diff --git a/docs/src/content/next/develop/rpc.mdx b/docs/src/content/next/develop/rpc.mdx index 09382fb9a8..dec8d990a8 100644 --- a/docs/src/content/next/develop/rpc.mdx +++ b/docs/src/content/next/develop/rpc.mdx @@ -14,7 +14,7 @@ The simplest way to achieve this is to keep all the agent types that need to cal The first step of calling a remote agent is creating a _client_ for it by specifying which agent type it is and identifying the agent by providing values for its constructor parameters. If the agent identified by the parameters has not been existing yet, it is going to be created and its constructor is executed remotely. If it has been already created, the client just points to it and no action is taken until an _agent method_ is called using it. - + In the following example, we have two agents defined; a weather agent with a constructor parameter identifying a location, and another example agent creating and calling this weather agent. @@ -67,6 +67,16 @@ In the following example, we have two agents defined; a weather agent with a con Here `weatherInLondon` is the client for calling a remote `WeatherAgent`, obtained from `WeatherAgent.client` with the target agent's id record - it exposes the agent methods, such as `currentWeather` as its own methods. + +In Effect TypeScript, the definition's typed client returns scoped Effects: + +```typescript +const weatherInLondon = yield* WeatherAgent.client.get({ location: "London" }) +``` + +Use it inside `Effect.scoped(...)` or an existing agent runtime scope. The client exposes each agent method as an Effect-returning function. + + In the following example, we have two agents defined; a weather agent with a constructor parameter identifying a location, and another example agent creating and calling this weather agent. @@ -209,9 +219,13 @@ Here `weather_in_london` is the client for calling a remote `WeatherAgent` - it When the caller and target agents belong to different components, use the pattern for your language: - + + +Export a plain shared schema/spec object. The provider calls `defineAgent(weatherSpec).implement(...)`; the caller uses `defineAgentClient(weatherSpec).client.get(...)`. Import `defineAgentClient` from `@golemcloud/golem-ts-sdk`; unlike `defineAgent`, it does not register the type in the caller component. + + -Export the agent definition from a shared package. The provider imports and implements that definition, while the caller imports it and uses its client. +Export an unimplemented `defineAgent` definition from a shared package, or declare a caller-owned interface with `defineAgentClient`. The provider implements its definition while the caller uses the definition's client. @@ -226,7 +240,7 @@ Put the `@agentDefinition` trait in a shared module. The provider depends on it MoonBit does not currently support putting an agent definition in a shared package and importing its generated client from separate components. Its `#derive.agent` annotation describes the concrete implementation type, and the MoonBit generator emits registration, dispatch, and client code together in the component package. -Instead, declare an agent dependency and use the generated internal guest bridge. Suppose `WeatherAgent` is defined in the `example:weather` component while `ExampleAgent` is defined in `example:caller`: +Instead, declare an agent dependency and use the generated internal guest bridge. The provider must be independently buildable before the caller's generated client exists. If components use separate MoonBit modules, configure their build commands, working directories, and output paths accordingly; the stock root-directed MoonBit template is not sufficient for that layout. ```yaml filename="golem.yaml" components: @@ -242,48 +256,15 @@ components: - example:weather/WeatherAgent ``` -The provider remains an ordinary MoonBit agent and does not contain a separately maintained interface: +Build the provider first so Golem can discover its schema and generate the internal client. Follow [Calling Another Agent (MoonBit)](/next/how-to-guides/moonbit/golem-call-another-agent-moonbit) to integrate it: the generated module itself still uses `moon.mod.json` and must not be edited, while current applications need `moon.mod` and `moon.work` plus workspace-aware debug and release build/embed overrides. -```moonbit filename="weather/weather_agent.mbt" -#derive.agent -struct WeatherAgent { - location : String -} - -fn WeatherAgent::new(location : String) -> WeatherAgent { - { location, } -} - -pub fn WeatherAgent::current_weather(self : Self) -> String { - "Sunny in \{self.location}" -} -``` - -The dependency makes `golem build` build the provider first and generate an internal MoonBit guest client at `golem-temp/bridge-sdk/moonbit/internal/weather-agent-guest-client`. Add that generated module as a local dependency of the application module: - -```json filename="moon.mod.json" -{ - "name": "example/weather-app", - "preferred-target": "wasm", - "deps": { - "golemcloud/golem_sdk": "0.5.1", - "weather-agent-guest-client": { - "path": "golem-temp/bridge-sdk/moonbit/internal/weather-agent-guest-client" - } - } -} -``` - -Import the generated client package in the caller component: +Add the generated client package to the caller's existing `moon.pkg`. Preserve the template's existing SDK imports, link configuration, and WASM exports: ```moonbit filename="caller/moon.pkg" import { - "golemcloud/golem_sdk/agents", "weather-agent-guest-client/client" @weather, - // ...the caller's other Golem SDK imports + // ...existing imports } - -pkgtype(kind: "executable") ``` The caller can now use the generated typed client without importing the provider component or duplicating its agent definition: @@ -305,7 +286,7 @@ pub async fn ExampleAgent::weather_in_london(self : Self) -> String { } ``` -Run `golem build --yes` after changing the provider API. Golem regenerates the guest client from the built provider's discovered agent schema before compiling the caller. The resulting call uses Golem's guest RPC host API directly; unlike an external [bridge library](/next/invoke/bridge-libraries), it does not use the REST API and does not need server configuration. +Rebuild after changing the provider API so Golem can regenerate the guest client from the provider's discovered schema before compiling the caller. The resulting call uses Golem's guest RPC host API directly and does not need REST API server configuration. This generated guest bridge provides the same separation needed for a two-component MoonBit application. No shared `#derive.agent` definition package or additional MoonBit SDK feature is required for cross-component RPC. @@ -315,22 +296,28 @@ Run `golem build --yes` after changing the provider API. Golem regenerates the g ### Phantom agents -Calling `get` **gets or creates** an agent identified by its constructor parameters. Sometimes we want to create more than one instance of the same agent (having the same constructor parameter values), especially when using **ephemeral agents**. Every possible agent has a "default" instance which is normally created by calling `get` on the client or by referring to it using the **agent ID** and can have an arbitrary number of **phantom** instances, distinguished by an additional **phantom ID** UUID. +Calling `get` **gets or creates** a durable agent identified by its constructor parameters. Durable agent types have a default instance and can have arbitrary **phantom** instances distinguished by an additional **phantom ID** UUID. Ephemeral clients expose only phantom factories. -When using **agent ID*s (for example in APIs or the CLI), phantom IDs are appended to the agent ID in square brackets: +When using **agent IDs** (for example in APIs or the CLI), phantom IDs are appended to the agent ID in square brackets: - Primary instance of `weather-agent` with parameter `London`: - `weather-agent("London")` - A phantom instance of the same agent: - `weather-agent("London")[43d9d7ab-e1d7-47e2-a213-d635eaa317a2]` -Phantom agents are created through Agent-to-Agent communication or [forking](forking.mdx). To create a new phantom agent via RPC, use the alternative client constructor method: +Phantom agents are created through Agent-to-Agent communication or [forking](/next/develop/forking). To create a new phantom agent via RPC, use the alternative client constructor method: - + ```typescript const { client: phantomAgent, agentId, phantomId } = WeatherAgent.client.newPhantom({ location: "London" }); ``` + + ```typescript + const phantomAgent = yield* WeatherAgent.client.newPhantom({ location: "London" }) + const phantomId = phantomAgent.phantomId + ``` + ```rust let phantom_agent = WeatherAgentClient::new_phantom("London".to_string()); @@ -344,19 +331,29 @@ Phantom agents are created through Agent-to-Agent communication or [forking](for ```moonbit let phantom_agent = WeatherAgentClient::new_phantom("London") + defer phantom_agent.drop() ``` If we know the **phantom ID** of an agent, we can create a client targeting an existing phantom agent: - + ```typescript const { client: phantomAgent, agentId, phantomId } = WeatherAgent.client.newPhantom({ location: "London" }); const samePhantomAgent = WeatherAgent.client.getPhantom({ location: "London" }, phantomId); ``` + + ```typescript + const { phantomId } = yield* WeatherAgent.client.newPhantom({ location: "London" }) + const samePhantomAgent = yield* WeatherAgent.client.getPhantom( + { location: "London" }, + phantomId, + ) + ``` + ```rust let phantom_agent = WeatherAgentClient::new_phantom("London".to_string()); @@ -366,6 +363,8 @@ If we know the **phantom ID** of an agent, we can create a client targeting an e ```scala + import golem.Uuid + val phantomId = Uuid(BigInt(42), BigInt(99)) val samePhantomAgent = WeatherAgentClient.getPhantom("London", phantomId) ``` @@ -373,8 +372,10 @@ If we know the **phantom ID** of an agent, we can create a client targeting an e ```moonbit let phantom_agent = WeatherAgentClient::new_phantom("London") + defer phantom_agent.drop() let phantom_id = phantom_agent.phantom_id().unwrap() let same_phantom_agent = WeatherAgentClient::get_phantom("London", phantom_id) + defer same_phantom_agent.drop() ``` @@ -391,9 +392,11 @@ const weatherType = getReflectedAgentType("WeatherAgent"); if (!weatherType) throw new Error("WeatherAgent is not registered"); if (weatherType.mode === "durable") { - const { client, agentId, phantomId } = weatherType.client.newPhantom({ + const phantom = weatherType.client.newPhantom({ location: "London", }); + if (!("client" in phantom)) throw new Error("Expected a durable phantom"); + const { client, agentId, phantomId } = phantom; const samePhantom = weatherType.client.getPhantom( { location: "London" }, phantomId, @@ -425,17 +428,56 @@ ephemeral agent ID reusable. See [Calling Agents with Runtime Reflection](/next/how-to-guides/ts/golem-agent-reflection-ts) for discovery, schema inspection, concrete `AgentId` binding, and error handling. +### Reflection and dynamic clients in Effect TypeScript + +Effect TypeScript supports both schema-aware reflection and schema-native dynamic invocation. Reflection discovers a deployed type and validates JSON-shaped inputs: + +```typescript +import { Effect } from "effect" +import { Reflection } from "@golemcloud/effect-golem" + +const reflected = Effect.scoped( + Effect.gen(function* () { + const type = yield* Reflection.getAgentType("WeatherAgent") + if (type === undefined || type.mode !== "durable") return undefined + const client = yield* type.client.get({ location: "London" }) + const method = yield* client.method("currentWeather") + return yield* method.invoke({}) + }), +) +``` + +When you already have an encoded durable agent identity and do not need discovery, parse it and bind a dynamic client. Dynamic methods accept native `SchemaValueTree` inputs, so the caller must supply values matching the deployed method contract: + +```typescript +import { AgentIdentity } from "@golemcloud/effect-golem" + +const dynamic = Effect.scoped( + Effect.gen(function* () { + const identity = yield* AgentIdentity.parse(encodedAgentId) + const client = yield* identity.dynamicClient() + return yield* client.method("currentWeather").invoke(inputTree) + }), +) +``` + ## Calling a remote agent method Once we have a _client_ for a remote agent, it is possible to call its methods just as if it would be a local instance. - + ```typescript const currentWeather = await weatherInLondon.currentWeather(); ``` + + ```typescript + const currentWeather = yield* weatherInLondon.currentWeather({}) + ``` + + ```rust let current_weather = weather_in_london.current_weather().await; @@ -449,6 +491,8 @@ Once we have a _client_ for a remote agent, it is possible to call its methods j + Inside an async MoonBit agent method: + ```moonbit let current_weather = weather_in_london.current_weather() ``` @@ -458,7 +502,7 @@ Once we have a _client_ for a remote agent, it is possible to call its methods j ## Triggering execution of an agent method It is possible to trigger the remote execution of an agent method without awaiting it. This is useful for spawning background tasks, for example. - + To trigger an agent method and return immediately, use the `trigger` method exposed on each remote method in the client: @@ -468,6 +512,15 @@ It is possible to trigger the remote execution of an agent method without awaiti ``` + + Each remote method exposes a typed `trigger` Effect: + + ```typescript + const remoteAgent = yield* BackgroundTaskAgent.client.get({ jobId: backgroundJobId }) + yield* remoteAgent.runTask.trigger({ message: "hello", count: 1234 }) + ``` + + To trigger an agent method and return immediately, use the `trigger_` variant exposed on the generated client. Example: If there is a `run_task`, its trigger variant is `trigger_run_task`: @@ -493,6 +546,7 @@ It is possible to trigger the remote execution of an agent method without awaiti ```moonbit let remote_agent = BackgroundTaskAgentClient::get(background_job_id) + defer remote_agent.drop() remote_agent.trigger_run_task("hello", 1234) ``` @@ -501,7 +555,7 @@ It is possible to trigger the remote execution of an agent method without awaiti ### Scheduling an agent method An advanced case of triggering the execution of an agent method is to **schedule it** to be executed at a later time. - + Similar to `.trigger`, there is a `.schedule` method as well on each remote agent method in the client: @@ -512,6 +566,18 @@ An advanced case of triggering the execution of an agent method is to **schedule Here the first parameter is a `Datetime` object, with seconds and nanoseconds fields representing the UNIX Epoch time when the method should be executed. In TypeScript, `seconds` is a `bigint`. The rest of the parameters are the same as for the original method. + + + + Pass an absolute time as a `Datetime`, JavaScript `Date`, Effect `DateTime`, or numeric Unix-epoch milliseconds: + + ```typescript + const remoteAgent = yield* BackgroundTaskAgent.client.get({ jobId: backgroundJobId }) + yield* remoteAgent.runTask.schedule(new Date(Date.now() + 10 * 60 * 1000), { + message: "hello", + count: 1234, + }) + ``` @@ -519,21 +585,21 @@ An advanced case of triggering the execution of an agent method is to **schedule Example: If `run_task` is a method, its schedule variant is `schedule_run_task`: ```rust - use golem_rust::wasip2::clocks::wall_clock::Datetime; + use golem_rust::ScheduledTime; let mut remote_agent = BackgroundTaskAgentClient::get(background_job_id); remote_agent.schedule_run_task( "hello".to_string(), 1234, - Datetime { + ScheduledTime { seconds: 60, nanoseconds: 0, }, - ); + ).expect("failed to schedule background task"); ``` - Here the last parameter is a `Datetime` object, with seconds and nanoseconds fields representing the UNIX Epoch time when the method should be executed. The rest of the parameters are the same as for the original method. + Here the last parameter is a `ScheduledTime`, with seconds and nanoseconds fields representing the UNIX Epoch time when the method should be executed. The rest of the parameters are the same as for the original method. @@ -554,15 +620,19 @@ An advanced case of triggering the execution of an agent method is to **schedule Example: If `run_task` is a method, its schedule variant is `schedule_run_task`: ```moonbit + // In moon.pkg: + // import { "golemcloud/golem_sdk/interface/wasi/clocks/system-clock" @systemClock } + let remote_agent = BackgroundTaskAgentClient::get(background_job_id) + defer remote_agent.drop() remote_agent.schedule_run_task( - @wallClock.Datetime::{ seconds: 60, nanoseconds: 0 }, + @systemClock.Instant::{ seconds: 60, nanoseconds: 0 }, "hello", 1234, ) ``` - Here the first parameter is a `Datetime` object, with `seconds` and `nanoseconds` fields representing the UNIX Epoch time when the method should be executed. The rest of the parameters are the same as for the original method. + Here the first parameter is a system-clock `Instant`, with `seconds` and `nanoseconds` fields representing the UNIX Epoch time when the method should be executed. The rest of the parameters are the same as for the original method. diff --git a/docs/src/content/next/develop/setup.mdx b/docs/src/content/next/develop/setup.mdx index e76bee27cc..febfaf219e 100644 --- a/docs/src/content/next/develop/setup.mdx +++ b/docs/src/content/next/develop/setup.mdx @@ -2,12 +2,18 @@ import { Callout, Tabs } from "nextra/components" ## Setting up the development environment - + Golem's TypeScript toolchain uses npm as the underlying build tool. Install **Node.js** and **npm** on your system by following the official [instructions](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). + +Golem's Effect TypeScript toolchain uses npm, TypeScript, and Rollup. Install **Node.js** and **npm** by following the official [instructions](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). + +Generated projects include compatible versions of `effect` and `@golemcloud/effect-golem`. Keep those versions aligned when updating dependencies; the Effect SDK currently uses Effect 4 APIs. + + ### Setup Rust diff --git a/docs/src/content/next/develop/snapshotting.mdx b/docs/src/content/next/develop/snapshotting.mdx index eb1a0030a6..c74e74cf4d 100644 --- a/docs/src/content/next/develop/snapshotting.mdx +++ b/docs/src/content/next/develop/snapshotting.mdx @@ -7,14 +7,14 @@ Golem recovers agent state by **replaying the oplog** — the log of all operati Snapshotting also enables **manual agent updates** between incompatible versions — see [Updating Agents](/next/develop/updating) for details. - Snapshotting is optional. If you do not configure it, Golem will continue to recover agents by replaying the full oplog. + Automatic snapshotting is optional. Without it, recovery replays from the last successful manual-update snapshot, if one exists; otherwise it replays the full oplog. ## Default snapshotting implementation Each language SDK can generate JSON saving and loading when the complete agent state has an explicit JSON-deserialization contract. Automatic loading is not generated for arbitrary state: use custom snapshotting when your SDK cannot deserialize the complete state. - + Provide a state schema in the snapshotting configuration and **do not** provide a custom `snapshot: { save, load }` block. The SDK serializes and restores the schema-defined state as JSON: @@ -27,6 +27,19 @@ Each language SDK can generate JSON saving and loading when the complete agent s A bare enabled policy without `state` requires a custom `save`/`load` pair. + + Declare the saved value with `Snapshot.define(...)` and use a matching implementation strategy such as `Snapshot.ref()`: + + ```typescript + snapshotting: Snapshot.define({ + schema: Schema.Struct({ value: Schema.Number }), + policy: Snapshot.policy.everyN(1), + }) + + // In .implement({ ... }): + snapshot: Snapshot.ref<{ value: number }>() + ``` + If your agent struct implements `Serialize` and `DeserializeOwned` (from [serde](https://serde.rs/)), **do not** override the `save_snapshot` and `load_snapshot` methods. The SDK will use a default JSON-based implementation. Ordinary owned structs commonly satisfy these bounds by deriving `Serialize` and `Deserialize`. @@ -42,7 +55,7 @@ Each language SDK can generate JSON saving and loading when the complete agent s You can implement custom saving and restoration to control exactly how agent state is serialized. Saving runs on the live instance. Loading is a separate factory that returns the complete state or instance; the SDK never runs the normal agent initialization path first and then hands that instance to the loader. The following example shows a counter agent that saves its count as raw bytes: - + Provide a `snapshot: { save, load }` block on `.implement(...)`. `save()` has the agent state as `this`. `load(bytes, context)` has no `this` and returns a complete fresh state object: @@ -69,8 +82,8 @@ You can implement custom saving and restoration to control exactly how agent sta }, snapshot: { save() { - const snapshot = new Uint8Array(4); - new DataView(snapshot.buffer).setUint32(0, this.value); + const snapshot = new Uint8Array(8); + new DataView(snapshot.buffer).setFloat64(0, this.value); return snapshot; }, load(bytes, _context) { @@ -78,13 +91,43 @@ You can implement custom saving and restoration to control exactly how agent sta bytes.buffer, bytes.byteOffset, bytes.byteLength, - ).getUint32(0); + ).getFloat64(0); return { value }; }, }, }); ``` + + Select binary snapshots on the definition and provide `save` and `restore` effects on the implementation. Restoration constructs fresh state and does not run `init` first: + + ```typescript + import { Effect, Ref } from "effect" + import { Snapshot } from "@golemcloud/effect-golem" + + // In defineAgent({ ... }): + snapshotting: Snapshot.custom({ + policy: Snapshot.policy.everyN(1), + }) + + // In .implement({ ... }): + snapshot: { + save: (state) => + Ref.get(state).pipe( + Effect.map((value) => { + const bytes = new Uint8Array(8) + new DataView(bytes.buffer).setFloat64(0, value) + return bytes + }), + ), + restore: (bytes, _context) => + Ref.make( + new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) + .getFloat64(0), + ), + } + ``` + Define instance `save_snapshot` and an associated `load_snapshot` factory. The factory returns `Self`; it does not receive `&mut self`: @@ -169,18 +212,19 @@ You can implement custom saving and restoration to control exactly how agent sta Snapshot loading is a specially supported SDK lifecycle operation, not an agent method. The SDKs keep it separate from normal initialization and install the returned state or instance only after restoration succeeds. Restore context exposes the agent identity and restored principal, plus the phantom ID and fresh configuration where applicable. -Golem runs snapshot loading in read-only mode and writes nothing from it to the oplog. Decoding, local computation, fresh randomness, configuration access, and other permitted reads can be used to construct the restored value. Mutating host operations and outgoing HTTP or agent RPC calls are rejected before they take effect. +Golem runs snapshot loading in read-only mode and writes nothing from it to the oplog. Decoding, local computation, fresh randomness, configuration access, and other permitted reads can be used to construct the restored value. Durable host calls classified as writes, and outgoing HTTP or agent RPC calls, are rejected before they take effect. This restriction is not a general filesystem write sandbox: local filesystem writes are not blocked by it. If loading fails, partial state is discarded: - A manual update fails and the agent remains on its previous component version. -- During automatic recovery, Golem recreates the component and recovers without the failed automatic snapshot. It does not try an older automatic snapshot. +- If an automatic snapshot is rejected by its loader, or loading or subsequent replay fails deterministically, Golem recreates the component and replays from the last successful manual-update snapshot—or from the beginning if none exists. It does not try an older automatic snapshot. Transient failures and resource-limit failures can instead propagate or cause recovery to retry. +- Failure to load the baseline snapshot from an already completed manual update cannot fall back to pre-update history. ## Configuring snapshot-based recovery To enable snapshotting, add a `snapshotting` option to your agent definition: - + Pass the `snapshotting` option to `defineAgent(...)`: @@ -198,6 +242,16 @@ To enable snapshotting, add a `snapshotting` option to your agent definition: }); ``` + + Pass a `Snapshot` definition to `snapshotting`: + + ```typescript + snapshotting: Snapshot.define({ + schema: Schema.Struct({ value: Schema.Number }), + policy: Snapshot.policy.periodic("5 seconds"), + }) + ``` + ```rust #[agent_definition(snapshotting = "periodic(5s)")] @@ -219,10 +273,10 @@ The available policy forms are listed below. Exact spelling and policy support v | Option | Description | |---|---| -| `disabled` | Snapshotting is turned off. Recovery replays the full oplog. | +| `disabled` | Automatic snapshot creation is disabled. Recovery still uses the last successful manual-update snapshot, if one exists. | | `enabled` | Uses the server-side default snapshotting configuration, which may be disabled. | | `every(N)` | Takes a snapshot every **N** successful invocations. | -| `periodic(duration)` | Takes a snapshot every **duration** interval (e.g. `5s`, `1m`). | +| `periodic(duration)` | Schedules snapshots at safe invocation boundaries after each **duration** interval (e.g. `5s`, `1m`). | ## Observability diff --git a/docs/src/content/next/develop/tool-authoring.mdx b/docs/src/content/next/develop/tool-authoring.mdx new file mode 100644 index 0000000000..134488c72d --- /dev/null +++ b/docs/src/content/next/develop/tool-authoring.mdx @@ -0,0 +1,250 @@ +import { Callout, Tabs } from "nextra/components" + +# Tool Authoring and Lifecycle + +This page shows how to define, test, bind, publish, and consume a [Golem-native tool](/next/concepts/golem-native-tools). A tool is stateless metadata plus handlers. Its identity is scoped to the owning agent and tool name, and each component-tool invocation runs in a fresh isolated instance under the owner's execution. + +## Define a tool + +The SDKs project the same model: a command tree, inherited global options and flags, command bodies, typed results, declared errors, and optional stdin/stdout byte streams. + + + +```typescript +import { z } from 'zod'; +import { ok, toolDefinition } from '@golemcloud/golem-ts-sdk'; + +export const files = toolDefinition('files') + .version('1.0.0') + .global('verbose', z.boolean(), { kind: 'flag', short: 'v' }) + .command('read', command => command.body(body => body + .positional('path', z.string()) + .stdout({ mime: ['application/octet-stream'], required: false }) + .returns(z.string()) + .error('not-found', { kind: 'runtime', exitCode: 2 }) + )); + +files.implement({ + read: async ({ path, verbose }) => ok(verbose ? `read:${path}` : path), +}); +``` + + +```typescript +import { Effect, Schema } from 'effect'; +import { Tool } from '@golemcloud/effect-golem'; + +const files = Tool.toolDefinition('files').command('read', command => + command.body(body => body + .positional('path', Schema.String) + .returns(Schema.String) + .error('not-found', Schema.Struct({ path: Schema.String }))) +); + +files.implement({ + read: ({ path }) => Effect.succeed(path), +}); +``` + +Handlers return `Effect` values. Tool services can be supplied with the optional `Layer` argument to `implement`. + + +```rust +use golem_rust::{tool_definition, tool_implementation, ToolError}; + +#[derive(ToolError)] +enum FileError { + #[tool_error(kind = "runtime-error", exit_code = 2)] + NotFound, +} + +#[tool_definition(version = "1.0.0")] +trait Files { + #[arg(path = "positional")] + async fn read(&self, path: String) -> Result; +} + +struct FilesImpl; + +#[tool_implementation] +impl Files for FilesImpl { + async fn read(&self, path: String) -> Result { Ok(path) } +} +``` + +Use `InputStream` and `OutputStream` parameters for stdin and stdout. The macros generate discovery metadata and a typed client. + + +```scala +import golem.runtime.annotations.* + +enum FileError { + @error(kind = "runtime-error", exitCode = 2) + case NotFound +} + +@toolDefinition(name = "files", version = "1.0.0") +trait Files { + @arg("path", scope = "positional") + def read(path: String): Either[FileError, String] +} + +@toolImplementation() +final class FilesImpl extends Files { + def read(path: String): Either[FileError, String] = Right(path) +} +``` + +`ToolInputStream` and `ToolOutputStream` add streaming stdin and stdout. + + +```moonbit +enum FileError { + #derive.error(kind="runtime-error", exit_code="2") + NotFound +} + +#derive.tool("files", version="1.0.0") +struct Files {} + +#derive.arg("path", scope="positional") +pub fn Files::read(path : String) -> Result[String, FileError] { + Ok(path) +} +``` + +Use `@asyncCore.Stream[Byte]` and `@tool.ProviderStdout` parameters for streaming. + + + +## Model commands precisely + +Prefer structure over parsing strings yourself: + +- **Commands and subcommands** describe mutually exclusive operations. A command can also have an implicit body for bare invocation. +- **Globals** are options or flags inherited by every descendant command. +- **Positionals** are ordered; only the final tail positional can be variadic. +- **Options and flags** carry long names, aliases, defaults, repetition, delimiters, environment fallback, and documentation. +- **Constraints** express requirements, all-or-none groups, implications, and exclusions. +- **Results and errors** are typed. Declared errors include a stable name, category, optional payload, and exit code; transport or protocol failures remain RPC errors. +- **stdin and stdout** are byte-stream slots with declared MIME metadata; each declared slot can be required or optional. They use backpressured streams rather than buffering a complete body. + +The command tree is metadata, not a spawned shell. A component cannot currently run an arbitrary native executable. Implement operations with code and libraries compiled into the component, or call an external service through supported network APIs. + +## Call through a typed client + +Definition-owned clients preserve the command tree and schemas. TypeScript uses `definition.client()`, Effect uses `Tool.client(definition)`, and the Rust, Scala, and MoonBit code generators project equivalent clients. For a tool implemented in another component or language, add it to bridge generation: + +```yaml filename="golem.yaml" +bridge: + ts: + internal: + tools: [files] + rust: + internal: + tools: [files] +``` + +Generated clients call the canonical name through `tool-rpc`; they do not embed a component ID, MCP endpoint, host-native implementation ID, or credential. + +## Add middleware + +Use **monomorphic middleware** when the wrapper knows the tool shape and may adapt it. Use **universal middleware** for shape-preserving policy across every tool. Middleware receives only an invocation-scoped handle to the next layer. + +```typescript +import { ToolInvokeError } from '@golemcloud/golem-ts-sdk'; + +export const safeFiles = files.middleware({ + name: 'safe-files', + implementation: { + read: async ({ path, ...input }, { underlying }) => { + if (path.startsWith('/etc/')) { + throw new ToolInvokeError({ tag: 'constraint-violation', val: 'path denied' }); + } + return underlying.read({ path, ...input }); + }, + }, +}); +``` + +Effect exports typed and universal middleware from `@golemcloud/effect-golem/middleware`. Rust, Scala, and MoonBit expose the same runtime middleware contract through their SDK authoring forms. Binding and traversal are separate: registering middleware in a component does not attach it to any call. + +## Declare and bind + +A local declaration identifies the component that implements the tool. A binding exposes it to a component's agents, then an agent entry can override, replace, or remove inherited settings: + +```yaml filename="golem.yaml" +tools: + files: + component: acme:file-tools + config: + root: /workspace + +components: + acme:assistant: + dependencies: + tools: [files] + tools: + files: + configKeysReadable: [workspace.root] + +agents: + ReadOnlyAssistant: + tools: + files: + filesystemAccess: denied + middleware: + - name: audit + parameters: { channel: security } +``` + +Bindings expose a tool; permission cards authorize calls. Component bindings provide defaults that agent entries can override, replace, or remove. Readable configuration and secret scopes intersect across retained layers and between environment and owner bindings; revealable secrets are also limited to readable secrets. Filesystem access uses the last explicit cascade value, with denial taking precedence when environment and owner bindings are combined. `toolsMergeMode` can `upsert`, `replace`, or `remove` inherited bindings. Middleware order is environment-universal middleware, then the merged per-tool list, then the leaf tool; `middlewareMergeMode` is `prepend` (the default), `append`, or `replace`. + +The example assumes that the `audit` middleware is declared separately under `tools.middleware`. + +See [Application Manifest](/next/app-manifest) for every field, merge rule, and validation mode. + +## Publish and consume + +Publish a local tool from an environment deployment: + +```yaml filename="golem.yaml" +toolReleases: + production: + files: {} +``` + +The published version comes from the tool's discovered metadata. The Effect SDK currently emits tool metadata version `0.1.0`; its `toolDefinition` does not expose a version option. The `1.0.0` consumer below therefore applies to the TypeScript, Rust, Scala, and MoonBit definitions shown above. + +A consumer selects an exact release and supplies its own configuration: + +```yaml filename="golem.yaml" +tools: + files: + release: + account: tools@example.com + name: files + version: "1.0.0" + config: { root: /workspace } +``` + +Deployment reconciles the environment's release grant before reading metadata or generating clients. Publication is additive: removing `toolReleases` does not de-publish. Existing deployment snapshots stay pinned. Use `golem tool release list|get|de-publish|restore` and `golem tool grant create|list|get|delete|restore` for explicit lifecycle operations. + + + A release grant permits consumption of an artifact. It does not bind the tool to an agent and is not a runtime permission-card grant. + + +## Wrap MCP or a CLI + +- **MCP:** configure an MCP import in the environment. Golem discovers and projects its tools into the registry; generated clients still call `tool-rpc`. Resolve naming collisions explicitly—application and ambient names cannot collide, ambient tools take precedence over MCP projections, and earlier MCP imports precede later ones. +- **CLI-shaped operation:** reproduce the required operation with libraries compiled into the component, or call an external service. Represent arguments as command metadata, stream bytes through stdin/stdout, and map failures to declared errors. Spawning an arbitrary native CLI inside a component is not currently supported. + +## Test the whole contract + +1. Unit-test metadata validation, handler results, declared errors, constraints, and stream cancellation/backpressure. +2. Test middleware independently with a fake underlying handler, including rejection and transformed input/output. +3. Build the component and run an integration call through `tool-rpc`; unit tests cannot prove discovery, instance isolation, runtime authorization, or middleware traversal. +4. Deploy with the intended binding and permission cards. Test both missing-binding and missing-permission cases. +5. For durable middleware, restart while a call or approval is pending and verify logical recovery without repeating recorded effects. External effects still require an appropriate idempotency policy across an ambiguous crash window. + +Build and deploy with the normal [component workflow](/next/develop/building-components). Tool and middleware publication is committed with deployment, but release de-publication and grant administration are explicit lifecycle operations. diff --git a/docs/src/content/next/develop/transactions.mdx b/docs/src/content/next/develop/transactions.mdx index e63ac29e8c..7ecc840269 100644 --- a/docs/src/content/next/develop/transactions.mdx +++ b/docs/src/content/next/develop/transactions.mdx @@ -1,27 +1,30 @@ import { Callout, Tabs } from "nextra/components" -# High level transactions +# High-level transactions On top of the [durability controls](/next/develop/durability) and [retry controls](/next/develop/retries), the SDKs also provide high level functions for defining transactions supporting **compensation actions** in case of getting reverted. -Although Golem's automatic retry policies and low-level atomic regions provide a lot of power automatically, many times a set of external operations such as HTTP requests needs to be executed **transactionally**; if one of the operations fails, the whole transaction need to be rolled back by executing some compensation actions. +Although Golem's automatic retry policies and low-level atomic regions provide a lot of power automatically, a set of external operations such as HTTP requests often needs to be executed **transactionally**; if one operation fails, compensation actions may need to undo earlier successful operations. The SDK provides support for two different types of transactions: - **fallible transactions** are only dealing with domain errors -- **infallible transactions** must always succeed, and Golem applies its active retry policy to it +- **infallible transactions** compensate domain errors and rewind for an immediate retry ### Fallible transactions Many times external operations (such as HTTP calls to remote hosts) need to be executed transactionally. If some operations failed, the transaction needs to be rolled back; compensation actions need to undo whatever the already successfully performed operations did. -A **fallible transaction** only deals with domain errors. Within the transaction every operation that succeeds gets recorded. If an operation fails, all the recorded operations get compensated in reverse order before the transaction block returns with a failure. +A **fallible transaction** deals with domain errors. Successful steps register compensations. Propagating a domain error out of the transaction body triggers these compensations in reverse order; handling the error inside the body does not automatically trigger rollback. The failing step has no registered compensation. TypeScript, Rust and Scala stop at the first compensation error and report partial rollback. Effect's `withFallibleCompensation` continues the remaining compensations and reports the first compensation error. - + - In TypeScript, a fallible transaction can be executed using the async `fallibleTransaction` function, by + In TypeScript, a fallible saga can be executed using the async `fallibleSaga` function, by passing an async callback that can execute operations on the open transaction (see below). + + In Effect TypeScript, wrap an Effect program with `Saga.fallibleTransaction`. Expected typed failures trigger registered compensations and return a `TransactionFailure` through the Effect error channel. + In Rust, a fallible transaction can be executed using the async `fallible_transaction` function in `golem_rust`, by passing a closure wrapped with `boxed(async move { ... })` that can execute operations on the @@ -40,13 +43,16 @@ A **fallible transaction** only deals with domain errors. Within the transaction ### Infallible transactions -An infallible transaction must always succeed; in case of a failure or interruption, it gets retried. If there is a domain error, the compensation actions are executed before the retry. +In an infallible transaction, a propagated domain error runs compensations and then rewinds for an immediate, retry-policy-independent restart. Unexpected failures follow host recovery semantics and do not guarantee compensation. In Effect, interruption triggers compensation: the fallible helper propagates the interruption, while the infallible helper rewinds. - + - In TypeScript, an infallible transaction can be executed using the async `infallibleTransaction` function, by + In TypeScript, an infallible saga can be executed using the async `infallibleSaga` function, by passing an async callback that can execute operations on the open transaction (see below). + + `Saga.infallibleTransaction` currently requires a body whose typed error channel is `never`. `Saga.operation` includes `DurabilityHostError` in its error channel, so operations cannot be passed directly to this helper as in the fallible example. + In Rust, an infallible transaction can be executed using the async `infallible_transaction` function in `golem_rust`, by passing a closure wrapped with `boxed(async move { ... })` that can execute operations on the @@ -70,16 +76,16 @@ Both transaction types require the definition of **operations**. It is defined with the following interface: - + ```typescript /** * Represents an atomic operation of the transaction which has a rollback action. * - * Implement this interface and use it within a `transaction` block. - * Operations can also be constructed from closures using `operation`. + * Implement this interface and use it within a saga block. + * Compensable steps can also be constructed with `compensable`. */ - export interface Operation { + export interface Compensable { /** * The action to execute. * @param input - The input to the operation. @@ -97,18 +103,35 @@ It is defined with the following interface: } ``` - There are two ways to define an operation: + There are two ways to define a compensable step: - 1. Implement the `Operation` interface manually - 2. Use the `operation` function to create an operation from a pair of closures + 1. Implement the `Compensable` interface manually + 2. Use the `compensable` function to create one from a pair of closures ```typescript - export function operation( + export function compensable( execute: (input: In) => Promise>, compensate: (input: In, result: Out) => Promise>, - ): Operation + ): Compensable + ``` + + + + + `Saga.operation` creates a reusable Effect step. The compensation receives the input, successful output, and failure cause, and must have an infallible (`never`) error channel: + + ```typescript + import { Effect } from "effect" + import { Saga } from "@golemcloud/effect-golem" + + const op = Saga.operation({ + execute: (idx: number) => Effect.succeed(`id-${idx}`), + compensate: (idx, id, _cause) => + Effect.log(`reverting ${id}, ${idx}`), + }) ``` + Use `Saga.withFallibleCompensation` instead when a compensation itself can fail. @@ -184,6 +207,8 @@ It is defined with the following interface: + MoonBit has no high-level saga API. The following are application-defined illustrative types, not SDK exports: + ```moonbit pub(all) struct Operation[In, Out, Err] { execute : (In) -> Result[Out, Err] @@ -213,13 +238,13 @@ It is defined with the following interface: The defined operations can be executed in _fallible_ or _infallible_ mode: - + ```typescript - import { fallibleTransaction, infallibleTransaction, operation, Result } from "@golemcloud/golem-ts-sdk" + import { compensable, fallibleSaga, infallibleSaga, Result } from "@golemcloud/golem-ts-sdk" // example operation with compensation - const op = operation( + const op = compensable( async (idx) => { // the operation / side effect return Result.ok("id-" + idx) @@ -231,8 +256,8 @@ The defined operations can be executed in _fallible_ or _infallible_ mode: } ) - // with fallibleTransaction errors have to be handled and propagated using the Result type - const resultFallible = await fallibleTransaction(async tx => { + // With fallibleSaga, errors are propagated using Result. + const resultFallible = await fallibleSaga(async tx => { const firstId = await tx.execute(op, 1) if (firstId.isErr()) return firstId @@ -242,12 +267,33 @@ The defined operations can be executed in _fallible_ or _infallible_ mode: return Result.ok([firstId.val, secondId.val]) }) - // with infallibleTransaction no explicit error handling is needed, as it is handled by Golem retries - const resultInfallible = await infallibleTransaction(async tx => { + const resultInfallible = await infallibleSaga(async tx => { const firstId = await tx.execute(op, 1) const secondId = await tx.execute(op, 2) return [firstId, secondId] }) + ``` + + + ```typescript + import { Effect } from "effect" + import { Saga } from "@golemcloud/effect-golem" + + const op = Saga.operation({ + execute: (idx: number) => Effect.succeed(`id-${idx}`), + compensate: (idx, id, _cause) => + Effect.log(`reverting ${id}, ${idx}`), + }) + + // Construct an Effect program to yield from an agent method. + const resultFallible = Saga.fallibleTransaction( + Effect.gen(function* () { + const firstId = yield* op(1) + const secondId = yield* op(2) + return [firstId, secondId] as const + }), + ) + ``` @@ -326,99 +372,37 @@ The defined operations can be executed in _fallible_ or _infallible_ mode: The MoonBit SDK does not yet provide high-level `fallibleTransaction` / `infallibleTransaction` helpers like the other languages. The saga pattern must be implemented manually using oplog primitives as shown below. - ```moonbit - import "golemcloud/golem_sdk/api" as @api + Add the API package alias to the component's `moon.pkg`; MoonBit source files do not contain import declarations: - struct CompletedStep { - compensate : () -> Unit - } - - fn compensate_all(steps : Array[CompletedStep]) -> Unit { - let mut i = steps.length() - while i > 0 { - i = i - 1 - (steps[i].compensate)() - } - } - - // example operation with compensation - let op : Operation[Int, String, String] = operation( - fn(idx : Int) -> Result[String, String] { - Result::Ok("id-" + idx.to_string()) - }, - fn(idx : Int, id : String) -> Result[Unit, String] { - println("reverting " + id + ", " + idx.to_string()) - Result::Ok(()) - }, - ) - - fn run_fallible() -> Result[(String, String), String] { - let completed : Array[CompletedStep] = [] - - let first_id = match @api.with_atomic_operation(fn() { (op.execute)(1) }) { - Ok(id) => id - Err(error) => return Err(error) - } - - completed.push({ - compensate: fn() { - @api.with_atomic_operation(fn() { - let _ = (op.compensate)(1, first_id) - () - }) - }, - }) - - let second_id = match @api.with_atomic_operation(fn() { (op.execute)(2) }) { - Ok(id) => id - Err(error) => { - compensate_all(completed) - return Err(error) - } - } - - Ok((first_id, second_id)) + ```moonbit + import { + "golemcloud/golem_sdk/api" @api, } + ``` - fn run_infallible() -> (String, String) { - let checkpoint = @api.get_oplog_index() - let completed : Array[CompletedStep] = [] + The following is pseudocode for domain-error handling, not a complete SDK implementation: - let first_id = match @api.with_atomic_operation(fn() { (op.execute)(1) }) { - Ok(id) => id - Err(_) => { - compensate_all(completed) - @api.set_oplog_index(checkpoint) - panic() - } - } + ```text + checkpoint = get_oplog_index() + completed = [] - completed.push({ - compensate: fn() { - @api.with_atomic_operation(fn() { - let _ = (op.compensate)(1, first_id) - () - }) - }, - }) - - let second_id = match @api.with_atomic_operation(fn() { (op.execute)(2) }) { - Ok(id) => id - Err(_) => { - compensate_all(completed) - @api.set_oplog_index(checkpoint) - panic() - } - } + for each step: + result = with_atomic_operation(step.execute) + if result succeeded: + append the step's compensation to completed + else: + compensate completed steps in reverse order + preserve every compensation result - (first_id, second_id) - } - - // with fallible transactions errors are returned after compensating completed steps - let result_fallible = run_fallible() + if a compensation failed: + in fallible mode, return partial-rollback(original error, compensation error) + in infallible mode, do not rewind; apply an application-specific failure policy - // with infallible transactions errors compensate and rewind to retry from the saved oplog position - let result_infallible = run_infallible() + if all compensations succeeded: + in fallible mode, return the original domain error + in infallible mode, set_oplog_index(checkpoint) to rewind and retry ``` + + Per-step atomic regions do not undo external effects and do not by themselves make the whole sequence crash-atomic. External operations must be idempotent. A stronger crash-reexecution guarantee requires an enclosing atomic region and application-specific handling of compensation failures. diff --git a/docs/src/content/next/develop/updating.mdx b/docs/src/content/next/develop/updating.mdx index f62d6f5221..dee946e2b8 100644 --- a/docs/src/content/next/develop/updating.mdx +++ b/docs/src/content/next/develop/updating.mdx @@ -14,123 +14,143 @@ The **automatic update** mode works as it is described in the general [Agents](/ Snapshots can also be used for fast recovery during normal operation, not just for updates. See [Snapshotting](/next/develop/snapshotting) for details on snapshot-based recovery, default snapshotting implementations, and configuring periodic snapshots. -It is also possible to manually implement a pair of functions for saving and loading an agent's state into an arbitrary byte array. This is useful when making significant changes to the implementation of an agent, such that the automatic replay based update mechanism is no longer working. Future versions of Golem may also use the same snapshotting functions for optimizing recovery time of agents. +It is also possible to manually implement a pair of functions for saving and loading an agent's state into an arbitrary byte array. This is useful when making significant changes to the implementation of an agent, such that the automatic replay based update mechanism is no longer working. - + - In TypeScript, all agents inherit the `BaseAgent` class. There are two methods in `BaseAgent` that can - be overridden to implement snapshot saving and loading for an agent. + Put `snapshot` inside `.implement({ ... })`. This fragment assumes the complete state is `{ value: number }`. `load` is a factory that returns fresh state; it does not mutate an instance created by `init`: - The following example implements these methods for the simple counter example used [on the Quickstart page](/next/quickstart): ```typescript - override async saveSnapshot(): Promise { - const snapshot = new Uint8Array(4); - const view = new DataView(snapshot.buffer); - view.setUint32(0, this.value); - return snapshot; + snapshot: { + save() { + const bytes = new Uint8Array(8) + new DataView(bytes.buffer).setFloat64(0, this.value) + return bytes + }, + load(bytes, _context) { + return { + value: new DataView( + bytes.buffer, + bytes.byteOffset, + bytes.byteLength, + ).getFloat64(0), } + }, + } + ``` + + + Select `Snapshot.custom(...)` in `defineAgent({ ... })`, then provide `save` and `restore` effects on the implementation. This fragment assumes `Ref.Ref` state initialized with `Ref.make(0)`: - override async loadSnapshot(bytes: Uint8Array): Promise { - let view = new DataView(bytes.buffer); - this.value = view.getUint32(0); - } + ```typescript + import { Effect, Ref } from "effect" + import { Snapshot } from "@golemcloud/effect-golem" + + snapshotting: Snapshot.custom({ + policy: Snapshot.policy.manual, + }) + + // In .implement({ ... }): + snapshot: { + save: (state) => + Ref.get(state).pipe( + Effect.map((value) => { + const bytes = new Uint8Array(8) + new DataView(bytes.buffer).setFloat64(0, value) + return bytes + }), + ), + restore: (bytes, _context) => + Ref.make( + new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) + .getFloat64(0), + ), + } ``` - In Rust, all agent traits (i.e., annotated with the `#[agent_definition]` macro) are generated with two extra methods along with the methods defined in the trait. These methods are `save_snapshot` and `load_snapshot`. - These methods can be overridden to implement snapshot saving and loading for an agent. + Add both methods inside the existing `#[agent_implementation] impl CounterAgent for CounterImpl` block. This fragment assumes the complete state is `value: u32`; `load_snapshot` is an associated factory that returns `Self`: - The following example implements these methods for the simple counter example used [on the Quickstart page](/next/quickstart): ```rust - impl CounterAgent for CounterImpl { - // ... - - async fn load_snapshot(&mut self, bytes: Vec) -> Result<(), String> { - let arr: [u8; 4] = bytes - .try_into() - .map_err(|_| "Expected a 4-byte long snapshot")?; - self.count = u32::from_be_bytes(arr); - Ok(()) - } + async fn save_snapshot(&self) -> Result, String> { + Ok(self.value.to_le_bytes().to_vec()) + } - async fn save_snapshot(&self) -> Result, String> { - Ok(self.count.to_be_bytes().to_vec()) - } + async fn load_snapshot( + bytes: Vec, + _context: golem_rust::agentic::SnapshotRestoreContext, + ) -> Result { + let arr: [u8; 4] = bytes + .try_into() + .map_err(|_| "Expected a 4-byte snapshot".to_string())?; + Ok(Self { + value: u32::from_le_bytes(arr), + }) } ``` - In Scala, custom snapshot saving and loading can be implemented on the agent implementation class by defining `saveSnapshot` and `loadSnapshot` methods. + Define `saveSnapshot()` on the annotated implementation and the restoring factory on its companion object. Identity field `0` is the counter's `String` name; ordinary agent methods are omitted: - The following example implements these methods for the simple counter example used [on the Quickstart page](/next/quickstart): ```scala + import golem.runtime.SnapshotRestoreContext + import golem.runtime.annotations.agentImplementation + + import scala.concurrent.Future + @agentImplementation() - final class CounterAgentImpl(@unused private val name: String) extends CounterAgent { - private var count: Int = 0 - - def saveSnapshot(): Future[Array[Byte]] = - Future.successful { - Array( - ((count >>> 24) & 0xff).toByte, - ((count >>> 16) & 0xff).toByte, - ((count >>> 8) & 0xff).toByte, - (count & 0xff).toByte - ) - } + final class CounterImpl(private val name: String) extends Counter { + private var value: Int = 0 - def loadSnapshot(bytes: Array[Byte]): Future[Unit] = - Future.successful { - count = ((bytes(0) & 0xff) << 24) | - ((bytes(1) & 0xff) << 16) | - ((bytes(2) & 0xff) << 8) | - (bytes(3) & 0xff) - } + // Counter methods ... - override def increment(): Future[Int] = - Future.successful { - count += 1 - count - } + def saveSnapshot(): Future[Array[Byte]] = Future.successful { + val buffer = java.nio.ByteBuffer.allocate(4) + buffer.putInt(value) + buffer.array() + } + } + + object CounterImpl { + def loadSnapshot( + bytes: Array[Byte], + context: SnapshotRestoreContext + ): Future[CounterImpl] = Future.successful { + val instance = new CounterImpl(context.identity[String](0)) + instance.value = java.nio.ByteBuffer.wrap(bytes).getInt() + instance + } } ``` - In MoonBit, custom snapshot saving and loading can be implemented by adding a `Snapshottable` implementation to the agent type. + For an existing `Counter` with `name: String`, `mut value: UInt64`, and its name at identity index `0`, implement saving with `@agents.Snapshottable` and define a separate static restoration factory: - The following example implements these methods for the simple counter example used [on the Quickstart page](/next/quickstart): ```moonbit - pub impl @agents.Snapshottable for CounterAgent with save_snapshot(self) -> Bytes { - Bytes::from_array([ - ((self.value >> 56) & 0xff).to_byte(), - ((self.value >> 48) & 0xff).to_byte(), - ((self.value >> 40) & 0xff).to_byte(), - ((self.value >> 32) & 0xff).to_byte(), - ((self.value >> 24) & 0xff).to_byte(), - ((self.value >> 16) & 0xff).to_byte(), - ((self.value >> 8) & 0xff).to_byte(), - (self.value & 0xff).to_byte(), - ]) + pub impl @agents.Snapshottable for Counter with save_snapshot(self) { + let arr: Array[Byte] = [] + for i = 7; i >= 0; i = i - 1 { + arr.push(((self.value >> (i * 8)).to_int() & 0xff).to_byte()) + } + Bytes::from_array(arr) } - pub impl @agents.Snapshottable for CounterAgent with load_snapshot( - self, - bytes, - ) -> Result[Unit, String] { + pub fn Counter::load_snapshot( + bytes: Bytes, + context: @agents.SnapshotRestoreContext, + ) -> Result[Counter, String] { if bytes.length() != 8 { - return Err("Expected an 8-byte long snapshot") + return Err("Expected an 8-byte snapshot") } - - self.value = - (bytes[0].to_int().to_uint64() << 56) | - (bytes[1].to_int().to_uint64() << 48) | - (bytes[2].to_int().to_uint64() << 40) | - (bytes[3].to_int().to_uint64() << 32) | - (bytes[4].to_int().to_uint64() << 24) | - (bytes[5].to_int().to_uint64() << 16) | - (bytes[6].to_int().to_uint64() << 8) | - bytes[7].to_int().to_uint64() - Ok(()) + let mut value: UInt64 = 0 + for i = 0; i < 8; i = i + 1 { + value = value | (bytes[i].to_uint64() << ((7 - i) * 8)) + } + let name: String = context.identity(0) catch { + error => return Err(error.to_string()) + } + Ok({ name, value }) } ``` diff --git a/docs/src/content/next/develop/webhooks.mdx b/docs/src/content/next/develop/webhooks.mdx index c9dc6f2410..e627ed1323 100644 --- a/docs/src/content/next/develop/webhooks.mdx +++ b/docs/src/content/next/develop/webhooks.mdx @@ -2,7 +2,7 @@ import {Callout, Tabs} from "nextra/components" # Webhooks -Golem **webhooks** are built on top of [Golem Promises](/next/develop/promises). They let an agent generate a temporary public URL that, when POSTed to by an external system, delivers the request body to the agent. The agent is durably suspended while waiting, consuming no resources. +Golem **webhooks** are built on top of [Golem Promises](/next/develop/promises). They let an agent generate a temporary public URL that, when POSTed to by an external system, delivers the request body to the agent. The agent is durably suspended without consuming active compute while it waits. Each URL completes one promise; treat it as a secret, not as a recurring endpoint. Webhooks are useful for: @@ -23,7 +23,7 @@ To use webhooks, your agent must: ### Creating a webhook - + To create a webhook, call `createWebhook`. The returned object provides the public URL and can be awaited to receive the incoming payload: @@ -45,6 +45,27 @@ To use webhooks, your agent must: } ``` + + `Webhook.create` returns an Effect that allocates the webhook handle. Await `handle.await` and decode JSON through an Effect Schema: + + ```typescript + import { Effect, Schema } from "effect" + import { Webhook } from "@golemcloud/effect-golem" + + const MyEvent = Schema.Struct({ + action: Schema.String, + resourceId: Schema.String, + }) + + const waitForCallback = Effect.gen(function* () { + const handle = yield* Webhook.create + const url = handle.url + // Send `url` to the external system to POST to + const payload = yield* handle.await + return yield* payload.decode(MyEvent) + }) + ``` + To create a webhook, call `create_webhook`. The returned object provides the public URL and can be awaited to receive the incoming request: @@ -59,8 +80,8 @@ To use webhooks, your agent must: } async fn wait_for_callback() -> MyEvent { - let webhook = create_webhook(); - let url: String = webhook.url(); + let webhook = create_webhook().expect("failed to create webhook"); + let url: String = webhook.url().to_owned(); // Send `url` to the external system to POST to let request = webhook.await; let data: MyEvent = request.json().unwrap(); @@ -76,6 +97,7 @@ To use webhooks, your agent must: import zio.blocks.schema.Schema import scala.concurrent.Future + import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue final case class MyEvent(action: String, resourceId: String) derives Schema @@ -84,7 +106,7 @@ To use webhooks, your agent must: val url: String = webhook.url // Send `url` to the external system to POST to webhook.await().map { payload => - val event = payload.json[MyEvent] + val event = payload.json[MyEvent]() event } ``` @@ -93,7 +115,7 @@ To use webhooks, your agent must: Assuming the `golemcloud/golem_sdk/webhook` package is imported as `@webhook`, call `create` to create a webhook and `wait` to await the incoming payload: ```moonbit - fn wait_for_callback() -> String { + async fn wait_for_callback() -> String { let webhook = @webhook.create() let url = webhook.url() // Send `url` to the external system to POST to @@ -110,7 +132,7 @@ To use webhooks, your agent must: When a webhook is created, Golem generates a URL with the following format: ``` -https:///// +:///// ``` - **``** — the domain of the HTTP API deployment @@ -122,7 +144,7 @@ https:///// You can customize the webhook URL suffix using the `webhookSuffix` annotation on your agent: - + In the TypeScript SDK the suffix is passed as the `webhookSuffix` option of `http.mount(...)` on `defineAgent(...)`: @@ -140,6 +162,25 @@ You can customize the webhook URL suffix using the `webhookSuffix` annotation on }) ``` + + Pass `webhookSuffix` to `Http.mount(...)`: + + ```typescript + import { Schema } from "effect" + import { defineAgent, Http } from "@golemcloud/effect-golem" + + export const WorkflowAgent = defineAgent({ + name: "WorkflowAgent", + id: { id: Schema.String }, + http: Http.mount("/workflow/{id}", { + webhookSuffix: "/workflow-hooks", + }), + methods: { + // ... + }, + }) + ``` + ```rust #[agent_definition(mount = "/workflow/{id}", webhook_suffix = "/workflow-hooks")] @@ -162,11 +203,7 @@ You can customize the webhook URL suffix using the `webhookSuffix` annotation on The webhook URL expects a **POST** request with an arbitrary body. When the external system sends the POST request, the awaiting agent resumes and receives the payload. -The payload object provides helpers to access the data in different formats: - -- **Raw bytes** — the unprocessed request body -- **String** — the request body decoded as a UTF-8 string -- **JSON** — the request body parsed as JSON into a typed value +All SDKs expose the raw request-body bytes. Text and JSON decoding helpers vary by SDK, as shown in the examples above. Plain TypeScript's generic `json()` parses and casts but does not validate `T`; use a schema validator when the payload is untrusted. ### Configuring webhook URL in golem.yaml diff --git a/docs/src/content/next/develop/websocket.mdx b/docs/src/content/next/develop/websocket.mdx index 7276bb0c4b..6b6adb2ec4 100644 --- a/docs/src/content/next/develop/websocket.mdx +++ b/docs/src/content/next/develop/websocket.mdx @@ -5,7 +5,7 @@ import { Tabs } from "nextra/components" Golem agents use **WASI HTTP** for outgoing HTTP requests, but WASI HTTP does not support WebSocket upgrades. Golem 1.5 introduces a dedicated WebSocket client API (`golem:websocket@1.5.0`) that complements WASI HTTP and allows agents to open WebSocket connections, send and receive messages, and handle connection lifecycle events. - + The Golem TypeScript SDK provides a WebSocket client API. `connectWebsocket` opens a connection and returns a `WebSocketHandle` with `send`, `receive`, `receiveWithTimeout`, and @@ -33,7 +33,7 @@ Golem 1.5 introduces a dedicated WebSocket client API (`golem:websocket@1.5.0`) ws.send("Hello, server!"); try { for (;;) { - const message = ws.receive(); + const message = await ws.receive(); if (message.tag === "text") { console.log("Text:", message.val); } else { @@ -56,39 +56,68 @@ Golem 1.5 introduces a dedicated WebSocket client API (`golem:websocket@1.5.0`) ``` + + The Effect SDK exposes a scoped Effect `Socket`. The surrounding scope closes the connection when the program finishes: + + ```typescript + import { Effect, Fiber } from "effect" + import { Websocket } from "@golemcloud/effect-golem" + + const run = Effect.scoped( + Effect.gen(function* () { + const socket = yield* Websocket.connect("wss://example.com/chat", { + closeCodeIsError: (code) => code !== 1000, + }) + const reader = yield* Effect.forkChild( + socket.runString((message) => { + console.log("Received:", message) + }), + ) + + const write = yield* socket.writer + yield* write("Hello, server!") + yield* Fiber.join(reader) + }), + ) + ``` + + This Effect runs inside an Effect Golem agent, whose runtime supplies the WebSocket host service. `Websocket.layer(...)` provides the same socket as an Effect `Layer` when layer composition is more convenient. + + The Golem Rust SDK provides its own WebSocket API inspired by the `tungstenite` library. The main types are `WebsocketConnection`, `WebSocketMessage`, and `WebSocketError`. ```rust - #[agent_implementation] - impl ExampleAgent for ExampleAgentImpl { - async fn run() -> Result<(), WebSocketError> { - let ws = WebsocketConnection::connect("wss://example.com/chat", None)?; - println!("Connected"); - ws.send(&WebSocketMessage::Text("Hello, server!".to_string()))?; - loop { - match ws.receive().await { - Ok(WebSocketMessage::Text(text)) => println!("Text: {text}"), - Ok(WebSocketMessage::Binary(data)) => println!("Binary: {data:?}"), - Err(WebSocketError::Closed(info)) => { - if let Some(info) = info { println!("Closed [{}] \"{}\"", info.code, info.reason); } - break; - } - Err(err) => return Err(err), + use golem_rust::{WebSocketError, WebSocketMessage, WebsocketConnection}; + + async fn run_websocket() -> Result<(), WebSocketError> { + let ws = WebsocketConnection::connect("wss://example.com/chat", None)?; + println!("Connected"); + ws.send(&WebSocketMessage::Text("Hello, server!".to_string()))?; + loop { + match ws.receive().await { + Ok(WebSocketMessage::Text(text)) => println!("Text: {text}"), + Ok(WebSocketMessage::Binary(data)) => println!("Binary: {data:?}"), + Err(WebSocketError::Closed(info)) => { + if let Some(info) = info { println!("Closed [{}] \"{}\"", info.code, info.reason); } + break; } + Err(err) => return Err(err), } - Ok(()) } + Ok(()) } ``` - Scala compiles to JavaScript via Scala.js, so the standard browser `WebSocket` and `WebSocketStream` APIs - are available through Scala.js interop. + Golem's JavaScript runtime provides the standard `WebSocket` global. Add `scalajs-dom` to the Scala component and use its Scala.js facade: ```scala + import org.scalajs.dom.{CloseEvent, Event, MessageEvent, WebSocket} + import scala.concurrent.{Future, Promise} + case class ExampleAgentImpl() extends ExampleAgent { def run(): Future[Unit] = { val done = Promise[Unit]() @@ -115,15 +144,15 @@ Golem 1.5 introduces a dedicated WebSocket client API (`golem:websocket@1.5.0`) and the common agent types are imported as `@common`: ```moonbit - pub fn ExampleAgent::run(self: Self) -> Unit raise @common.AgentError { + pub async fn ExampleAgent::run(self: Self) -> Unit raise @common.AgentError { let conn = match @websocket_client.WebsocketConnection::connect("wss://example.com/chat", None) { Ok(c) => c - Err(e) => raise @common.AgentError::InvalidInput("Connect failed: \{e}") + Err(e) => raise @common.AgentError::InvalidInput("Connect failed: \{Repr(e)}") } println("Connected") match conn.send(Text("Hello, server!")) { Ok(_) => () - Err(e) => raise @common.AgentError::InvalidInput("Send failed: \{e}") + Err(e) => raise @common.AgentError::InvalidInput("Send failed: \{Repr(e)}") } // receive loop... } @@ -134,11 +163,10 @@ Golem 1.5 introduces a dedicated WebSocket client API (`golem:websocket@1.5.0`) ## WIT interface reference -The underlying WIT interface is `golem:websocket@1.5.0`. It defines the `websocket-connection` resource type with the following methods: +The underlying package is `golem:websocket@1.5.0`; its `golem:websocket/client@1.5.0` interface defines the `websocket-connection` resource type with the following methods: - **`connect`** — opens a WebSocket connection to the given URL, with optional headers - **`send`** — sends a text or binary message -- **`receive`** — blocks until the next message arrives -- **`receive-with-timeout`** — same as `receive` but with a timeout +- **`receive`** — asynchronously waits for the next message +- **`receive-with-timeout`** — waits up to the given number of milliseconds and returns `none` on expiry - **`close`** — initiates a graceful close handshake -- **`subscribe`** — returns a pollable that resolves when a message is available diff --git a/docs/src/content/next/how-to-guides.mdx b/docs/src/content/next/how-to-guides.mdx index 086f112829..195d2745e9 100644 --- a/docs/src/content/next/how-to-guides.mdx +++ b/docs/src/content/next/how-to-guides.mdx @@ -6,9 +6,9 @@ Practical, step-by-step guides for building with Golem. Each guide covers a spec - - + + - - + + diff --git a/docs/src/content/next/how-to-guides/common/golem-edit-manifest.mdx b/docs/src/content/next/how-to-guides/common/golem-edit-manifest.mdx index 4b4935c8bb..c894e5f108 100644 --- a/docs/src/content/next/how-to-guides/common/golem-edit-manifest.mdx +++ b/docs/src/content/next/how-to-guides/common/golem-edit-manifest.mdx @@ -67,10 +67,10 @@ components: dir: billing # Base directory (relative to golem.yaml). Use "." for single-component apps templates: # Parent template names (inherit build, env, plugins, files) - rust - componentWasm: target/wasm32-wasip1/debug/billing.wasm # Path to built WASM + componentWasm: target/wasm32-wasip2/debug/billing.wasm # Path to built WASM outputWasm: golem-temp/billing.wasm # Path to final output WASM build: # Build commands (see Build Commands below) - - command: cargo build --target wasm32-wasip1 + - command: cargo build --target wasm32-wasip2 env: # Environment variables LOG_LEVEL: info tools: # Owner authorization; inherited by the component's agents @@ -125,7 +125,7 @@ Templates define reusable property layers. Components reference them via `templa componentTemplates: rust: build: - - command: cargo build --target wasm32-wasip1 + - command: cargo build --target wasm32-wasip2 env: RUST_LOG: info @@ -247,14 +247,14 @@ The `build` array contains commands executed during `golem build`. Each entry is ```yaml build: - - command: cargo build --target wasm32-wasip1 + - command: cargo build --target wasm32-wasip2 dir: . # Optional working directory env: # Optional extra env vars RUSTFLAGS: "-C opt-level=2" rmdirs: [target/old] # Directories to delete before running (runs before mkdirs) mkdirs: [target/new] # Directories to create before running (runs after rmdirs) sources: ["src/**/*.rs"] # Inputs for up-to-date checks - targets: ["target/wasm32-wasip1/debug/*.wasm"] # Outputs for up-to-date checks + targets: ["target/wasm32-wasip2/debug/*.wasm"] # Outputs for up-to-date checks ``` ### TypeScript/QuickJS-specific commands @@ -284,10 +284,10 @@ Define CLI commands at the application or component level: ```yaml customCommands: test: - - command: cargo test --target wasm32-wasip1 + - command: cargo test --target wasm32-wasip2 dir: . lint: - - command: cargo clippy --target wasm32-wasip1 + - command: cargo clippy --target wasm32-wasip2 ``` Run with `golem exec ` (e.g., `golem exec test`). @@ -318,8 +318,7 @@ environments: componentPresets: [release] # Preset names to activate cli: format: json - redeployAgents: true - reset: true + redeployAgents: true # Delete and recreate agents; agent state is lost deployment: compatibilityCheck: true versionCheck: true @@ -352,8 +351,10 @@ auth: |-------|-------------| | `format` | Default output: `text`, `json`, `yaml`, `pretty`, `pretty-json`, `pretty-yaml`, `toon` | | `autoConfirm` | Auto-confirm prompts (`true`) | -| `redeployAgents` | Redeploy agents by default (`true`) | -| `reset` | Reset agents by default (`true`) | +| `redeployAgents` | Equivalent to `--redeploy-agents`: delete and recreate agents; agent state is lost | +| `reset` | Equivalent to `--reset`: delete existing agents for the deployed components after deployment, losing their state, and enable incompatibility-replacement fallbacks; the environment itself is retained | + +Configure at most one destructive default. If both are enabled, `reset` takes precedence. When `format: toon` is used, structured stdout is emitted as framed TOON documents. Parse exact `@toon` and `@end` marker lines, and treat the content between them as one TOON document. Stderr may still contain progress or diagnostics and should not be parsed as the structured payload. @@ -610,7 +611,7 @@ resourceDefaults: enforcementAction: reject unit: byte units: bytes - - name: connections + connections: limit: type: Concurrency value: 50 diff --git a/docs/src/content/next/how-to-guides/common/golem-profiles-and-environments.mdx b/docs/src/content/next/how-to-guides/common/golem-profiles-and-environments.mdx index f2bfd24d09..b2ae45b49d 100644 --- a/docs/src/content/next/how-to-guides/common/golem-profiles-and-environments.mdx +++ b/docs/src/content/next/how-to-guides/common/golem-profiles-and-environments.mdx @@ -133,10 +133,13 @@ environments: cli: format: json # Default output format autoConfirm: true # Auto-answer "yes" to prompts - redeployAgents: true # Equivalent to --reset on deploy - reset: true # Reset all state on deploy + redeployAgents: true # Delete/recreate agents; agent state is lost ``` +`reset: true` is equivalent to `--reset`: it deletes existing agents for the deployed components +after deployment, losing their state, and enables incompatibility-replacement fallbacks. The +environment itself is retained. It takes precedence over `redeployAgents` when both are configured. + ### Deployment options (`deployment:`) ```yaml @@ -156,7 +159,7 @@ environments: | `-L` / `--local` | Select the `local` environment (or `local` profile if no manifest) | | `-C` / `--cloud` | Select the `cloud` environment (or `cloud` profile if no manifest) | | `-e ` | Select a named environment from the manifest | -| *(none)* | Use the `default: true` environment, or fall back to active profile | +| *(none)* | Use the explicitly default environment, otherwise the first declared manifest environment | ### Managing environments @@ -247,10 +250,10 @@ components: presets: local: build: - - command: cargo build --target wasm32-wasip1 + - command: cargo build --target wasm32-wasip2 release: build: - - command: cargo build --target wasm32-wasip1 --release + - command: cargo build --target wasm32-wasip2 --release agents: MyAgent: diff --git a/docs/src/content/next/how-to-guides/effect/golem-call-from-external-effect.mdx b/docs/src/content/next/how-to-guides/effect/golem-call-from-external-effect.mdx index 9d288338b9..c36d26243d 100644 --- a/docs/src/content/next/how-to-guides/effect/golem-call-from-external-effect.mdx +++ b/docs/src/content/next/how-to-guides/effect/golem-call-from-external-effect.mdx @@ -12,7 +12,8 @@ standalone Node.js process. ## Steps 1. Ensure the Effect agent has the required TypeScript name and method contract, then build it. -2. Configure a `ts` bridge in `golem.yaml`; there is no separate `effect` bridge target. +2. Configure a `ts` external bridge in `golem.yaml` for the Promise-based client used below. An + `effect` bridge target also exists, but has a different Effect-native API. 3. Run `golem build` to regenerate the bridge from the built agent metadata. 4. Deploy the built application so the external client can reach the current agent definition. 5. Install and build the generated npm package. @@ -28,20 +29,20 @@ Add or extend the top-level `bridge` section in `golem.yaml`: ```yaml bridge: ts: - agents: - - CounterAgent - outputDir: ./bridge-sdk/ts/counter-agent-client + external: + agents: + - CounterAgent + outputDir: ./bridge-sdk/ts ``` `agents` accepts `"*"` or a list containing agent type names and component names (`namespace:name`). Preserve any existing bridge languages and selected agents. -For this Golem manifest version: +For this Promise-based client: -- use `bridge.ts.agents`, not `bridge.effect`; -- do not add an `external:` level under `ts`; -- a custom `outputDir` is the generated package directory itself, not a parent directory for all - generated clients; +- use `bridge.ts.external`; use `bridge.effect.external` only when the caller will consume the + Effect-native generated API; +- `outputDir` is the parent directory for generated clients; - without `outputDir`, `CounterAgent` is generated under `golem-temp/bridge-sdk/ts/counter-agent-client/`. @@ -79,7 +80,7 @@ do not guess them from the component source. Create the external application outside the Golem component source. Add the generated package as a file dependency and install Effect v4. Keep the `effect` version exactly aligned with the root -Effect component's `package.json` (the pinned SDK uses `4.0.0-beta.98`): +Effect component's `package.json` (the current SDK uses `4.0.0-rc.117`): ```json { @@ -92,7 +93,7 @@ Effect component's `package.json` (the pinned SDK uses `4.0.0-beta.98`): }, "dependencies": { "counter-agent-client": "file:../bridge-sdk/ts/counter-agent-client", - "effect": "4.0.0-beta.98" + "effect": "4.0.0-rc.117" }, "devDependencies": { "@types/node": "^25", @@ -226,7 +227,7 @@ For a durable agent, `get` creates or gets the instance identified by its agent const getAgent = Effect.tryPromise(() => MyAgent.get("my-instance")); ``` -Every generated agent supports phantom instances: +Durable agents support known and fresh phantom instances: ```typescript const phantomProgram = Effect.gen(function* () { @@ -240,14 +241,16 @@ const phantomProgram = Effect.gen(function* () { }); ``` -When the agent declares local configuration, the generated package also provides -`getWithConfig`, `getPhantomWithConfig`, and `newPhantomWithConfig`. Read their generated -declarations because configuration arguments follow the agent id values and reflect the -agent's exact config schema. +For ephemeral agents, use `newPhantom`; they do not expose durable `getPhantom` constructors. When +a durable agent declares local configuration, the generated package provides `getWithConfig`, +`getPhantomWithConfig`, and `newPhantomWithConfig`. An ephemeral agent with local configuration +provides `newPhantomWithConfig`. Read the generated declarations because configuration arguments +reflect the agent's exact schema. -Generated remote methods are callable Promises for invoke-and-await. They also expose -`abortable(signal, ...args)`, `trigger(...args)`, and `schedule(isoTimestamp, ...args)`. The latter -two return `void`; do not wrap them as if they awaited a result. +Generated remote methods are callable Promises for invoke-and-await and expose +`abortable(signal, ...args)`. Durable non-streaming methods also expose `trigger(...args)` and +`schedule(isoTimestamp, ...args)`, which return `void`. Ephemeral trigger/schedule operations return +`Promise`. Streaming methods do not expose trigger or schedule variants. ## CLI Values for Effect Components @@ -263,7 +266,8 @@ checks, but the external application should use the generated bridge for typed c ## Key Constraints -- Generate `bridge.ts`; there is no Effect-specific external bridge target. +- Generate `bridge.ts.external` for the Promise-based adapter shown here; an Effect-specific + external target also exists and exposes a different API. - Use the generated `configure` function and generated class declarations as the source of truth. - Wrap generated Promise calls in deferred `Effect.tryPromise` thunks. - Yield stateful calls sequentially when order matters. diff --git a/docs/src/content/next/how-to-guides/effect/golem-tools-middleware-effect.mdx b/docs/src/content/next/how-to-guides/effect/golem-tools-middleware-effect.mdx index 5076485fe5..f88bfd46dd 100644 --- a/docs/src/content/next/how-to-guides/effect/golem-tools-middleware-effect.mdx +++ b/docs/src/content/next/how-to-guides/effect/golem-tools-middleware-effect.mdx @@ -2,6 +2,6 @@ Build a definition with `Tool.toolDefinition(name).body(...)`. A provider finishes it with `.implement({ camelCaseName: handler })`; a caller uses `Tool.client(definition)`. Handlers and clients return Effects and stream stdin/stdout with Effect `Stream`. -Use `Middleware.typed({ name, presented, handler })` when the presented tool shape is known. Use the universal middleware API only when every tool must be intercepted. Forward input, output, permission cards, and streams exactly once to `underlying`; capability handles are affine. +Use `Middleware.typed({ name, parameters: Middleware.NoParameters, presented, handler })` when the presented tool shape is known. Use the universal middleware API only when every tool must be intercepted. Forward input, output, permission cards, and streams at most once; capability handles are affine. The default world supports ordinary, standalone-middleware, and combined components. Standalone middleware can be attached and deployed independently; unused agent and tool discovery returns empty lists. diff --git a/docs/src/content/next/how-to-guides/moonbit.mdx b/docs/src/content/next/how-to-guides/moonbit.mdx index 295af4e9a5..5921082df5 100644 --- a/docs/src/content/next/how-to-guides/moonbit.mdx +++ b/docs/src/content/next/how-to-guides/moonbit.mdx @@ -5,7 +5,7 @@ import { Cards } from "nextra/components" Guides specific to developing Golem agents in MoonBit. - + @@ -15,6 +15,7 @@ Guides specific to developing Golem agents in MoonBit. + @@ -23,6 +24,7 @@ Guides specific to developing Golem agents in MoonBit. + @@ -34,12 +36,14 @@ Guides specific to developing Golem agents in MoonBit. + + diff --git a/docs/src/content/next/how-to-guides/moonbit/_meta.js b/docs/src/content/next/how-to-guides/moonbit/_meta.js index a50961e964..a8c68bd41b 100644 --- a/docs/src/content/next/how-to-guides/moonbit/_meta.js +++ b/docs/src/content/next/how-to-guides/moonbit/_meta.js @@ -1,5 +1,5 @@ export default { - "golem-add-moonbit-package": "Adding a MoonBit Package Dependency", + "golem-add-moonbit-package": "Add a MoonBit dependency", "golem-add-agent-moonbit": "Adding a New Agent to a MoonBit Golem Component", "golem-add-http-endpoint-moonbit": "Adding HTTP Endpoints to a MoonBit Golem Agent", "golem-add-llm-moonbit": "Adding LLM and AI Capabilities (MoonBit)", @@ -9,6 +9,7 @@ export default { "golem-agent-reflection-moonbit": "Agent Reflection and Client Approaches (MoonBit)", "golem-annotate-agent-moonbit": "Annotating Agent Methods (MoonBit)", "golem-atomic-block-moonbit": "Atomic Blocks and Durability Controls (MoonBit)", + "golem-call-tool-moonbit": "Call a Golem tool from MoonBit", "golem-call-from-external-moonbit": "Calling Agents from External MoonBit Applications", "golem-call-another-agent-moonbit": "Calling Another Agent (MoonBit)", "golem-configure-durability-moonbit": "Configuring Agent Durability (MoonBit)", @@ -17,6 +18,7 @@ export default { "golem-create-agent-instance-moonbit": "Creating a Golem Agent Instance with `golem agent new`", "golem-stateless-agent-moonbit": "Creating Ephemeral (Stateless) Agents (MoonBit)", "golem-custom-snapshot-moonbit": "Custom Snapshots in MoonBit", + "golem-define-tool-moonbit": "Define a Golem tool in MoonBit", "golem-add-http-auth-moonbit": "Enabling Authentication on MoonBit HTTP Endpoints", "golem-enable-otlp-moonbit": "Enabling OpenTelemetry for a MoonBit Agent", "golem-file-io-moonbit": "File I/O in MoonBit Golem Agents", @@ -28,12 +30,14 @@ export default { "golem-make-http-request-moonbit": "Making Outgoing HTTP Requests (MoonBit)", "golem-mark-read-only-moonbit": "Marking Agent Methods as Read-Only (MoonBit)", "golem-parallel-workers-moonbit": "Parallel Workers — Fan-Out / Fan-In (MoonBit)", + "golem-permission-card-moonbit": "Permission cards in MoonBit", "golem-multi-instance-agent-moonbit": "Phantom Agents in MoonBit", "golem-recurring-task-moonbit": "Recurring Tasks via Self-Scheduling (MoonBit)", "golem-add-transactions-moonbit": "Saga-Pattern Transactions (MoonBit)", "golem-schedule-agent-moonbit": "Scheduling a Future Agent Invocation", "golem-schedule-future-call-moonbit": "Scheduling a Future Agent Invocation (MoonBit)", "golem-streaming-agent-moonbit": "Streaming Agent Methods in MoonBit", + "golem-tools-middleware-moonbit": "Tool middleware in MoonBit", "golem-trigger-agent-moonbit": "Triggering a Fire-and-Forget Agent Invocation", "golem-add-ignite-moonbit": "Using Apache Ignite from a MoonBit Agent", "golem-add-mysql-moonbit": "Using MySQL from a MoonBit Agent", diff --git a/docs/src/content/next/how-to-guides/moonbit/golem-add-http-endpoint-moonbit.mdx b/docs/src/content/next/how-to-guides/moonbit/golem-add-http-endpoint-moonbit.mdx index 9a731e7032..0436b4b7c4 100644 --- a/docs/src/content/next/how-to-guides/moonbit/golem-add-http-endpoint-moonbit.mdx +++ b/docs/src/content/next/how-to-guides/moonbit/golem-add-http-endpoint-moonbit.mdx @@ -203,7 +203,10 @@ Golem maps method return types to HTTP status codes and response bodies accordin | `T?` (`Option[T]`) | 200 OK if `Some`, 404 Not Found if `None` | JSON `T` or empty | | `Result[T, E]` | 200 OK if `Ok`, 500 Internal Server Error if `Err` | JSON `T` or JSON `E` | | `Result[Unit, E]` | 204 No Content if `Ok`, 500 if `Err` | empty or JSON `E` | -| `UnstructuredBinary` | 200 OK | Raw binary with Content-Type | + +The current MoonBit SDK has no high-level `UnstructuredText` or `UnstructuredBinary` endpoint +types. Endpoint values use the supported schema types and JSON mapping described by +[`golem-http-params-moonbit`](/next/how-to-guides/moonbit/golem-http-params-moonbit); do not add the removed wrappers or their old derive attributes. ## Complete Example @@ -224,7 +227,7 @@ pub(all) struct Task { id : String title : String priority : Priority - done : Bool + mut done : Bool } derive(ToJson, @json.FromJson) ///| diff --git a/docs/src/content/next/how-to-guides/moonbit/golem-add-moonbit-package.mdx b/docs/src/content/next/how-to-guides/moonbit/golem-add-moonbit-package.mdx index 4e0392c712..6586ab6c7c 100644 --- a/docs/src/content/next/how-to-guides/moonbit/golem-add-moonbit-package.mdx +++ b/docs/src/content/next/how-to-guides/moonbit/golem-add-moonbit-package.mdx @@ -1,56 +1,58 @@ -# Adding a MoonBit Package Dependency +# Add a MoonBit dependency -## Overview +Current MoonBit projects declare module dependencies in `moon.mod` and package imports in +`moon.pkg`. The JSON files `moon.mod.json` and `moon.pkg.json` are legacy formats; do not create +them for application source. -MoonBit projects manage dependencies through the `moon.mod.json` file at the project root. Dependencies are published on [mooncakes.io](https://mooncakes.io) and installed with the `moon` CLI. +## Add the module -## Steps +Use the package manager so it selects a version and updates `moon.mod`: -1. **Edit `moon.mod.json`** — add the package to the `"deps"` section -2. **Run `moon install`** — download and install the dependency -3. **Use the package** — import it in your `.mbt` files +```shell +moon add example/json-utils +``` -## Adding a Dependency +The resulting module declaration has this shape: -Edit `moon.mod.json` and add the package under the `"deps"` object with a version constraint: +```moonbit +name = "my-org/my-project" -```json -{ - "name": "my-org/my-project", - "version": "0.1.0", - "deps": { - "example/json-utils": "0.2.0" - } +import { + "example/json-utils@0.2.0", } ``` -Then install: - -```shell -moon install -``` +Use `moon add --upgrade example/json-utils` to update an existing dependency. `moon check` and +`moon build` fetch declared dependencies automatically; the old advice to run `moon install` for +project dependencies no longer applies. -## Version Constraints +## Import the package -Specify the version as a string in `moon.mod.json`. Use the exact version published on mooncakes.io. +Module dependencies make packages available, but each source package must explicitly import the +package it uses in `moon.pkg`: -## Using the Dependency +```moonbit +import { + "example/json-utils/parser" @json_parser, +} +``` -After installation, reference the package in your `moon.pkg.json` file's `"import"` section and use it in your `.mbt` source files. +Code can then call that package through `@json_parser`. -In `moon.pkg.json`: +For local modules, use a `moon.work` workspace and list each module directory as a member; local +path dependencies in the new `moon.mod` format are deprecated. -```json -{ - "import": [ - "example/json-utils" - ] -} -``` +Golem's stock MoonBit build template currently assumes a single-module artifact path. Adding +workspace members changes Moon's output path, so an unchanged `golem build` will not find the +component WASM. A Golem component that uses `moon.work` must override its debug and release build, +embed-source, and `componentWasm` paths. Do not present a workspace dependency as a drop-in change +to a generated Golem application. -## Key Constraints +Golem-generated MoonBit bridge modules are a deliberate exception: the bridge generator currently +emits self-contained modules with `moon.mod.json`. Do not rename or edit that generated file. Put +the application and generated module in `moon.work`, then import the generated module by its module +name from the application's `moon.mod` and import its packages from `moon.pkg`. -- Only packages published on [mooncakes.io](https://mooncakes.io) can be added as dependencies -- Ensure the package is compatible with the `wasm` / `wasm-gc` backend — some MoonBit packages may only support native or JS targets -- After adding a dependency, always run `moon install` before `golem build` -- Check the package's documentation on mooncakes.io for usage examples and API reference +For an ordinary registry dependency, run `moon check` and `golem build --yes`. Confirm that the +dependency supports the component's `wasm` target; native-only and JavaScript-only packages cannot +be linked into a Golem agent. diff --git a/docs/src/content/next/how-to-guides/moonbit/golem-call-another-agent-moonbit.mdx b/docs/src/content/next/how-to-guides/moonbit/golem-call-another-agent-moonbit.mdx index c95742404d..26a8133c9f 100644 --- a/docs/src/content/next/how-to-guides/moonbit/golem-call-another-agent-moonbit.mdx +++ b/docs/src/content/next/how-to-guides/moonbit/golem-call-another-agent-moonbit.mdx @@ -6,10 +6,12 @@ The `#derive.agent` code generation tool auto-generates a `Client` st ## Getting a Client (Scoped) -Use `Client::scoped(...)` with the target agent's constructor parameters and a callback. The client is automatically dropped when the callback returns: +Use `Client::scoped(...)` with the target agent's constructor parameters and an async +callback. Call it from an `async fn`; MoonBit has no `await` keyword. The client is automatically +dropped when the callback returns: ```moonbit -CounterClient::scoped("my-counter", fn(counter) raise @common.AgentError { +CounterClient::scoped("my-counter", async fn(counter) { counter.increment() counter.increment() let value = counter.get_value() @@ -39,7 +41,7 @@ This does **not** create the agent — the agent is created implicitly on its fi Call a method and block until the result returns: ```moonbit -CounterClient::scoped("my-counter", fn(counter) raise @common.AgentError { +CounterClient::scoped("my-counter", async fn(counter) { counter.increment() let count = counter.get_value() count @@ -53,7 +55,7 @@ The calling agent **blocks** until the target agent processes the request and re Agent methods accept custom types defined in your agent code: ```moonbit -TaskManagerClient::scoped(fn(tm) raise @common.AgentError { +TaskManagerClient::scoped(async fn(tm) { let count = tm.add_task({ title: "Build RPC support", priority: High, @@ -88,22 +90,19 @@ components: - example:weather/WeatherAgent ``` -`golem build` generates `golem-temp/bridge-sdk/moonbit/internal/weather-agent-guest-client`. Add it as a local module dependency in the application's `moon.mod.json`: - -```json -{ - "name": "example/weather-app", - "preferred-target": "wasm", - "deps": { - "golemcloud/golem_sdk": "0.5.1", - "weather-agent-guest-client": { - "path": "golem-temp/bridge-sdk/moonbit/internal/weather-agent-guest-client" - } - } -} -``` +`golem build` generates `golem-temp/bridge-sdk/moonbit/internal/weather-agent-guest-client`. The +generated module intentionally still contains `moon.mod.json`; do not rename or edit it. + +An application using current `moon.mod` would normally put that generated module and the +application in `moon.work`, import `weather-agent-guest-client@0.0.1` from `moon.mod`, and import its +client package from `moon.pkg`. This is **not currently a drop-in Golem build setup**: multiple +workspace members add the module name to Moon's output path, while Golem's stock MoonBit build +template embeds the single-module path. Use this bridge only after providing workspace-aware debug +and release build/embed overrides in `golem.yaml`; otherwise the build will not find the component +WASM. Do not revert the application to legacy `moon.mod.json` merely to add a path dependency. -Import its client package from the caller's `moon.pkg`: +With that custom build integration in place, import the generated client package from the caller's +`moon.pkg`: ```moonbit import { diff --git a/docs/src/content/next/how-to-guides/moonbit/golem-call-from-external-moonbit.mdx b/docs/src/content/next/how-to-guides/moonbit/golem-call-from-external-moonbit.mdx index c0f59a8a87..64c8548463 100644 --- a/docs/src/content/next/how-to-guides/moonbit/golem-call-from-external-moonbit.mdx +++ b/docs/src/content/next/how-to-guides/moonbit/golem-call-from-external-moonbit.mdx @@ -32,32 +32,44 @@ The recommended approach is to declare the bridge in `golem.yaml` (as shown abov golem build --yes ``` -This produces a MoonBit module per agent type (e.g., `my-agent-client/`) in the configured output directory (or `golem-temp/bridge-sdk/moonbit/` by default). Re-running `golem build` after agent changes keeps the generated client in sync automatically. +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 as a Local Dependency +## Step 3: Add the Generated Module to a Workspace -The generated `my-agent-client/` directory is a self-contained `moon` module. Its `moon.mod.json` declares an agent-derived module name matching the generated directory name (for example, `my-agent-client`), so multiple generated bridge modules can be used from the same external project. The module bundles its own runtime, only depends on `moonbitlang/async` and the MoonBit core library, and builds for the `native` target. +The generated `my-agent-client/` directory is a self-contained module. The generator deliberately +emits its metadata as `moon.mod.json`; do not rename or edit that generated file. The application +itself should use the current `moon.mod` format. -Add it to your external MoonBit project as a local path dependency in `moon.mod.json`. Also depend on `moonbitlang/async` directly — your own `async fn main` entry point needs it imported (see the next step), and the version must match the one the generated module pins (`0.21.2`): +Create the external application in `external-client/`. Because local path dependencies in +`moon.mod` are deprecated, add `external-client/moon.work` with both modules: -```json -{ - "name": "my-org/external-client", - "preferred-target": "native", - "deps": { - "my-agent-client": { - "path": "../golem-temp/bridge-sdk/moonbit/my-agent-client" - }, - "moonbitlang/async": "0.21.2" - } -} +```moonbit +members = [ + ".", + "../bridge-sdk/moonbit/my-agent-client", +] ``` -The `path` points to the directory containing the generated module's `moon.mod.json`. Since the client uses the native HTTP transport, `preferred-target` is set to `native` for your standalone app. +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.json`: + +```moonbit +name = "my-org/external-client" +preferred_target = "native" + +import { + "my-agent-client@0.0.1", + "moonbitlang/async@0.21.2", +} +``` -Then run `moon install`. +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 @@ -176,7 +188,7 @@ let reply = agent.analyze([ ## Unstructured Text and Binary -Rich `text` and `binary` parameters and return values map to the ergonomic +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: @@ -190,7 +202,9 @@ which is either inline content or a URL reference: @runtime.BinaryUrl("https://example.com/cat.png") ``` -When the agent restricts the allowed language codes or MIME types, the generated +These wrappers do not represent arbitrary bare text or binary schema values. Bare binary maps to +`@runtime.AgentBinary`, and bare text is not supported by this bridge mapping. 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. @@ -208,4 +222,5 @@ Each agent type gets its own `moon` module directory containing: - 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 -- Add the generated module as a local path dependency and run `moon install` before using it +- Keep application metadata in `moon.mod`; leave the generated bridge's `moon.mod.json` unchanged +- Resolve the generated module through `moon.work`, then run `moon check --target native` diff --git a/docs/src/content/next/how-to-guides/moonbit/golem-call-tool-moonbit.mdx b/docs/src/content/next/how-to-guides/moonbit/golem-call-tool-moonbit.mdx new file mode 100644 index 0000000000..4f53940f7e --- /dev/null +++ b/docs/src/content/next/how-to-guides/moonbit/golem-call-tool-moonbit.mdx @@ -0,0 +1,16 @@ +# Call a Golem tool from MoonBit + +`#derive.tool` generates `Client` in `golem_tool_clients.mbt`: + +```moonbit +async fn call_echo() -> Result[String, @tool.ToolError[@tool.NoToolError]] { + let client = EchoClient::new() + defer client.drop() + client.echo("hello") +} +``` + +Call generated tool methods from an `async fn`; MoonBit has no `await` keyword. +Use `new_for(name)` when targeting another registration name. Generated signatures preserve custom +errors and stream capabilities; handle the returned `@tool.ToolError[E]` rather than assuming every +failure is a domain error. Always call `drop()` when finished, and never edit the generated client. diff --git a/docs/src/content/next/how-to-guides/moonbit/golem-define-tool-moonbit.mdx b/docs/src/content/next/how-to-guides/moonbit/golem-define-tool-moonbit.mdx new file mode 100644 index 0000000000..bd0a14628e --- /dev/null +++ b/docs/src/content/next/how-to-guides/moonbit/golem-define-tool-moonbit.mdx @@ -0,0 +1,20 @@ +# Define a Golem tool in MoonBit + +Annotate an empty struct with `#derive.tool` and implement commands as public static methods: + +```moonbit +/// Echo text. +#derive.tool("echo", version="1.0.0") +struct Echo {} + +/// Echo one value. +#derive.arg("value", scope="positional") +pub fn Echo::echo(value : String) -> String { + value +} +``` + +Use `#derive.command`, `#derive.arg`, `#derive.constraint`, `#derive.result`, and `#derive.error` +for explicit command metadata. Custom schema values need `#derive.golem_schema`. `golem build` +generates registration and typed clients; never edit `golem_tool_clients.mbt` or other generated +files. diff --git a/docs/src/content/next/how-to-guides/moonbit/golem-http-params-moonbit.mdx b/docs/src/content/next/how-to-guides/moonbit/golem-http-params-moonbit.mdx index dc2887eca8..f145905725 100644 --- a/docs/src/content/next/how-to-guides/moonbit/golem-http-params-moonbit.mdx +++ b/docs/src/content/next/how-to-guides/moonbit/golem-http-params-moonbit.mdx @@ -111,97 +111,12 @@ Each unmapped parameter becomes a top-level field in the expected JSON body obje > **⚠️ Important:** The request body is **always** a JSON object with parameter names as keys — even when there is only a single body parameter. For example, an endpoint `decide(self : Self, decision : String)` expects `{"decision": "approved"}`, **never** a bare string like `"approved"`. Sending a non-object JSON value or plain text will fail with `REQUEST_JSON_BODY_PARSING_FAILED`. -## Binary Request and Response Bodies +## Body Format -Use `UnstructuredBinary` from the SDK for raw binary payloads: - -```moonbit -///| -/// Accepting any binary content type -#derive.endpoint(post="/upload/{bucket}") -pub fn TaskAgent::upload(self : Self, bucket : String, payload : UnstructuredBinary) -> Int64 { - match payload { - Url(_) => -1L - Inline(data~, mime_type~) => data.length().to_int64() - } -} - -///| -/// Restricting to specific MIME types -#derive.endpoint(post="/upload-image/{bucket}") -#derive.mime_types("payload", "image/png", "image/jpeg") -pub fn TaskAgent::upload_image(self : Self, bucket : String, payload : UnstructuredBinary) -> Int64 { - match payload { - Url(_) => -1L - Inline(data~, mime_type~) => data.length().to_int64() - } -} - -///| -/// Returning binary data -#derive.endpoint(get="/download") -pub fn TaskAgent::download(self : Self) -> UnstructuredBinary { - UnstructuredBinary::from_inline(b"\x01\x02\x03\x04", mime_type="application/octet-stream") -} -``` - -`UnstructuredBinary` parameters cannot be bound to path, query, or header variables. - -## Plain Text Request and Response Bodies - -Use `UnstructuredText` from the SDK for raw `text/plain` payloads. Like -`UnstructuredBinary`, a method using `UnstructuredText` may have **only one -body parameter**, and that parameter cannot be bound to a path/query/header/mount -variable. The body is decoded as UTF-8. - -```moonbit -///| -/// Accepting any text/plain content -#derive.endpoint(post="/notes/{id}") -pub fn TaskAgent::add_note(self : Self, id : String, body : UnstructuredText) -> UInt64 { - match body { - Url(_) => 0UL - Inline(data~, language_code~) => data.length().to_uint64() - } -} - -///| -/// Restricting to specific language codes -#derive.endpoint(post="/translate/{id}") -#derive.text_languages("body", "en", "de") -pub fn TaskAgent::translate(self : Self, id : String, body : UnstructuredText) -> String { - match body { - Url(_) => "" - Inline(data~, ..) => data - } -} - -///| -/// Returning text/plain -#derive.endpoint(get="/notes/{id}") -pub fn TaskAgent::get_note(self : Self, id : String) -> UnstructuredText { - UnstructuredText::from_inline("hello", language_code=Some("en")) -} -``` - -HTTP-level rules: -- The request must have either no `Content-Type`, `text/plain`, or - `text/plain; charset=utf-8` (case-insensitive). Any other content type is - rejected with `415 Unsupported Media Type`. -- `Content-Language` is **always optional**, even when language codes are - restricted via `#derive.text_languages`. If present, it must be a single - value (multi-valued or comma-separated headers are rejected with - `400 Bad Request`). -- When restricted, the supplied `Content-Language` is matched - case-insensitively against the allowed list; otherwise `415 Unsupported Media Type`. -- A non-UTF-8 request body is rejected with `400 Bad Request`. -- `Content-Language` cannot also be bound as an endpoint header parameter when - the body is `UnstructuredText` — that header is reserved for declaring the - body language. - -The response is sent as `Content-Type: text/plain; charset=utf-8`. If the -returned `UnstructuredText` (inline form) carries a language code, it is -forwarded as the `Content-Language` response header. +The current MoonBit SDK maps endpoint bodies through schema-backed JSON values. It does not expose +the removed `@types.UnstructuredText` or `@types.UnstructuredBinary` wrappers, and +`#derive.text_languages` and `#derive.mime_types` no longer exist. Do not use those old APIs for +raw `text/plain` or binary request and response bodies. ## Return Type to HTTP Response Mapping @@ -212,8 +127,6 @@ forwarded as the `Content-Language` response header. | `T?` (`Option[T]`) | 200 OK if `Some`, 404 Not Found if `None` | JSON `T` or empty | | `Result[T, E]` | 200 OK if `Ok`, 500 Internal Server Error if `Err` | JSON `T` or JSON `E` | | `Result[Unit, E]` | 204 No Content if `Ok`, 500 if `Err` | empty or JSON `E` | -| `UnstructuredBinary` | 200 OK | Raw binary with Content-Type | -| `UnstructuredText` | 200 OK | `text/plain; charset=utf-8` (+ optional `Content-Language`) | ## Data Type to JSON Mapping diff --git a/docs/src/content/next/how-to-guides/moonbit/golem-permission-card-moonbit.mdx b/docs/src/content/next/how-to-guides/moonbit/golem-permission-card-moonbit.mdx new file mode 100644 index 0000000000..f34ca66a4a --- /dev/null +++ b/docs/src/content/next/how-to-guides/moonbit/golem-permission-card-moonbit.mdx @@ -0,0 +1,23 @@ +# Permission cards in MoonBit + +Import the low-level schema model and core WIT types in `moon.pkg`: + +```moonbit +import { + "golemcloud/golem_sdk/schema_model" @model, + "golemcloud/golem_sdk/interface/golem/core/types" @types, +} +``` + +The carrier is `@model.GuestPermissionCardHandle`. It wraps a runtime-provided +`@types.PermissionCard` and can be placed in `@model.SchemaValue::PermissionCard` with a matching +`@model.SchemaTypeBody::PermissionCard(@types.PermissionCardSpec)`. + +Use `is_present()` to check whether ownership remains and `take()` only when transferring the raw +resource to a host API. `with_handle(...)` borrows it without transfer. Do not construct a fake raw +card, persist it, compare its contents, or call `take()` twice. + +The current high-level MoonBit agent/tool derives do not expose a permission-card field annotation +equivalent to TypeScript's `s.permissionCard`. Use this carrier only in APIs that already operate on +raw schema values, such as universal tool middleware; do not fabricate a high-level derived method +signature. diff --git a/docs/src/content/next/how-to-guides/moonbit/golem-tools-middleware-moonbit.mdx b/docs/src/content/next/how-to-guides/moonbit/golem-tools-middleware-moonbit.mdx new file mode 100644 index 0000000000..cba6636d07 --- /dev/null +++ b/docs/src/content/next/how-to-guides/moonbit/golem-tools-middleware-moonbit.mdx @@ -0,0 +1,24 @@ +# Tool middleware in MoonBit + +For typed middleware, annotate an empty struct and implement async methods whose first parameter is +the generated underlying. `Echo` must be a same-package `#derive.tool` declaration; see +[`golem-define-tool-moonbit`](/next/how-to-guides/moonbit/golem-define-tool-moonbit). + +```moonbit +#derive.tool_middleware("echo-policy", presented="Echo") +struct EchoPolicy {} + +pub async fn EchoPolicy::echo( + underlying : EchoUnderlying, + value : String, +) -> Result[String, @toolMiddleware.ToolInvokeError[@tool.NoToolError]] { + underlying.echo(value) +} +``` + +Set `expected="OtherTool"` for an adapter and translate its values and errors. Use +`#derive.universal_tool_middleware` on an async free function only when arbitrary tool metadata and +raw carriers are required. MoonBit async functions use no `await` keyword. The underlying tool is +valid only during this middleware invocation and may be called zero, one, or multiple times. Raw +input/result carriers and owned stream or permission-card handles are take-once: do not reuse a +transferred carrier or let invocation-scoped resources escape. diff --git a/docs/src/content/next/how-to-guides/rust.mdx b/docs/src/content/next/how-to-guides/rust.mdx index b1cab88be0..0070abcde4 100644 --- a/docs/src/content/next/how-to-guides/rust.mdx +++ b/docs/src/content/next/how-to-guides/rust.mdx @@ -6,14 +6,15 @@ Guides specific to developing Golem agents in Rust. + - + @@ -23,6 +24,7 @@ Guides specific to developing Golem agents in Rust. + @@ -34,12 +36,14 @@ Guides specific to developing Golem agents in Rust. + - + + - + diff --git a/docs/src/content/next/how-to-guides/rust/_meta.js b/docs/src/content/next/how-to-guides/rust/_meta.js index 40aa74b847..fc42f5d05e 100644 --- a/docs/src/content/next/how-to-guides/rust/_meta.js +++ b/docs/src/content/next/how-to-guides/rust/_meta.js @@ -1,13 +1,14 @@ export default { "golem-add-rust-crate": "Add a Rust Crate Dependency", + "golem-add-llm-rust": "Add AI capabilities to a Rust agent", "golem-add-agent-rust": "Adding a New Agent to a Rust Golem Component", "golem-add-http-endpoint-rust": "Adding HTTP Endpoints to a Rust Golem Agent", - "golem-add-llm-rust": "Adding LLM and AI Capabilities (Rust)", "golem-quota-rust": "Adding Resource Quotas to an Agent (Rust)", "golem-add-secret-rust": "Adding Secrets to a Rust Agent", "golem-add-config-rust": "Adding Typed Configuration to an Agent (Rust)", "golem-annotate-agent-rust": "Annotating Agent Methods (Rust)", "golem-atomic-block-rust": "Atomic Blocks and Durability Controls (Rust)", + "golem-call-tool-rust": "Call a Golem tool from Rust", "golem-call-from-external-rust": "Calling Agents from External Rust Applications", "golem-agent-reflection-rust": "Calling Agents with Runtime Reflection (Rust)", "golem-call-another-agent-rust": "Calling Another Agent (Rust)", @@ -17,6 +18,7 @@ export default { "golem-create-agent-instance-rust": "Creating a Golem Agent Instance with `golem agent new`", "golem-stateless-agent-rust": "Creating Ephemeral (Stateless) Agents (Rust)", "golem-custom-snapshot-rust": "Custom Snapshots in Rust", + "golem-define-tool-rust": "Define a Golem tool in Rust", "golem-add-http-auth-rust": "Enabling Authentication on Rust HTTP Endpoints", "golem-enable-otlp-rust": "Enabling OpenTelemetry for a Rust Agent", "golem-file-io-rust": "File I/O in Rust Golem Agents", @@ -28,12 +30,14 @@ export default { "golem-make-http-request-rust": "Making Outgoing HTTP Requests (Rust)", "golem-mark-read-only-rust": "Marking Agent Methods as Read-Only (Rust)", "golem-parallel-workers-rust": "Parallel Workers — Fan-Out / Fan-In (Rust)", + "golem-permission-card-rust": "Permission cards in Rust", "golem-multi-instance-agent-rust": "Phantom Agents in Rust", - "golem-recurring-task-rust": "Recurring Tasks via Self-Scheduling (Rust)", + "golem-recurring-task-rust": "Recurring tasks via self-scheduling (Rust)", "golem-add-transactions-rust": "Saga-Pattern Transactions (Rust)", + "golem-schedule-future-call-rust": "Schedule a future agent invocation (Rust)", "golem-schedule-agent-rust": "Scheduling a Future Agent Invocation", - "golem-schedule-future-call-rust": "Scheduling a Future Agent Invocation (Rust)", "golem-streaming-agent-rust": "Streaming Agent Methods in Rust", + "golem-tools-middleware-rust": "Tool middleware in Rust", "golem-trigger-agent-rust": "Triggering a Fire-and-Forget Agent Invocation", "golem-add-ignite-rust": "Using Apache Ignite from a Rust Agent", "golem-add-mysql-rust": "Using MySQL from a Rust Agent", diff --git a/docs/src/content/next/how-to-guides/rust/golem-add-llm-rust.mdx b/docs/src/content/next/how-to-guides/rust/golem-add-llm-rust.mdx index 2f16d6f134..e4f924dcf8 100644 --- a/docs/src/content/next/how-to-guides/rust/golem-add-llm-rust.mdx +++ b/docs/src/content/next/how-to-guides/rust/golem-add-llm-rust.mdx @@ -1,358 +1,46 @@ -# Adding LLM and AI Capabilities (Rust) +# Add AI capabilities to a Rust agent -## Overview +The `golemcloud/golem-ai` project provides provider-neutral core crates and provider crates. Golem +agents now target `wasm32-wasip2` and WASI HTTP 0.3, so dependency selection matters. -Golem provides the **golem-ai** library collection — a set of Rust crates from [golemcloud/golem-ai](https://github.com/golemcloud/golem-ai) that provide unified, provider-agnostic APIs for AI capabilities. Each domain has a **core crate** (shared types and traits) plus **provider crates** (concrete backends). You add them as regular Cargo dependencies and call them directly from your agent code. +## Compatibility status -> **These crates are not on crates.io yet.** Use git dependencies pointing to the `v0.5.1` tag. +There is currently no verified `golem-ai` release or revision compatible with this repository's +Rust SDK. The latest crates.io release, `0.5.2`, predates the WASIp2/WASI P3 migration. The current +upstream source targets `wasm32-wasip2`, but still expects the older infallible Golem secret API and +does not compile against this SDK with its default Golem integration enabled. -## Available Libraries +Do not add `0.5.2`, pin the upstream migration commit, disable durability features, or copy an old +provider example merely to make the dependency resolve. Recheck upstream releases and build the +chosen version in a current scaffold before documenting or shipping it. Keep every selected core +and provider crate on the same verified release or revision. -### LLM (Chat Completions) +## Available crate families -Core crate: `golem-ai-llm` — unified chat completion API with blocking and streaming responses, multi-turn conversation, tool calling, and multimodal image inputs. +Choose one core crate and the provider crate needed by the application: -Provider crates (pick one): +| Capability | Core crate | Providers | +|---|---|---| +| LLM chat | `golem-ai-llm` | Anthropic, Bedrock, Grok, Ollama, OpenAI, OpenRouter | +| Embeddings/reranking | `golem-ai-embed` | Cohere, Hugging Face, OpenAI, VoyageAI | +| Web search | `golem-ai-web-search` | Brave, Google, Serper, Tavily | +| Document search | `golem-ai-search` | Algolia, Elasticsearch, Meilisearch, OpenSearch, Typesense | +| Graph database | `golem-ai-graph` | ArangoDB, JanusGraph, Neo4j | +| Vector database | `golem-ai-vector` | Milvus, pgvector, Pinecone, Qdrant | +| Video | `golem-ai-video` | Kling, Runway, Stability, Veo | +| Speech-to-text | `golem-ai-stt` | AWS, Azure, Deepgram, Google, Whisper | +| Text-to-speech | `golem-ai-tts` | AWS, Deepgram, ElevenLabs, Google | +| Code execution | `golem-ai-exec` | JavaScript and Python execution | -| Provider | Crate | Required Env Vars | -|----------|-------|-------------------| -| OpenAI | `golem-ai-llm-openai` | `OPENAI_API_KEY` | -| Anthropic | `golem-ai-llm-anthropic` | `ANTHROPIC_API_KEY` | -| Amazon Bedrock | `golem-ai-llm-bedrock` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION` | -| xAI / Grok | `golem-ai-llm-grok` | `XAI_API_KEY` | -| Ollama | `golem-ai-llm-ollama` | `GOLEM_OLLAMA_BASE_URL` (optional, defaults to `http://localhost:11434`) | -| OpenRouter | `golem-ai-llm-openrouter` | `OPENROUTER_API_KEY` | +The repository also contains `golem-ai-http`, the shared WASI HTTP transport. Provider crate names +follow `golem-ai--`. -### Embeddings & Reranking +## Safe alternative -Core crate: `golem-ai-embed` — generate vector embeddings from text/images and rerank documents by relevance. +Until a compatible release exists, call a provider's HTTPS API with the supported outgoing HTTP +client described by [`golem-make-http-request-rust`](/next/how-to-guides/rust/golem-make-http-request-rust). Store credentials with Golem secrets; load +[`golem-add-secret-rust`](/next/how-to-guides/rust/golem-add-secret-rust) for provisioning. External calls made through supported host APIs remain +durable, while a generic third-party HTTP client may not integrate correctly with replay. -Provider crates: - -| Provider | Crate | Required Env Vars | -|----------|-------|-------------------| -| OpenAI | `golem-ai-embed-openai` | `OPENAI_API_KEY` | -| Cohere | `golem-ai-embed-cohere` | `COHERE_API_KEY` | -| Hugging Face | `golem-ai-embed-hugging-face` | `HUGGING_FACE_API_KEY` | -| VoyageAI | `golem-ai-embed-voyageai` | `VOYAGEAI_API_KEY` | - -### Web Search - -Core crate: `golem-ai-web-search` — unified web search with one-shot and paginated session modes. - -Provider crates: - -| Provider | Crate | Required Env Vars | -|----------|-------|-------------------| -| Brave | `golem-ai-web-search-brave` | `BRAVE_API_KEY` | -| Google | `golem-ai-web-search-google` | `GOOGLE_API_KEY`, `GOOGLE_SEARCH_ENGINE_ID` | -| Serper | `golem-ai-web-search-serper` | `SERPER_API_KEY` | -| Tavily | `golem-ai-web-search-tavily` | `TAVILY_API_KEY` | - -### Document Search - -Core crate: `golem-ai-search` — full-text/document search with index management, document CRUD, faceted search. - -Provider crates: - -| Provider | Crate | Required Env Vars | -|----------|-------|-------------------| -| Algolia | `golem-ai-search-algolia` | `ALGOLIA_APPLICATION_ID`, `ALGOLIA_API_KEY` | -| Elasticsearch | `golem-ai-search-elasticsearch` | `ELASTICSEARCH_URL`, credentials | -| Meilisearch | `golem-ai-search-meilisearch` | `MEILISEARCH_BASE_URL`, `MEILISEARCH_API_KEY` | -| OpenSearch | `golem-ai-search-opensearch` | `OPENSEARCH_BASE_URL`, credentials | -| Typesense | `golem-ai-search-typesense` | `TYPESENSE_BASE_URL`, `TYPESENSE_API_KEY` | - -### Graph Databases - -Core crate: `golem-ai-graph` — vertex/edge CRUD, traversal, path-finding, transactions, schema management. - -Provider crates: - -| Provider | Crate | -|----------|-------| -| ArangoDB | `golem-ai-graph-arangodb` | -| JanusGraph | `golem-ai-graph-janusgraph` | -| Neo4j | `golem-ai-graph-neo4j` | - -### Vector Databases - -Core crate: `golem-ai-vector` — collection management, vector upsert/search, ANN queries, namespaces. - -Provider crates: - -| Provider | Crate | -|----------|-------| -| Qdrant | `golem-ai-vector-qdrant` | -| Milvus | `golem-ai-vector-milvus` | -| PgVector | `golem-ai-vector-pgvector` | -| Pinecone | `golem-ai-vector-pinecone` | - -### Video Generation - -Core crate: `golem-ai-video` — text-to-video, image-to-video, async job polling. - -Provider crates: - -| Provider | Crate | -|----------|-------| -| Google Veo | `golem-ai-video-veo` | -| Stability AI | `golem-ai-video-stability` | -| Kling | `golem-ai-video-kling` | -| Runway ML | `golem-ai-video-runway` | - -### Speech-to-Text - -Core crate: `golem-ai-stt` — audio transcription with speaker diarization, word-level timing. - -Provider crates: - -| Provider | Crate | -|----------|-------| -| OpenAI Whisper | `golem-ai-stt-whisper` | -| Deepgram | `golem-ai-stt-deepgram` | -| AWS Transcribe | `golem-ai-stt-aws` | -| Azure Speech | `golem-ai-stt-azure` | -| Google STT | `golem-ai-stt-google` | - -### Text-to-Speech - -Core crate: `golem-ai-tts` — voice discovery, batch/streaming synthesis, SSML support. - -Provider crates: - -| Provider | Crate | -|----------|-------| -| AWS Polly | `golem-ai-tts-aws` | -| Deepgram | `golem-ai-tts-deepgram` | -| ElevenLabs | `golem-ai-tts-elevenlabs` | -| Google Cloud TTS | `golem-ai-tts-google` | - -## Adding Dependencies - -Add the core crate plus your chosen provider to the component's `Cargo.toml`: - -```toml -[dependencies] -# LLM — core + provider -golem-ai-llm = "0.5.1" -golem-ai-llm-openai = "0.5.1" -``` - -Store the required API key as a **secret** using Golem's typed config system. See the [`golem-add-secret-rust`](/next/how-to-guides/rust/golem-add-secret-rust) skill for full details. In brief: - -```rust -use golem_rust::ConfigSchema; -use golem_rust::agentic::{Config, Secret}; - -#[derive(ConfigSchema)] -pub struct MyAgentConfig { - #[config_schema(secret)] - pub api_key: Secret, -} -``` - -Then manage the secret via the CLI: - -```shell -golem secret create api_key --secret-type String --secret-value "sk-..." -``` - -## Usage: LLM Chat Completion - -```rust -use golem_ai_llm::model::*; -use golem_ai_llm::LlmProvider; - -// Pick a provider — type alias makes it easy to swap later -type Provider = golem_ai_llm_openai::DurableOpenAI; - -let config = Config { - model: "gpt-4o".to_string(), - temperature: None, - max_tokens: None, - stop_sequences: None, - tools: None, - tool_choice: None, - provider_options: None, -}; - -let events = vec![Event::Message(Message { - role: Role::User, - name: None, - content: vec![ContentPart::Text("Hello!".to_string())], -})]; - -// Blocking request -let response = Provider::send(events, config).expect("LLM call failed"); - -// Extract text from response -let text: String = response - .content - .iter() - .filter_map(|part| match part { - ContentPart::Text(txt) => Some(txt.clone()), - _ => None, - }) - .collect::>() - .join("\n"); -``` - -## Usage: Multi-turn Conversation (Session) - -```rust -use golem_ai_llm::model::*; -use golem_ai_llm::LlmProvider; - -type Provider = golem_ai_llm_openai::DurableOpenAI; - -// Keep events as agent state for multi-turn conversation -let mut events: Vec = vec![]; - -// Add system message -events.push(Event::Message(Message { - role: Role::System, - name: None, - content: vec![ContentPart::Text("You are a helpful assistant.".to_string())], -})); - -// Add user message -events.push(Event::Message(Message { - role: Role::User, - name: None, - content: vec![ContentPart::Text("What is Golem?".to_string())], -})); - -let config = Config { - model: "gpt-4o".to_string(), - temperature: None, - max_tokens: None, - stop_sequences: None, - tools: None, - tool_choice: None, - provider_options: None, -}; - -// Send and record the response for next turn -let response = Provider::send(events.clone(), config.clone()).expect("LLM call failed"); -events.push(Event::Response(response)); -``` - -## Usage: Web Search - -```rust -use golem_ai_web_search::model::types; -use golem_ai_web_search::model::web_search; - -type SearchProvider = golem_ai_web_search_google::DurableGoogleCustomSearch; - -let session = SearchProvider::start_search(&web_search::SearchParams { - query: "Golem distributed computing".to_string(), - language: Some("lang_en".to_string()), - safe_search: Some(types::SafeSearchLevel::Off), - max_results: Some(10), - time_range: None, - include_domains: None, - exclude_domains: None, - include_images: None, - include_html: None, - advanced_answer: Some(true), - region: None, -}).expect("Failed to start search"); - -let results = session.next_page().expect("Failed to get results"); -for result in results { - println!("{}: {}", result.title, result.url); -} -``` - -## Switching Providers - -To switch providers, change the type alias and dependency. The core API stays the same: - -```rust -// Switch from OpenAI to Anthropic: -// 1. In Cargo.toml: replace golem-ai-llm-openai with golem-ai-llm-anthropic -// 2. In code: -type Provider = golem_ai_llm_anthropic::DurableAnthropic; -// All other code stays the same -``` - -## Complete Agent Example - -```rust -use golem_ai_llm::model::*; -use golem_ai_llm::LlmProvider; -use golem_rust::{agent_definition, agent_implementation, endpoint}; - -type Provider = golem_ai_llm_openai::DurableOpenAI; - -#[agent_definition(mount = "/chats/{chat_name}")] -pub trait ChatAgent { - fn new(chat_name: String) -> Self; - - #[endpoint(post = "/ask")] - async fn ask(&mut self, question: String) -> String; -} - -struct ChatAgentImpl { - chat_name: String, - events: Vec, - config: Config, -} - -#[agent_implementation] -impl ChatAgent for ChatAgentImpl { - fn new(chat_name: String) -> Self { - let config = Config { - model: std::env::var("LLM_MODEL").unwrap_or_else(|_| "gpt-4o".to_string()), - temperature: None, - max_tokens: None, - stop_sequences: None, - tools: None, - tool_choice: None, - provider_options: None, - }; - let events = vec![Event::Message(Message { - role: Role::System, - name: None, - content: vec![ContentPart::Text(format!( - "You are a helpful assistant for chat '{}'", - chat_name - ))], - })]; - Self { chat_name, events, config } - } - - async fn ask(&mut self, question: String) -> String { - self.events.push(Event::Message(Message { - role: Role::User, - name: None, - content: vec![ContentPart::Text(question)], - })); - - let response = Provider::send(self.events.clone(), self.config.clone()) - .expect("LLM call failed"); - self.events.push(Event::Response(response.clone())); - - response - .content - .iter() - .filter_map(|part| match part { - ContentPart::Text(txt) => Some(txt.clone()), - _ => None, - }) - .collect::>() - .join("\n") - } -} -``` - -## Key Constraints - -- All golem-ai crates should use version `"0.5.1"` from crates.io -- Always add both the core crate and a provider crate (e.g., `golem-ai-llm` + `golem-ai-llm-openai`) -- Provider API keys should be stored as secrets using Golem's typed config system (See the [`golem-add-secret-rust`](/next/how-to-guides/rust/golem-add-secret-rust) guide) -- The `Durable*` provider types (e.g., `DurableOpenAI`) automatically integrate with Golem's durable execution — responses are recorded in the oplog and replayed on recovery -- To switch providers, change the type alias and Cargo dependency — the rest of the code stays the same -- These crates target `wasm32-wasip1` and work correctly in Golem's WebAssembly environment +When upstream compatibility is restored, verify the provider configuration, async call signature, +and secret integration from that exact release rather than relying on the examples from `0.5.2`. diff --git a/docs/src/content/next/how-to-guides/rust/golem-add-rust-crate.mdx b/docs/src/content/next/how-to-guides/rust/golem-add-rust-crate.mdx index 4f3c899d52..29c5efd1fc 100644 --- a/docs/src/content/next/how-to-guides/rust/golem-add-rust-crate.mdx +++ b/docs/src/content/next/how-to-guides/rust/golem-add-rust-crate.mdx @@ -3,9 +3,11 @@ ## Important constraints - The compilation target is `wasm32-wasip2` — only crates that support this target will work. -- Crates that use threads, native system calls, `mmap`, networking via `std::net`, or platform-specific C libraries **will not compile**. -- Pure Rust crates and crates that support `wasm32-wasi` generally work. -- If unsure whether a crate compiles for WASM, add it and run `golem build` to find out. +- Crates requiring unsupported OS facilities may fail to compile or fail at runtime. +- A generic `wasm32-wasi` compatibility claim is insufficient; verify the exact features used + against `wasm32-wasip2` and exercise them in Golem. +- Pure Rust is not by itself proof of compatibility. Platform-specific C libraries, threading, + sockets, and memory-mapping APIs require particular scrutiny. ## Steps @@ -30,7 +32,7 @@ 3. **If the build fails** - - Check the error for unsupported target or missing C dependencies — these crates are incompatible with `wasm32-wasip1`. + - Check the error for unsupported target features or missing native dependencies. - Try enabling a `wasm` or `wasi` feature flag if the crate provides one. - Look for an alternative crate that supports WASM. @@ -45,8 +47,9 @@ These crates are already in the project's `Cargo.toml` — do NOT add them again ## HTTP and networking -Use `wstd::http` for HTTP requests. The standard `std::net` module is **not available** on WASM. +Prefer `wstd::http` for outgoing HTTP requests. Compile support for an API does not guarantee that +the runtime grants the required network capability, so test the actual operation in Golem. ## AI / LLM features -To add AI capabilities, add the relevant `golem-ai-*` provider crate (e.g., `golem-ai-llm-openai`) and configure the provider in the component's `golem.yaml` dependencies section. +To add AI capabilities, See the [`golem-add-llm-rust`](/next/how-to-guides/rust/golem-add-llm-rust) guide. The published `golem-ai` release and the current WASIp2-compatible source do not currently use the same API, so do not guess a crate version or copy an older example. diff --git a/docs/src/content/next/how-to-guides/rust/golem-call-tool-rust.mdx b/docs/src/content/next/how-to-guides/rust/golem-call-tool-rust.mdx new file mode 100644 index 0000000000..ff641ff46f --- /dev/null +++ b/docs/src/content/next/how-to-guides/rust/golem-call-tool-rust.mdx @@ -0,0 +1,20 @@ +# Call a Golem tool from Rust + +`#[tool_definition]` generates a `Client`. Its async methods return +`Result>`, where `E` is the tool's declared error type: + +```rust +use golem_rust::agentic::ToolError; + +let client = EchoClient::default(); +match client.echo("hello".to_string()).await { + Ok(value) => println!("{value}"), + Err(ToolError::Tool(error)) => eprintln!("declared tool error: {error:?}"), + Err(error) => eprintln!("tool invocation failed: {error}"), +} +``` + +The default client targets the definition's tool name. Use the generated named-target constructor +only when deployment assigns another tool registration name. Do not treat protocol errors as the +tool's declared domain error. Drop or finish any stream handles according to the generated method +signature. diff --git a/docs/src/content/next/how-to-guides/rust/golem-define-tool-rust.mdx b/docs/src/content/next/how-to-guides/rust/golem-define-tool-rust.mdx new file mode 100644 index 0000000000..936017bada --- /dev/null +++ b/docs/src/content/next/how-to-guides/rust/golem-define-tool-rust.mdx @@ -0,0 +1,27 @@ +# Define a Golem tool in Rust + +Declare the public command surface with `#[tool_definition]`, then implement it with +`#[tool_implementation]`. The macro generates metadata, guest exports, and a typed client. + +```rust +use golem_rust::{tool_definition, tool_implementation}; + +#[tool_definition(version = "1.0.0")] +pub trait Echo { + async fn echo(&self, value: String) -> String; +} + +struct EchoImpl; + +#[tool_implementation] +impl Echo for EchoImpl { + async fn echo(&self, value: String) -> String { + value + } +} +``` + +Use `IntoSchema` and `FromSchema` for custom inputs, outputs, and error payload types. For a +declared command error in `Result`, derive `golem_rust::ToolError` on `E` and annotate its +variants with `#[tool_error(...)]`. Keep the definition and implementation in provider source; +never edit macro-generated bindings. A tool component can provide tools without defining an agent. diff --git a/docs/src/content/next/how-to-guides/rust/golem-file-io-rust.mdx b/docs/src/content/next/how-to-guides/rust/golem-file-io-rust.mdx index 6f5fdcd5f0..278885ae6e 100644 --- a/docs/src/content/next/how-to-guides/rust/golem-file-io-rust.mdx +++ b/docs/src/content/next/how-to-guides/rust/golem-file-io-rust.mdx @@ -2,7 +2,7 @@ ## Overview -Golem Rust agents compile to `wasm32-wasip1` which provides WASI filesystem access. Use the standard `std::fs` module for all filesystem operations — it works out of the box with WASI. +Golem Rust agents compile to `wasm32-wasip2`, which provides WASI filesystem access. Use the standard `std::fs` module for filesystem operations — it works with the directories mounted into the agent filesystem. To provision files into an agent's filesystem, See the [`golem-add-initial-files`](/next/how-to-guides/common/golem-add-initial-files) guide. @@ -33,6 +33,7 @@ Only files provisioned with `read-write` permission (or files in non-provisioned ```rust use std::fs; +fs::create_dir_all("/tmp").expect("Failed to create /tmp"); fs::write("/tmp/output.txt", "Hello, world!") .expect("Failed to write file"); ``` @@ -43,6 +44,7 @@ fs::write("/tmp/output.txt", "Hello, world!") use std::fs::OpenOptions; use std::io::Write; +std::fs::create_dir_all("/tmp").expect("Failed to create /tmp"); let mut file = OpenOptions::new() .append(true) .create(true) @@ -106,6 +108,7 @@ impl FileReaderAgent for FileReaderAgentImpl { } fn write_log(&mut self, message: String) { + std::fs::create_dir_all("/tmp").expect("Failed to create /tmp"); let mut file = std::fs::OpenOptions::new() .append(true) .create(true) diff --git a/docs/src/content/next/how-to-guides/rust/golem-permission-card-rust.mdx b/docs/src/content/next/how-to-guides/rust/golem-permission-card-rust.mdx new file mode 100644 index 0000000000..5bee823ecb --- /dev/null +++ b/docs/src/content/next/how-to-guides/rust/golem-permission-card-rust.mdx @@ -0,0 +1,20 @@ +# Permission cards in Rust + +Use `golem_rust::schema::wit::GuestPermissionCardHandle` in an agent or tool input/output. It has +the required `IntoSchema` and `FromSchema` implementations: + +```rust +use golem_rust::schema::wit::GuestPermissionCardHandle; + +async fn forward(card: GuestPermissionCardHandle) -> GuestPermissionCardHandle { + ReceiverClient::get("target".to_string()) + .accept(card) + .await +} +``` + +Cards are opaque affine capabilities. Encoding a call transfers the card, so do not serialize, +clone for reuse, persist, or send the same handle twice. This guide covers transferring an already +received handle. The SDK does not currently expose a supported high-level constructor for wrapping +cards returned by the lower-level permissions host bindings. Do not use hidden schema-codec +constructors as application APIs, invent authority locally, or log card internals. diff --git a/docs/src/content/next/how-to-guides/rust/golem-recurring-task-rust.mdx b/docs/src/content/next/how-to-guides/rust/golem-recurring-task-rust.mdx index 8155ee35a5..8b922a57c9 100644 --- a/docs/src/content/next/how-to-guides/rust/golem-recurring-task-rust.mdx +++ b/docs/src/content/next/how-to-guides/rust/golem-recurring-task-rust.mdx @@ -1,211 +1,101 @@ -# Recurring Tasks via Self-Scheduling (Rust) +# Recurring tasks via self-scheduling (Rust) -## Overview +A durable agent can schedule its own next invocation after completing each tick. Scheduled +invocations survive recovery and execute sequentially with the agent's other invocations. -A Golem agent can act as its own scheduler by calling `schedule_` on itself at the end of each invocation. This creates a durable, crash-resilient recurring task — if the agent restarts, the scheduled invocation is still pending and will fire at the designated time. - -## Basic Pattern - -The agent schedules its own method to run again after a delay: +Use the current `#[agent_definition]` and `#[agent_implementation]` macros, construct a +`golem_rust::ScheduledTime`, pass method arguments before the scheduled time, and handle the +generated scheduling result: ```rust -use golem_rust::wasip2::clocks::wall_clock::Datetime; - -#[agent_definition] -pub trait PollerAgent: HasSchema { - fn new(name: String) -> Self; - fn start(&mut self); - fn poll(&mut self); -} - -impl PollerAgent for PollerAgentImpl { - fn new(name: String) -> Self { - Self { name } - } - - fn start(&mut self) { - // Kick off the first poll - self.poll(); - } - - fn poll(&mut self) { - // 1. Do the recurring work - do_work(); - - // 2. Schedule the next run (60 seconds from now) - let mut client = PollerAgentClient::get(self.name.clone()); - let now_secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - client.schedule_poll(Datetime { - seconds: now_secs + 60, - nanoseconds: 0, - }); +use golem_rust::{ScheduledTime, agent_definition, agent_implementation}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +fn after(delay: Duration) -> ScheduledTime { + let at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time went backwards") + + delay; + ScheduledTime { + seconds: at.as_secs() as i64, + nanoseconds: at.subsec_nanos(), } } -``` -## Exponential Backoff - -Increase the delay on repeated failures, reset on success: - -```rust -fn poll(&mut self) { - let success = try_work(); - - let delay = if success { - self.consecutive_failures = 0; - self.base_interval_secs // e.g. 60 - } else { - self.consecutive_failures += 1; - let backoff = self.base_interval_secs * 2u64.pow(self.consecutive_failures.min(6)); - backoff.min(self.max_interval_secs) // cap at e.g. 3600 - }; - - let mut client = PollerAgentClient::get(self.name.clone()); - let now_secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - client.schedule_poll(Datetime { - seconds: now_secs + delay, - nanoseconds: 0, - }); +#[agent_definition] +pub trait PollerAgent { + fn new(name: String) -> Self; + async fn poll(&mut self, endpoint: String); } -``` - -## Cancellation with CancellationToken -The Rust SDK generates `schedule_cancelable_{method}` variants that return a `CancellationToken`. Store the token and cancel it to stop the next scheduled invocation: - -```rust -fn poll(&mut self) { - if self.cancelled { - return; // stop the loop - } - - do_work(); - - // Schedule next run and store the cancellation token - let mut client = PollerAgentClient::get(self.name.clone()); - let now_secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - self.pending_token = Some(client.schedule_cancelable_poll(Datetime { - seconds: now_secs + 60, - nanoseconds: 0, - })); +struct PollerAgentImpl { + name: String, + stopped: bool, } -fn cancel(&mut self) { - self.cancelled = true; - // Cancel the pending scheduled invocation so it never fires - if let Some(token) = self.pending_token.take() { - token.cancel(); +#[agent_implementation] +impl PollerAgent for PollerAgentImpl { + fn new(name: String) -> Self { + Self { name, stopped: false } } -} -``` -### Cancellation via State Flag + async fn poll(&mut self, endpoint: String) { + if self.stopped { + return; + } -For simpler cases, just use a boolean flag — the next scheduled `poll` checks it and exits early: + do_work(&endpoint).await; -```rust -fn poll(&mut self) { - if self.cancelled { - return; + let me = PollerAgentClient::get(self.name.clone()); + me.schedule_poll(endpoint, after(Duration::from_secs(60))) + .expect("failed to schedule next poll"); } - do_work(); - self.schedule_next(60); } - -fn cancel(&mut self) { - self.cancelled = true; -} -``` - -### Cancellation from the CLI - -Schedule with an explicit idempotency key and cancel the pending invocation: - -```shell -# Schedule with a known idempotency key -golem agent invoke --trigger --schedule-at 2026-03-15T10:30:00Z -i 'poll-next' 'PollerAgent("my-poller")' poll - -# Cancel the pending invocation -golem agent invocation cancel 'PollerAgent("my-poller")' 'poll-next' ``` -## Common Use Cases +Generated durable-agent scheduling methods return +`Result<(), golem_rust::golem_agentic::golem::agent::host::RpcError>`. Never discard +that result: a failed enqueue breaks the recurring chain. For a method with arguments, the +signature is `schedule_(arguments..., scheduled_time)`; `ScheduledTime` is always last. -### Periodic Polling +## Backoff -Check an external API or queue for new work at regular intervals: +Store the consecutive failure count in agent state. Reset it after success; otherwise compute a +capped delay and schedule one next invocation. For example: ```rust -fn poll(&mut self) { - let items = fetch_pending_items(); - for item in items { - process(item); - } - self.schedule_next(60); // poll again in 60s -} +let delay = if succeeded { + self.consecutive_failures = 0; + 60 +} else { + self.consecutive_failures += 1; + (60 * 2u64.pow(self.consecutive_failures.min(6))).min(3600) +}; + +PollerAgentClient::get(self.name.clone()) + .schedule_poll(endpoint, after(Duration::from_secs(delay))) + .expect("failed to schedule retry"); ``` -### Periodic Cleanup +## Cancel the pending tick -Remove expired data or stale resources on a schedule: +`schedule_cancelable_(arguments..., scheduled_time)` returns +`Result` for a durable agent. Store the token in agent state +and call `cancel()` to prevent that scheduled invocation from starting: ```rust -fn cleanup(&mut self) { - self.entries.retain(|e| !e.is_expired()); - self.schedule_next(3600); // run hourly -} -``` +let token = PollerAgentClient::get(self.name.clone()) + .schedule_cancelable_poll(endpoint, after(Duration::from_secs(60))) + .expect("failed to schedule next poll"); +self.pending = Some(token); -### Heartbeat / Keep-Alive - -Periodically notify an external service that the agent is alive: - -```rust -fn heartbeat(&mut self) { - send_heartbeat(&self.service_url); - self.schedule_next(30); // every 30s +if let Some(token) = self.pending.take() { + token.cancel(); } ``` -## Helper for Scheduling Self - -Extract the scheduling logic into a helper to keep methods clean: - -```rust -impl PollerAgentImpl { - fn schedule_next(&self, delay_secs: u64) { - let mut client = PollerAgentClient::get(self.name.clone()); - let now_secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - client.schedule_poll(Datetime { - seconds: now_secs + delay_secs, - nanoseconds: 0, - }); - } -} -``` - -## Key Points - -- The agent is durable — if it crashes, the pending scheduled invocation still fires and the agent recovers -- Invocations are sequential — no concurrent executions of `poll` on the same agent -- Each `schedule_` call is a fire-and-forget enqueue; the current invocation completes immediately -- Use a state flag or generation counter to stop the loop gracefully -- Keep the scheduled method idempotent — it may be retried on recovery - -## Recovery & Oplog Growth - -Each scheduled tick (heartbeat, poll, cleanup) appends entries to the agent's oplog. For long-running or high-frequency recurring tasks, the oplog grows unboundedly, and recovery on crash will replay the full history — which becomes slow over time. +A state flag is still useful because a tick may already have started when cancellation races with +delivery. Keep only one pending tick unless overlapping schedules are intentional. -**You cannot opt out of oplog writes for a durable agent.** The fix is **snapshot-based recovery**: enable periodic snapshotting so recovery starts from the latest snapshot instead of replaying every prior tick. See [`golem-custom-snapshot-rust`](/next/how-to-guides/rust/golem-custom-snapshot-rust) for the `snapshotting = "every(N)"` / `snapshotting = "periodic(...)"` attribute and serde-based or custom save/load implementations. +Recurring invocations grow the oplog. For long-lived or frequent loops, configure periodic +snapshots; do not try to bypass durability. Load [`golem-custom-snapshot-rust`](/next/how-to-guides/rust/golem-custom-snapshot-rust) for that workflow. diff --git a/docs/src/content/next/how-to-guides/rust/golem-schedule-future-call-rust.mdx b/docs/src/content/next/how-to-guides/rust/golem-schedule-future-call-rust.mdx index 5f84ff7667..f11795f4f6 100644 --- a/docs/src/content/next/how-to-guides/rust/golem-schedule-future-call-rust.mdx +++ b/docs/src/content/next/how-to-guides/rust/golem-schedule-future-call-rust.mdx @@ -1,53 +1,42 @@ -# Scheduling a Future Agent Invocation (Rust) +# Schedule a future agent invocation (Rust) -## Overview - -A **scheduled invocation** enqueues a method call on the target agent to be executed at a specific future time. The call returns immediately; the target agent processes it when the scheduled time arrives. - -## Usage - -Every method on the generated `Client` has a corresponding `schedule_` variant that takes a `Datetime` as the first argument: +Generated agent clients expose `schedule_` and `schedule_cancelable_`. Pass the +method arguments first and a `golem_rust::ScheduledTime` last: ```rust -use golem_rust::wasip2::clocks::wall_clock::Datetime; - -let mut counter = CounterAgentClient::get("my-counter".to_string()); +use golem_rust::ScheduledTime; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +let at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time went backwards") + + Duration::from_secs(60); +let scheduled_time = ScheduledTime { + seconds: at.as_secs() as i64, + nanoseconds: at.subsec_nanos(), +}; -// Schedule increment to run 60 seconds from now -let now_secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - -counter.schedule_increment(Datetime { - seconds: now_secs + 60, - nanoseconds: 0, -}); - -// Schedule with arguments let reporter = ReportAgentClient::get("daily".to_string()); -reporter.schedule_generate_report( - "summary".to_string(), - Datetime { seconds: tomorrow_midnight, nanoseconds: 0 } -); +reporter + .schedule_generate_report("summary".to_string(), scheduled_time) + .expect("failed to schedule report"); ``` -## Datetime Type +`ScheduledTime` is an absolute Unix timestamp with signed seconds and nanosecond precision. Do not +use the removed `wasip2::clocks::wall_clock::Datetime` path. -The `Datetime` struct represents a point in time as seconds + nanoseconds since the Unix epoch: +For durable agents, `schedule_` returns `Result<(), RpcError>`, where `RpcError` is +`golem_rust::golem_agentic::golem::agent::host::RpcError`. The cancelable +variant returns a cancellation token: ```rust -use golem_rust::wasip2::clocks::wall_clock::Datetime; +let token = reporter + .schedule_cancelable_generate_report("summary".to_string(), scheduled_time) + .expect("failed to schedule report"); -Datetime { - seconds: 1700000000, // Unix timestamp in seconds - nanoseconds: 0, // Sub-second precision -} +// Cancel before the invocation starts. +token.cancel(); ``` -## Use Cases - -- **Periodic tasks**: Schedule the next run at the end of each invocation -- **Delayed processing**: Process an order after a cooling-off period -- **Reminders and notifications**: Send a reminder at a specific time -- **Retry with backoff**: Schedule a retry after a delay on failure +Scheduling is fire-and-forget: it confirms that the invocation was enqueued, not that the future +method succeeded. Keep the target method idempotent when retries or recovery may repeat effects. diff --git a/docs/src/content/next/how-to-guides/rust/golem-tools-middleware-rust.mdx b/docs/src/content/next/how-to-guides/rust/golem-tools-middleware-rust.mdx new file mode 100644 index 0000000000..99d88f63e8 --- /dev/null +++ b/docs/src/content/next/how-to-guides/rust/golem-tools-middleware-rust.mdx @@ -0,0 +1,31 @@ +# Tool middleware in Rust + +Annotate an implementation of a generated middleware trait with `#[tool_middleware]`. A transparent +middleware presents and wraps the same tool: + +```rust +use golem_rust::{tool::ToolInvokeError, tool_middleware}; + +struct EchoPolicy; +impl EchoPolicy { fn new() -> Self { Self } } + +#[tool_middleware(name = "echo-policy", constructor = EchoPolicy::new)] +impl EchoMiddleware for EchoPolicy { + async fn echo( + &self, + underlying: &EchoUnderlying, + value: String, + ) -> Result> { + if value.is_empty() { + Err(ToolInvokeError::ConstraintViolation("value is empty".into())) + } else { + underlying.echo(value).await + } + } +} +``` + +For an adapter, implement `PresentedMiddleware` and translate inputs, outputs, +and custom errors explicitly. The supplied underlying handle is invocation-scoped: do not store it, +return it, or use it after the middleware method finishes. Forward affine permission cards and +streams exactly once. diff --git a/docs/src/content/next/how-to-guides/scala.mdx b/docs/src/content/next/how-to-guides/scala.mdx index 5d0b8c982b..aa7f67b92e 100644 --- a/docs/src/content/next/how-to-guides/scala.mdx +++ b/docs/src/content/next/how-to-guides/scala.mdx @@ -15,6 +15,7 @@ Guides specific to developing Golem agents in Scala. + @@ -23,6 +24,7 @@ Guides specific to developing Golem agents in Scala. + @@ -34,12 +36,14 @@ Guides specific to developing Golem agents in Scala. + + diff --git a/docs/src/content/next/how-to-guides/scala/_meta.js b/docs/src/content/next/how-to-guides/scala/_meta.js index d95f9bf9ed..8aed4adc69 100644 --- a/docs/src/content/next/how-to-guides/scala/_meta.js +++ b/docs/src/content/next/how-to-guides/scala/_meta.js @@ -9,6 +9,7 @@ export default { "golem-agent-reflection-scala": "Agent Reflection and Client Approaches (Scala)", "golem-annotate-agent-scala": "Annotating Agent Methods (Scala)", "golem-atomic-block-scala": "Atomic Blocks and Durability Controls (Scala)", + "golem-call-tool-scala": "Call a Golem tool from Scala", "golem-call-from-external-scala": "Calling Agents from External Applications (Scala)", "golem-call-another-agent-scala": "Calling Another Agent (Scala)", "golem-configure-durability-scala": "Configuring Agent Durability (Scala)", @@ -17,6 +18,7 @@ export default { "golem-create-agent-instance-scala": "Creating a Golem Agent Instance with `golem agent new`", "golem-stateless-agent-scala": "Creating Ephemeral (Stateless) Agents (Scala)", "golem-custom-snapshot-scala": "Custom Snapshots in Scala", + "golem-define-tool-scala": "Define a Golem tool in Scala", "golem-add-http-auth-scala": "Enabling Authentication on Scala HTTP Endpoints", "golem-enable-otlp-scala": "Enabling OpenTelemetry for a Scala Agent", "golem-file-io-scala": "File I/O in Scala Golem Agents", @@ -28,12 +30,14 @@ export default { "golem-make-http-request-scala": "Making Outgoing HTTP Requests (Scala)", "golem-mark-read-only-scala": "Marking Agent Methods as Read-Only (Scala)", "golem-parallel-workers-scala": "Parallel Workers — Fan-Out / Fan-In (Scala)", + "golem-permission-card-scala": "Permission cards in Scala", "golem-multi-instance-agent-scala": "Phantom Agents in Scala", "golem-recurring-task-scala": "Recurring Tasks via Self-Scheduling (Scala)", "golem-add-transactions-scala": "Saga-Pattern Transactions (Scala)", "golem-schedule-agent-scala": "Scheduling a Future Agent Invocation", "golem-schedule-future-call-scala": "Scheduling a Future Agent Invocation (Scala)", "golem-streaming-agent-scala": "Streaming Agent Methods in Scala", + "golem-tools-middleware-scala": "Tool middleware in Scala", "golem-trigger-agent-scala": "Triggering a Fire-and-Forget Agent Invocation", "golem-add-ignite-scala": "Using Apache Ignite from a Scala Agent", "golem-add-mysql-scala": "Using MySQL from a Scala Agent", diff --git a/docs/src/content/next/how-to-guides/scala/golem-add-http-endpoint-scala.mdx b/docs/src/content/next/how-to-guides/scala/golem-add-http-endpoint-scala.mdx index 89fa1e6f8c..d503a8fc17 100644 --- a/docs/src/content/next/how-to-guides/scala/golem-add-http-endpoint-scala.mdx +++ b/docs/src/content/next/how-to-guides/scala/golem-add-http-endpoint-scala.mdx @@ -176,7 +176,10 @@ Scala `Either[E, T]` is mapped to the WIT `result` type, with `Right` trea | `Future[Either[E, T]]` | 200 OK if `Right`, 500 Internal Server Error if `Left` | JSON `T` or JSON `E` | | `Future[Either[E, Unit]]` | 204 No Content if `Right`, 500 if `Left` | empty or JSON `E` | | `Future[Either[Unit, T]]` | 200 OK if `Right`, 500 if `Left` | JSON `T` or empty | -| `Future[UnstructuredBinary]` | 200 OK | Raw binary with Content-Type | + +The current Scala SDK does not expose `UnstructuredText` or `UnstructuredBinary`. Use ordinary +schema-backed values, which are JSON encoded, or choose an SDK that supports rich raw-body types +when an endpoint must return plain text or arbitrary binary bytes. ## Complete Example diff --git a/docs/src/content/next/how-to-guides/scala/golem-call-tool-scala.mdx b/docs/src/content/next/how-to-guides/scala/golem-call-tool-scala.mdx new file mode 100644 index 0000000000..5158bbce1f --- /dev/null +++ b/docs/src/content/next/how-to-guides/scala/golem-call-tool-scala.mdx @@ -0,0 +1,19 @@ +# Call a Golem tool from Scala + +`@toolDefinition` generates `Client`. Construct it with the generated factory and call the +typed asynchronous methods: + +```scala +import golem.tool.ToolError +import scala.concurrent.Future + +val client: EchoClient = EchoClient() +val result: Future[Either[ToolError[Nothing], String]] = + client.echo("hello") +``` + +Use the exact generated signature: a command with declared errors uses the error type declared by +the tool method instead of `Nothing`. Commands without stdout, including stdin-only commands, +return `Future[Either[ToolError[E], A]]`; stdin is a `ToolInputStream` parameter. A command with +stdout returns `Either[ToolError[E], ToolInvocation[E, A]]`, whose invocation exposes the result and +stream. Handle `ToolError.Tool` separately from protocol failures. Never edit generated clients. diff --git a/docs/src/content/next/how-to-guides/scala/golem-define-tool-scala.mdx b/docs/src/content/next/how-to-guides/scala/golem-define-tool-scala.mdx new file mode 100644 index 0000000000..e50c7e6693 --- /dev/null +++ b/docs/src/content/next/how-to-guides/scala/golem-define-tool-scala.mdx @@ -0,0 +1,23 @@ +# Define a Golem tool in Scala + +Annotate the definition trait and implementation class. The build plugin generates registration, +metadata, and typed client sources: + +```scala +import golem.runtime.annotations.{toolDefinition, toolImplementation} + +@toolDefinition(version = "1.0.0") +trait Echo { + def echo(value: String): String +} + +@toolImplementation() +final class EchoImpl extends Echo { + override def echo(value: String): String = value +} +``` + +Use `@arg`, `@command`, `@constraint`, `@result`, and `@annotations` when the command-line surface +needs explicit metadata. Custom values require `zio.blocks.schema.Schema`. Keep +`scalacOptions += "-experimental"` enabled for macro annotations and never edit generated client +or registration sources. diff --git a/docs/src/content/next/how-to-guides/scala/golem-permission-card-scala.mdx b/docs/src/content/next/how-to-guides/scala/golem-permission-card-scala.mdx new file mode 100644 index 0000000000..fc86733a75 --- /dev/null +++ b/docs/src/content/next/how-to-guides/scala/golem-permission-card-scala.mdx @@ -0,0 +1,16 @@ +# Permission cards in Scala + +The carrier is `golem.schema.GuestPermissionCardHandle`. Because the handle is opaque, choose its +schema explicitly with `GuestPermissionCardHandle.intoSchema(PermissionCardSpec(...))`: + +```scala +import golem.schema.{GuestPermissionCardHandle, IntoSchema, PermissionCardSpec} + +given IntoSchema[GuestPermissionCardHandle] = + GuestPermissionCardHandle.intoSchema(PermissionCardSpec(polymorphic = false)) +``` + +Use the handle in an agent or tool input/output only where that `IntoSchema` is in scope. Decoding +uses the SDK's provided `FromSchema`. Guest code cannot construct or re-wrap a raw card through the +public Scala API; it receives one from the runtime or another call. Encoding transfers ownership, +so never serialize, persist, duplicate, or reuse a transferred handle. diff --git a/docs/src/content/next/how-to-guides/scala/golem-tools-middleware-scala.mdx b/docs/src/content/next/how-to-guides/scala/golem-tools-middleware-scala.mdx new file mode 100644 index 0000000000..e7cc67a8f0 --- /dev/null +++ b/docs/src/content/next/how-to-guides/scala/golem-tools-middleware-scala.mdx @@ -0,0 +1,25 @@ +# Tool middleware in Scala + +The build plugin generates `Middleware` and `Underlying` from each tool definition. +Extend the generated middleware trait and annotate the concrete no-argument class. The example +assumes the `Echo` definition from [`golem-define-tool-scala`](/next/how-to-guides/scala/golem-define-tool-scala). + +```scala +import golem.runtime.annotations.toolMiddleware +import golem.tool.ToolInvokeError +import scala.concurrent.Future + +@toolMiddleware(name = "echo-policy") +final class EchoPolicy extends EchoMiddleware { + def echo( + underlying: EchoUnderlying, + value: String + ): Future[Either[ToolInvokeError[Nothing], String]] = + underlying.echo(value).toMiddlewareResult +} +``` + +For adapters, extend `PresentedMiddleware.Adapter[ExpectedUnderlying]` and translate values and +custom errors. Use `@universalToolMiddleware` with `UniversalToolMiddleware` only for arbitrary tool +metadata and raw `TypedSchemaValue` carriers. The underlying is invocation-scoped; never store or +return it. Forward each stream or permission card at most once. diff --git a/docs/src/content/next/how-to-guides/ts.mdx b/docs/src/content/next/how-to-guides/ts.mdx index 0c21d27eaa..4f327cbd75 100644 --- a/docs/src/content/next/how-to-guides/ts.mdx +++ b/docs/src/content/next/how-to-guides/ts.mdx @@ -14,6 +14,7 @@ Guides specific to developing Golem agents in TypeScript. + @@ -23,6 +24,7 @@ Guides specific to developing Golem agents in TypeScript. + @@ -34,12 +36,14 @@ Guides specific to developing Golem agents in TypeScript. + + diff --git a/docs/src/content/next/how-to-guides/ts/_meta.js b/docs/src/content/next/how-to-guides/ts/_meta.js index a85b493e89..b173bf9a92 100644 --- a/docs/src/content/next/how-to-guides/ts/_meta.js +++ b/docs/src/content/next/how-to-guides/ts/_meta.js @@ -8,6 +8,7 @@ export default { "golem-add-config-ts": "Adding Typed Configuration to a TypeScript Agent", "golem-annotate-agent-ts": "Annotating Agents and Methods (TypeScript)", "golem-atomic-block-ts": "Atomic Blocks and Durability Controls (TypeScript)", + "golem-call-tool-ts": "Call a Golem tool from TypeScript", "golem-call-from-external-ts": "Calling Agents from External TypeScript Applications", "golem-agent-reflection-ts": "Calling Agents with Runtime Reflection (TypeScript)", "golem-call-another-agent-ts": "Calling Another Agent (TypeScript)", @@ -17,6 +18,7 @@ export default { "golem-create-agent-instance-ts": "Creating a Golem Agent Instance with `golem agent new`", "golem-stateless-agent-ts": "Creating Ephemeral (Stateless) Agents (TypeScript)", "golem-custom-snapshot-ts": "Custom Snapshots in TypeScript", + "golem-define-tool-ts": "Define a Golem tool in TypeScript", "golem-add-http-auth-ts": "Enabling Authentication on TypeScript HTTP Endpoints", "golem-enable-otlp-ts": "Enabling OpenTelemetry for a TypeScript Agent", "golem-file-io-ts": "File I/O in TypeScript Golem Agents", @@ -28,12 +30,14 @@ export default { "golem-make-http-request-ts": "Making Outgoing HTTP Requests (TypeScript)", "golem-mark-read-only-ts": "Marking Agent Methods as Read-Only (TypeScript)", "golem-parallel-workers-ts": "Parallel Workers — Fan-Out / Fan-In (TypeScript)", + "golem-permission-card-ts": "Permission cards in TypeScript", "golem-multi-instance-agent-ts": "Phantom Agents in TypeScript", "golem-recurring-task-ts": "Recurring Tasks via Self-Scheduling (TypeScript)", "golem-add-transactions-ts": "Saga-Pattern Transactions (TypeScript)", "golem-schedule-agent-ts": "Scheduling a Future Agent Invocation", "golem-schedule-future-call-ts": "Scheduling a Future Agent Invocation (TypeScript)", "golem-streaming-agent-ts": "Streaming Agent Methods in TypeScript", + "golem-tools-middleware-ts": "Tool middleware in TypeScript", "golem-trigger-agent-ts": "Triggering a Fire-and-Forget Agent Invocation", "golem-add-ignite-ts": "Using Apache Ignite from a TypeScript Agent", "golem-add-mysql-ts": "Using MySQL from a TypeScript Agent", diff --git a/docs/src/content/next/how-to-guides/ts/golem-call-tool-ts.mdx b/docs/src/content/next/how-to-guides/ts/golem-call-tool-ts.mdx new file mode 100644 index 0000000000..5465d48ce0 --- /dev/null +++ b/docs/src/content/next/how-to-guides/ts/golem-call-tool-ts.mdx @@ -0,0 +1,29 @@ +# Call a Golem tool from TypeScript + +Bind a client from the same definition, or use `toolClientDefinition` for a caller-owned subset: + +```typescript +import { ToolCallError, toolClientDefinition, toolDefinition } from '@golemcloud/golem-ts-sdk'; +import { z } from 'zod/v4'; + +const echo = toolDefinition('echo').body((body) => + body.positional('value', z.string()).returns(z.string()), +); +const client = toolClientDefinition(echo).client('echo'); + +try { + const value = await client.echo({ value: 'hello' }); + console.log(value); +} catch (error) { + if (error instanceof ToolCallError && error.cause.tag === 'tool') { + console.error(error.cause.error); + } else { + throw error; + } +} +``` + +Commands without declared stdout return a Promise of their result. Commands with required or +optional stdout return a `StartedToolInvocation`; consume its `stdout` and await its `result`, or +use `.collect()` to collect both. `ToolCallError.cause.tag` is `tool`, `rpc`, or `unknown-error`. +Protocol errors use `rpc` with `cause.error.tag === 'protocol-error'`. diff --git a/docs/src/content/next/how-to-guides/ts/golem-define-tool-ts.mdx b/docs/src/content/next/how-to-guides/ts/golem-define-tool-ts.mdx new file mode 100644 index 0000000000..135dc8c26a --- /dev/null +++ b/docs/src/content/next/how-to-guides/ts/golem-define-tool-ts.mdx @@ -0,0 +1,23 @@ +# Define a Golem tool in TypeScript + +Build a definition with `toolDefinition(...).body(...)`, then register one implementation object: + +```typescript +import { ok, toolDefinition } from '@golemcloud/golem-ts-sdk'; +import { z } from 'zod/v4'; + +export const echo = toolDefinition('echo') + .version('1.0.0') + .body((body) => + body.positional('value', z.string()).returns(z.string()), + ); + +echo.implement({ + echo: async ({ value }) => ok(value), +}); +``` + +The implementation key is the generated command name. Use the body builder for positional, +option, flag, tail, stdin/stdout, result, and declared-error metadata; use Standard Schema values +such as Zod schemas for typed data. Provider handlers return `ok(result)` or a declared `err(...)`. +Call `.implement(...)` only once per definition. diff --git a/docs/src/content/next/how-to-guides/ts/golem-permission-card-ts.mdx b/docs/src/content/next/how-to-guides/ts/golem-permission-card-ts.mdx new file mode 100644 index 0000000000..ee4c011c7a --- /dev/null +++ b/docs/src/content/next/how-to-guides/ts/golem-permission-card-ts.mdx @@ -0,0 +1,19 @@ +# Permission cards in TypeScript + +Declare the capability with `s.permissionCard({ polymorphic })`: + +```typescript +import { s, toolDefinition } from '@golemcloud/golem-ts-sdk'; + +const delegate = toolDefinition('delegate').body((body) => + body + .positional('card', s.permissionCard({ polymorphic: false })) + .returns(s.permissionCard({ polymorphic: false })), +); +``` + +The runtime value is an opaque raw permission-card resource received from a host or another call. +The SDK does not expose a public constructor for forging one. Successful encoding transfers +ownership before the call completes. Never reuse a transferred handle, even if the invocation +subsequently fails. Do not inspect, stringify, persist, or duplicate permission-card values. Set +`polymorphic: true` only when the card schema may contain owner or resource-id slots. diff --git a/docs/src/content/next/how-to-guides/ts/golem-tools-middleware-ts.mdx b/docs/src/content/next/how-to-guides/ts/golem-tools-middleware-ts.mdx new file mode 100644 index 0000000000..0e4fc97ae5 --- /dev/null +++ b/docs/src/content/next/how-to-guides/ts/golem-tools-middleware-ts.mdx @@ -0,0 +1,36 @@ +# Tool middleware in TypeScript + +Call `.middleware(...)` on the presented definition. Omit `wraps` for transparent middleware; +provide another definition in `wraps` for an adapter: + +```typescript +import { ToolInvokeError, toolDefinition } from '@golemcloud/golem-ts-sdk'; +import { z } from 'zod/v4'; + +const echo = toolDefinition('echo').body((body) => + body.positional('value', z.string()).returns(z.string()), +); + +echo.middleware({ + name: 'echo-policy', + implementation: { + echo: async ({ value }, { underlying }) => { + if (value.length === 0) { + throw new ToolInvokeError({ + tag: 'constraint-violation', + val: 'value is empty', + }); + } + return underlying.echo({ value }); + }, + }, +}); +``` + +Use `universalToolMiddleware(...)` only when middleware must inspect arbitrary tool metadata and +raw typed values. Prefer typed middleware whenever the surface is known. The exact handler context +and result shape follows the command's arguments, declared errors, and streams; let TypeScript +infer it rather than casting. Use `underlying` only during the middleware handler; it may make +multiple calls before the handler settles. Do not retain it for later invocations. Do not reuse +transferred streams or permission-card handles. Returned stdout may be consumed lazily after the +handler returns. diff --git a/docs/src/content/next/invoke.mdx b/docs/src/content/next/invoke.mdx index af538cf6c6..0da9dc5778 100644 --- a/docs/src/content/next/invoke.mdx +++ b/docs/src/content/next/invoke.mdx @@ -6,3 +6,4 @@ Learn how to invoke [workers](/next/concepts/agents) using: - The [CLI](invoke/cli) - The [REPL](invoke/repl) - By mapping to a [custom API](invoke/making-custom-apis) +- With [HTTP routers and file mappings](/next/invoke/http-routers) diff --git a/docs/src/content/next/invoke/_meta.js b/docs/src/content/next/invoke/_meta.js index 9e6df511f6..89b19749ff 100644 --- a/docs/src/content/next/invoke/_meta.js +++ b/docs/src/content/next/invoke/_meta.js @@ -3,6 +3,7 @@ export default { cli: "CLI", repl: "REPL", "making-custom-apis": "Making Custom APIs", + "http-routers": "Using HTTP Routers and Files", mcp: "MCP", "bridge-libraries": "Bridge Libraries", "stream-session-public-protocol-v1": "Streaming Protocol v1", diff --git a/docs/src/content/next/invoke/bridge-libraries.mdx b/docs/src/content/next/invoke/bridge-libraries.mdx index 50adad2a39..df651d0657 100644 --- a/docs/src/content/next/invoke/bridge-libraries.mdx +++ b/docs/src/content/next/invoke/bridge-libraries.mdx @@ -2,7 +2,7 @@ import { Callout, Tabs } from "nextra/components" # Bridge Libraries -Bridge libraries let you invoke Golem agents from **non-Golem applications** in a type-safe way. Golem generates self-contained Rust crates, Node.js TypeScript npm packages, Scala sbt projects, and MoonBit modules that implement a fully typed client for specific agents. +Bridge libraries let you invoke Golem agents from **non-Golem applications** in a type-safe way. Golem generates self-contained Rust crates, Node.js TypeScript and Effect TypeScript npm packages, Scala sbt projects, and MoonBit modules that implement a fully typed client for specific agents. Methods without streams use the existing REST invocation API and remain compatible with previously generated clients. A method whose input or output contains a stream instead opens the public streaming WebSocket endpoint described below. @@ -17,6 +17,9 @@ bridge: agents: - InboxAgent - EscalationAgent + effect: + external: + agents: WorkflowAgent rust: external: agents: AuditAgent @@ -28,13 +31,13 @@ bridge: agents: ReportAgent ``` -This will generate a TypeScript package for `InboxAgent` and `EscalationAgent`, a Rust crate for `AuditAgent`, a Scala sbt project for `BillingAgent`, and a MoonBit module for `ReportAgent`. +This will generate TypeScript packages for `InboxAgent` and `EscalationAgent`, an Effect-native TypeScript package for `WorkflowAgent`, a Rust crate for `AuditAgent`, a Scala sbt project for `BillingAgent`, and a MoonBit module for `ReportAgent`. ## Using Bridge Libraries Once generated, you can use the bridge libraries like any other typed client. First configure the connection, then interact with agents: - + ```typescript import { CounterAgent, configure } from 'counter-agent-client/counter-agent-client.js' @@ -50,6 +53,25 @@ const value = await c1.increment() ``` +```typescript +import { Effect } from "effect" +import { CounterAgent, configure } from "counter-agent-client/counter-agent-client.js" + +configure({ + server: { type: "local" }, + application: "bridgetest", + environment: "local", +}) + +const program = Effect.gen(function* () { + const c1 = yield* CounterAgent.get("c1") + return yield* c1.increment() +}) + +const value = await Effect.runPromise(program) +``` + + ```rust use counter_agent_client::{configure, CounterAgent, GolemServer}; @@ -90,7 +112,7 @@ async fn main { Generated clients preserve streams wherever they occur in a method's typed input or output, including nested values and multiple sibling streams. The examples below assume a `MediaAgent` with input-only `upload`, output-only `transcribe`, and mixed `transform` methods. The generated method names and parameter order follow the agent definition; there is no separate streaming method suffix. - + ```rust use futures_util::{stream, StreamExt}; @@ -156,6 +178,36 @@ for await (const item of output) { +```typescript +import { Effect, Stream } from "effect" +import { MediaAgent, configure } from "media-agent-client/media-agent-client.js" + +configure({ server: { type: "local" }, application: "media-app", environment: "prod" }) + +const program = Effect.scoped( + Effect.gen(function* () { + const agent = yield* MediaAgent.get("studio-a") + + yield* agent.upload(Stream.make(1, 2)) + + const words = yield* agent.transcribe() + yield* Stream.runForEach(words, (word) => Effect.logInfo(word)) + + const output = yield* agent.transform(Stream.make(10)) + return yield* Stream.runHead(output) + }), +) + +const first = await Effect.runPromise(program) +``` + +Effect bridges expose nested streams as native `Stream.Stream` values. Streaming calls require a `Scope`; `Effect.scoped` closes it and propagates interruption to the underlying invocation. + + + Effect external bridges use the same Node.js WebSocket transport as TypeScript bridges. Browser streaming is not supported. + + + ```scala import golem.bridge.client.media_agent.MediaAgentClient import golem.bridge.runtime.{AgentStream, AgentStreamStep, GolemServer} @@ -251,7 +303,7 @@ Sec-WebSocket-Protocol: golem.agent-invocation.v1 Authorization: Bearer ``` -The URL uses `ws:` for an HTTP server and `wss:` for HTTPS. `Local` and `Cloud` select the standard server and credentials; a custom server configuration supplies its own base URL and bearer token. For example, TypeScript uses `server: { type: "custom", url: "https://golem.example.com", token }`; the corresponding variants are `GolemServer::Custom { url: reqwest::Url, token }` in Rust, `GolemServer.Custom(url, token)` in Scala, and `@runtime.Custom(url, token)` in MoonBit. Authentication and authorization are checked both when starting and when reattaching to a session. The generated runtime negotiates the required `golem.agent-invocation.v1` subprotocol automatically; applications should not open this socket directly. +The URL uses `ws:` for an HTTP server and `wss:` for HTTPS. `Local` and `Cloud` select the standard server and credentials; a custom server configuration supplies its own base URL and bearer token. TypeScript and Effect TypeScript use `server: { type: "custom", url: "https://golem.example.com", token }`; the corresponding variants are `GolemServer::Custom { url: reqwest::Url, token }` in Rust, `GolemServer.Custom(url, token)` in Scala, and `@runtime.Custom(url, token)` in MoonBit. Authentication and authorization are checked both when starting and when reattaching to a session. The generated runtime negotiates the required `golem.agent-invocation.v1` subprotocol automatically; applications should not open this socket directly. The [Stream Session Public Protocol v1](/next/invoke/stream-session-public-protocol-v1) reference specifies every JSON message, binary envelope, stable error code, limit, token, and resume rule. @@ -315,7 +367,7 @@ This feature is the generated external bridge and CLI **invocation-session WebSo ## Configuration Options -The TypeScript, Rust, Scala and MoonBit bridge libraries require three configuration parameters: +The TypeScript, Effect TypeScript, Rust, Scala and MoonBit bridge libraries require three configuration parameters: | Parameter | Description | |-----------|-------------| diff --git a/docs/src/content/next/invoke/http-routers.mdx b/docs/src/content/next/invoke/http-routers.mdx new file mode 100644 index 0000000000..2b59773056 --- /dev/null +++ b/docs/src/content/next/invoke/http-routers.mdx @@ -0,0 +1,972 @@ +import { Callout, Tabs } from "nextra/components" + +# Using HTTP routers and files + +Use an HTTP router to handle streaming requests, serve static assets, or integrate +an HTTP framework. To publish files created by a durable agent, add live file +mappings to that agent instead. The [HTTP router concepts](/next/concepts/http-routers) +page explains how these features interact with code-first endpoints, authentication, +and durable execution. + +The examples below belong in an existing [Golem application](/next/quickstart). +Use a CLI and SDK from the same Golem version. Commands assume a local server on +your machine, with the application gateway on port 9006. + +## Handling streaming requests + +Define a router with a unique type name and a literal mount path. This `EchoRouter` +streams an upload back to the caller at `/echo`. +It does not collect the upload into memory or calculate its length first. + + + +```typescript +import { defineHttpRouter } from '@golemcloud/golem-ts-sdk'; + +export const EchoRouter = defineHttpRouter('EchoRouter') + .mount('/echo') + .implement((request) => new Response(request.body)); +``` + +The Web adapter accepts standard `Request` and `Response` objects, so a framework +with a Fetch-compatible handler can supply the function passed to `.implement`. +Keep its body streaming. For the original, unnormalized HTTP envelope, use +`.implementRaw` instead; see [Working with headers](#working-with-headers). + + +```typescript +import { Effect } from "effect" +import { + HttpRouter as Routes, + HttpServerRequest, + HttpServerResponse, +} from "effect/unstable/http" +import { Http, HttpRouter } from "@golemcloud/effect-golem" + +const routes = Routes.add("POST", "/", Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest + return HttpServerResponse.stream(request.stream) +})) + +HttpRouter.define("EchoRouter", { + mount: Http.mount("/echo"), +}).implement(Routes.toHttpEffect(routes)) +``` + +Use Effect's `HttpRouter` (aliased here as `Routes`) to compose routes. It sees +mount-relative URLs: `POST /` above handles public `POST /echo`. Inside the handler, +`yield* HttpRouter.request` gives the original full path, query, byte-valued headers, +and the same single-consumer body. Do not consume both body views. + +Acquire request resources with `Effect.acquireRelease` or `Effect.addFinalizer`. +The request scope stays open through response consumption; do not rely solely on +finalizers inside a body stream that might never start. See the +[Effect adapter lifecycle](/next/concepts/http-routers#effect-adapter-lifecycle-and-limitations) +for cancellation limits. + + +```rust +use golem_rust::agentic::{Config, HttpRequest, HttpResponse, HttpRouter}; +use golem_rust::http_router; + +struct EchoRouter; + +#[http_router(name = "EchoRouter", mount = "/echo")] +impl HttpRouter for EchoRouter { + type Config = (); + + fn new(_: Config<()>) -> Self { + Self + } + + async fn handle(&self, request: HttpRequest) -> HttpResponse { + HttpResponse { + status: 200, + headers: vec![], + body: request.body, + } + } +} +``` + + +```scala +import golem.{BaseAgent, UShort} +import golem.runtime.annotations.* +import golem.runtime.http.* +import scala.concurrent.Future + +@httpRouter(typeName = "EchoRouter", mount = "/echo") +trait EchoRouter extends BaseAgent { + @httpHandler def serve(request: HttpRequest): Future[HttpResponse] +} + +@agentImplementation() +final class EchoRouterImpl extends EchoRouter { + def serve(request: HttpRequest): Future[HttpResponse] = + Future.successful(HttpResponse(UShort(200), Nil, request.body)) +} +``` + + +```moonbit +///| +#derive.http_router +#derive.mount("/echo") +struct EchoRouter {} + +///| +fn EchoRouter::new() -> EchoRouter { EchoRouter::{ } } + +///| +#derive.http_handler +pub fn EchoRouter::serve( + _self : Self, + request : @http.HttpRequest, +) -> @http.HttpResponse { + let input = request.body + { + status: 200, + headers: [], + body: @schema.AgentStream::produce( + async fn(writer) { + defer input.drop() + for ;; { + match input.read() { + None => return + Some(chunk) => + if writer.write_one(chunk) is @schema.PeerDropped { return } + } + } + }, + on_unstarted_drop=() => input.drop(), + ), + } +} +``` + +Import `golemcloud/golem_sdk/http` and `golemcloud/golem_sdk/schema` in the +component's `moon.pkg`. Build through `golem build` so it generates registration +and schema code before compiling. + + + +Merge this deployment into your application's manifest: + +```yaml filename="golem.yaml" +httpApi: + deployments: + local: + - domain: localhost:9006 + scheme: http + agents: + EchoRouter: {} +``` + +Build, deploy, and send an upload: + +```shell +golem build -L +golem deploy -L -Y +printf 'stream me' | curl --data-binary @- http://localhost:9006/echo +``` + +The response is `stream me`. For a router with several routes, dispatch on the +request's method and path, and return 404 for unknown routes. The canonical request +uses the **full public path**; framework adapters may present mount-relative paths +as noted in their tabs. [Routing precedence](/next/concepts/http-routers#request-routing-precedence) +explains when a code-first endpoint or file mapping takes priority over the handler. + +### Working with headers + +Keep repeated headers separate, especially `Set-Cookie`. The raw APIs use lowercase +names and byte-valued fields. For example, add two cookies to a streaming response: + + + +The Web adapter already preserves separate `Set-Cookie` fields. For byte-valued +or strictly ordered fields, import `withRawHeaders` from `@golemcloud/golem-ts-sdk` +and replace the handler with: + +```typescript +.implement((request) => withRawHeaders(new Response(request.body), [ + { name: 'set-cookie', value: new TextEncoder().encode('first=one') }, + { name: 'set-cookie', value: new TextEncoder().encode('second=two') }, +])); +``` + +`withRawHeaders` replaces **all** response headers with this list, rather than +adding to `Response.headers`. Include every header you want emitted. + +For canonical methods, paths, queries, and headers without Web API normalization, +replace `.implement(...)` with a raw handler: + +```typescript +.implementRaw((request) => ({ + status: 200, + headers: [ + { name: 'set-cookie', value: new TextEncoder().encode('first=one') }, + { name: 'set-cookie', value: new TextEncoder().encode('second=two') }, + ], + body: request.body, +})); +``` + +A Web handler can also read `context.rawRequest` from its second argument to inspect +the original head. The body still has only one reader. + + +Replace the response returned by the handler with: + +```typescript +return HttpRouter.withRawHeaders(HttpServerResponse.stream(request.stream), [ + { name: "set-cookie", value: new TextEncoder().encode("first=one") }, + { name: "set-cookie", value: new TextEncoder().encode("second=two") }, +]) +``` + +Effect's `withRawHeaders` replaces normalized fields with the **same names** and +preserves unrelated response headers. Apply it after ordinary response combinators: +subsequent cloning can lose the metadata. Use `HttpRouter.request` for original +request field occurrences that normalized Effect headers cannot represent. + + +Import `Header` from `golem_rust::agentic`, then replace the response's `headers`: + +```rust +headers: vec![ + Header { name: "set-cookie".into(), value: b"first=one".to_vec() }, + Header { name: "set-cookie".into(), value: b"second=two".to_vec() }, +], +``` + + +Replace `Nil` in the response with: + +```scala +List( + HttpHeader.ascii("set-cookie", "first=one"), + HttpHeader.ascii("set-cookie", "second=two") +) +``` + +Use `HttpHeader(name, bytes)` for non-ASCII field values. + + +Import `moonbitlang/core/encoding/utf8`, then replace the response's `headers`: + +```moonbit +headers: [ + { name: "set-cookie", value: @utf8.encode("first=one").to_array() }, + { name: "set-cookie", value: @utf8.encode("second=two").to_array() }, +], +``` + + + +Let Golem handle transfer framing. Omit `Content-Length` unless you know the exact +emitted byte count. Release unused input and stop producing when the output consumer +closes. Do not turn stream errors into normal EOF. See the +[streaming contract](/next/concepts/http-routers#streaming-handler-reference) for +bodyless responses and the distinction between EOF and invocation success. + +## Serving static files + +Create a router with file mappings and no handler. This `StaticRouter` serves an +explicit index at `/static` and assets below `/static/assets/`: + + + +```typescript +import { defineHttpRouter } from '@golemcloud/golem-ts-sdk'; + +export const StaticRouter = defineHttpRouter('StaticRouter') + .mount('/static') + .static('/', '/public/index.html') + .static('/assets/*', '/public/assets/$1') + .implement(); +``` + + +```typescript +import { Http, HttpRouter } from "@golemcloud/effect-golem" + +HttpRouter.define("StaticRouter", { + mount: Http.mount("/static"), + static: [ + { route: "/", path: "/public/index.html" }, + { route: "/assets/*", path: "/public/assets/$1" }, + ], +}).register() +``` + +Use `.register()` for a static-only or provider-only router; `.implement(...)` +adds a handler. Static mappings are served by Golem, not an Effect filesystem layer. + + +```rust +use golem_rust::agentic::{Config, HttpRouter}; +use golem_rust::http_router; + +struct StaticRouter; + +#[http_router(name = "StaticRouter", mount = "/static", static_files = [ + ("/", "/public/index.html"), + ("/assets/*", "/public/assets/$1"), +])] +impl HttpRouter for StaticRouter { + type Config = (); + fn new(_: Config<()>) -> Self { Self } +} +``` + + +```scala +import golem.BaseAgent +import golem.runtime.annotations.* + +@httpRouter( + typeName = "StaticRouter", + mount = "/static", + staticBindings = Array( + ("/", "/public/index.html"), + ("/assets/*", "/public/assets/$1") + ) +) +trait StaticRouter extends BaseAgent + +@agentImplementation() +final class StaticRouterImpl extends StaticRouter +``` + + +```moonbit +///| +#derive.http_router +#derive.mount("/static") +#derive.static_file("/", "/public/index.html") +#derive.static_file("/assets/*", "/public/assets/$1") +struct StaticRouter {} + +///| +fn StaticRouter::new() -> StaticRouter { StaticRouter::{ } } +``` + + + +Create `index.html` and `message.txt` beside `golem.yaml`. Provision them as +**read-only** files under the router's type name, and add it to the HTTP deployment: + +```yaml filename="golem.yaml" +agents: + StaticRouter: + files: + - sourcePath: ./index.html + targetPath: /public/index.html + permissions: read-only + - sourcePath: ./message.txt + targetPath: /public/assets/message.txt + permissions: read-only + +httpApi: + deployments: + local: + - domain: localhost:9006 + scheme: http + agents: + EchoRouter: {} + StaticRouter: {} +``` + +Redeploy and check the index, metadata, and a byte range: + +```shell +golem deploy -L -Y +curl http://localhost:9006/static +curl -I http://localhost:9006/static/assets/message.txt +curl -i -H 'Range: bytes=0-3' http://localhost:9006/static/assets/message.txt +``` + +Use a nonempty `message.txt` for the range example. Static responses include a +strong ETag and `Cache-Control: no-cache`. Send the returned ETag in `If-None-Match` +to revalidate; an unchanged asset returns 304. See the +[file HTTP reference](/next/concepts/http-routers#file-http-reference) for HEAD, +preconditions, and range edge cases. + +You can add the same mappings to a router with a handler: matching GET/HEAD files +are served first, and an absent file falls through to the handler. For an override +directory, declare two `/assets/*` mappings, first to `/overrides/$1`, then to +`/public/assets/$1`. Order matters; see +[ordered mappings](/next/concepts/http-routers#ordered-file-mappings). + + + Expose only directories intended for HTTP access. There is no implicit index + lookup, directory listing, or symlink support. Keep secrets outside exposed + roots and protect private mounts with authentication. + + +## Serving live agent files + +Use a normal durable agent when files are created or modified at runtime. Bind its +constructor parameters in the mount and expose only the desired files. This +`FileOwner` creates `/value.txt` during initialization and overwrites it on `update`: + + + +```typescript +import { defineAgent, http, method } from '@golemcloud/golem-ts-sdk'; +import * as fs from 'node:fs'; +import { z } from 'zod'; + +export const FileOwner = defineAgent({ + name: 'FileOwner', + id: { name: z.string() }, + http: http.mount('/files/{name}', { + exposeFiles: [{ route: '/value', path: '/value.txt' }], + }), + methods: { update: method({ input: {}, returns: z.string() }) }, +}); + +export const FileOwnerImpl = FileOwner.implement({ + init({ id }) { + fs.writeFileSync('/value.txt', `initial:${id.name}`); + return {}; + }, + methods: { + update() { + fs.writeFileSync('/value.txt', 'updated'); + return 'updated'; + }, + }, +}); +``` + + +```typescript +import { Effect, Schema } from "effect" +import { defineAgent, Http, method } from "@golemcloud/effect-golem" +import * as fs from "node:fs" + +export const FileOwner = defineAgent({ + name: "FileOwner", + id: { name: Schema.String }, + http: Http.mount("/files/{name}", { + exposeFiles: [{ route: "/value", path: "/value.txt" }], + }), + methods: { update: method({ input: {}, success: Schema.String }) }, +}) + +FileOwner.implement({ + init: ({ name }) => Effect.sync(() => { + fs.writeFileSync("/value.txt", `initial:${name}`) + }), + methods: () => ({ + update: () => Effect.sync(() => { + fs.writeFileSync("/value.txt", "updated") + return "updated" + }), + }), +}) +``` + + +```rust +use golem_rust::{agent_definition, agent_implementation}; + +#[agent_definition(mount = "/files/{name}", filesystem_bindings = [ + ("/value", "/value.txt"), +])] +pub trait FileOwner { + fn new(name: String) -> Self; + fn update(&self) -> String; +} + +struct FileOwnerImpl; + +#[agent_implementation] +impl FileOwner for FileOwnerImpl { + fn new(name: String) -> Self { + std::fs::write("/value.txt", format!("initial:{name}")).unwrap(); + Self + } + + fn update(&self) -> String { + std::fs::write("/value.txt", "updated").unwrap(); + "updated".into() + } +} +``` + + +```scala +import golem.BaseAgent +import golem.runtime.annotations.* +import scala.concurrent.Future +import scala.scalajs.js +import scala.scalajs.js.annotation.JSImport + +@agentDefinition( + mount = "/files/{name}", + exposeFiles = Array(("/value", "/value.txt")) +) +trait FileOwner extends BaseAgent { + class Id(val name: String) + def update(): Future[String] +} + +@agentImplementation() +final class FileOwnerImpl(name: String) extends FileOwner { + FileOwnerFs.writeFileSync("/value.txt", s"initial:$name") + + def update(): Future[String] = { + FileOwnerFs.writeFileSync("/value.txt", "updated") + Future.successful("updated") + } +} + +@js.native +@JSImport("node:fs", JSImport.Namespace) +private object FileOwnerFs extends js.Object { + def writeFileSync(path: String, text: String): Unit = js.native +} +``` + + +```moonbit +///| +#derive.agent +#derive.mount("/files/{name}") +#derive.expose_files("/value", "/value.txt") +struct FileOwner {} + +///| +async fn FileOwner::new(name : String) -> FileOwner { + @fs.write_string(@fs.get_root_dir().unwrap(), "value.txt", "initial:" + name).unwrap() + FileOwner::{ } +} + +///| +pub async fn FileOwner::update(_self : Self) -> String { + @fs.write_string(@fs.get_root_dir().unwrap(), "value.txt", "updated").unwrap() + "updated" +} +``` + +Import `golemcloud/golem_sdk/filesystem` as `@fs` in `moon.pkg`. + + + +Add `FileOwner: {}` beside the routers in the deployment's `agents` map, then: + +```shell +golem deploy -L -Y +curl http://localhost:9006/files/alice/value +golem agent invoke -L 'FileOwner("alice")' update +curl http://localhost:9006/files/alice/value +``` + +The first read returns `initial:alice`; the second returns `updated`. Reads use +`Cache-Control: no-store` with no ETag. No exported method is called to serve the +file, but the first read can initialize the agent. A slow download can delay other +work on that agent; see [live-file lifecycle](/next/concepts/http-routers#live-durable-agent-files). + +### Calling an agent from a router + +Call ordinary agents through the same clients used for +[agent-to-agent communication](/next/develop/rpc). For example, inside an async +handler, update the file owner before constructing the response: + + + +```typescript +await FileOwner.client.get({ name: 'from-router' }).update(); +``` + + +Inside the handler's `Effect.gen` block: + +```typescript +const owner = yield* FileOwner.client.get({ name: "from-router" }) +yield* owner.update({}) +``` + +Keep the call in the request's Effect scope; do not launch a detached `runPromise`. + + +```rust +FileOwnerClient::get("from-router".to_string()).update().await; +``` + + +Chain the response with `map` after the update, using the Scala.js execution context: + +```scala +import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue + +FileOwnerClient.get("from-router").update().map { _ => + HttpResponse(UShort(200), Nil, request.body) +} +``` + + +Make the handler `pub async fn` before awaiting the generated client: + +```moonbit +let owner = FileOwnerClient::get("from-router") +defer owner.drop() +owner.update() |> ignore +``` + + + +For cross-component calls, declare the dependency and generate its client as described +in [Calling an agent in another component](/next/develop/rpc#calling-an-agent-in-another-component). +A router's cancellation does not roll back completed agent calls. Client retries are +new requests and can repeat effects; see +[cancellation and recovery](/next/concepts/http-routers#cancellation-and-ephemeral-recovery). + +## Publishing OpenAPI + +Add an OpenAPI provider to describe your router's operations. For `EchoRouter`, +describe `POST /` relative to its mount; Golem publishes it as `POST /echo`. +The provider must return OpenAPI 3.1.0, not an OpenAPI 3.0 or YAML document. + + + +Insert this before `.implement(...)`; the SDK serializes the returned object: + +```typescript +.openApi(() => ({ + openapi: '3.1.0', + info: { title: 'Echo API', version: '1' }, + paths: { + '/': { + post: { + operationId: 'echo', + responses: { '200': { description: 'Streamed request bytes' } }, + }, + }, + }, +})) +``` + + +Add an `openApi` effect beside `mount` in the `EchoRouter` options: + +```typescript +openApi: Effect.succeed({ + openapi: "3.1.0", + info: { title: "Echo API", version: "1" }, + paths: { + "/": { + post: { + operationId: "echo", + responses: { "200": { description: "Streamed request bytes" } }, + }, + }, + }, +}), +``` + +For a schema-defined Effect `HttpApi`, generate the description from the same API +that builds the handler. This separate `CatalogRouter` serves `GET /catalog/items/{id}`: + +```typescript +import { Effect, FileSystem, Layer, Path, Schema } from "effect" +import { Etag, HttpPlatform, HttpRouter as Routes } from "effect/unstable/http" +import { + HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi, +} from "effect/unstable/httpapi" +import { Http, HttpRouter } from "@golemcloud/effect-golem" + +const Api = HttpApi.make("Catalog").add( + HttpApiGroup.make("items").add( + HttpApiEndpoint.get("getItem", "/items/:id", { + params: Schema.Struct({ id: Schema.String }), + success: Schema.Struct({ id: Schema.String, count: Schema.Int }), + }), + ), +) +const Handlers = HttpApiBuilder.group(Api, "items", (handlers) => + handlers.handle("getItem", ({ params }) => + Effect.succeed({ id: params.id, count: 7 }), + ), +) +const Platform = Layer.mergeAll( + Path.layer, + Etag.layerWeak, + FileSystem.layerNoop({}), + HttpPlatform.layer.pipe(Layer.provide(FileSystem.layerNoop({}))), +) + +HttpRouter.define("CatalogRouter", { + mount: Http.mount("/catalog"), + openApi: Effect.sync(() => OpenApi.fromApi(Api)), +}).implement(Routes.toHttpEffect( + HttpApiBuilder.layer(Api).pipe( + Layer.provide(Handlers), + Layer.provide(Platform), + ), +)) +``` + +Add `CatalogRouter: {}` to the deployment's `agents` map. The example supplies the +platform services required by `HttpApiBuilder`; its no-op filesystem deliberately +does not support filesystem operations. Golem's static mappings do not use it. +Keep generated paths mount-relative. `Effect.sync` builds the document when the +provider effect runs rather than at module load. Golem invokes providers lazily, +separately from ordinary requests. + + +Add this method to the `HttpRouter` implementation: + +```rust +async fn openapi(&self) -> String { + r#"{ + "openapi": "3.1.0", + "info": {"title": "Echo API", "version": "1"}, + "paths": {"/": {"post": { + "operationId": "echo", + "responses": {"200": {"description": "Streamed request bytes"}} + }}} + }"#.into() +} +``` + + +Add a declaration to `EchoRouter`: + +```scala +@openApiProvider def describe(): Future[String] +``` + +Implement it in `EchoRouterImpl`: + +```scala +def describe(): Future[String] = Future.successful( + """{ + "openapi": "3.1.0", + "info": {"title": "Echo API", "version": "1"}, + "paths": {"/": {"post": { + "operationId": "echo", + "responses": {"200": {"description": "Streamed request bytes"}} + }}} + }""" +) +``` + + +```moonbit +///| +#derive.openapi_provider +pub fn EchoRouter::describe( + _self : Self, +) -> String raise @http.OpenApiJsonError { + @http.openapi_json({ + "openapi": "3.1.0", + "info": { "title": "Echo API", "version": "1" }, + "paths": { + "/": { + "post": { + "operationId": "echo", + "responses": { "200": { "description": "Streamed request bytes" } }, + }, + }, + }, + }) +} +``` + + + +Keep providers finite and deterministic. Use unique component names and operation +IDs across the domain, and local references that resolve within the provider's own +document. See [OpenAPI generation](/next/concepts/http-routers#openapi-generation) +for the supported subset, security merging, conflicts, caching, and size limits. + +After redeployment, retrieve both formats: + +```shell +golem deploy -L -Y +curl http://localhost:9006/openapi.json +curl http://localhost:9006/openapi.yaml +``` + +To place them under `/docs`, add `openapiEndpoint: /docs` beside `domain` and `scheme` +in the HTTP deployment. The URLs become `/docs/openapi.json` and `/docs/openapi.yaml`. +Set `scheme` to match the public origin. Built-in local environments default to +`http`, cloud environments to `https`; custom-server environments must set it explicitly. + + + OpenAPI endpoints are public even when a router requires authentication. Do not + include secret values, private examples, or requester-specific information. Apply + a separate ingress policy if the description must be private. + + +## Protecting mounts + +Configure authentication and CORS on the **router mount**, not its handler method. +Use the same [security scheme configuration](/next/invoke/making-custom-apis/authentication) +as code-first endpoints. The router declarations accept these options: + + + +```typescript +.mount('/echo', { auth: true, cors: ['https://app.example.com'] }) +``` + + +Replace the router's `mount` option with: + +```typescript +mount: Http.mount("/echo", { + auth: true, + cors: ["https://app.example.com"], +}), +``` + + +```rust +#[http_router(name = "EchoRouter", mount = "/echo", + auth = true, cors = ["https://app.example.com"])] +``` + + +```scala +@httpRouter(typeName = "EchoRouter", mount = "/echo", + auth = true, cors = Array("https://app.example.com")) +``` + + +Add these annotations to the router struct: + +```moonbit +#derive.mount_auth(true) +#derive.mount_cors("https://app.example.com") +``` + + + +Protect each child mount explicitly: it does not inherit authentication from a +parent. Test denial before checking file existence, and test browser preflight +separately from plain OPTIONS. See +[authentication and failures](/next/concepts/http-routers#authentication-cors-and-failures) +for host behavior and status codes. + +## Configuring self-hosted deployments + +These settings are for platform operators. Applications use worker-service's +**apps gateway** (`custom_request_port`, default 9006), not its management REST or +gRPC port. + +### Configuring shared storage + +Static files are read directly by worker-service. Configure registry, executors, +and every serving worker-service to share the initial-file blob store and namespace. +For a local/shared-volume topology: + +```toml +[blob_storage] +type = "LocalFileSystem" + +[blob_storage.config] +root = "/blob_storage" +``` + +Mount the **same volume** into all services; identical paths in isolated containers +are not shared storage. The equivalent environment variables are +`GOLEM__BLOB_STORAGE__TYPE=LocalFileSystem` and +`GOLEM__BLOB_STORAGE__CONFIG__ROOT=/blob_storage`. The +[PostgreSQL compose example](https://github.com/golemcloud/golem/blob/main/docker-examples/published-postgres/compose.yaml) +shows the shared volume wiring. + +For S3, configure worker-service with the same `initial_agent_files_bucket`, +`object_prefix`, region, endpoint, and path-style settings as the other services. +Use the complete service configuration for the other required buckets. Grant +worker-service read access, including ranged reads, and supply credentials through +your deployment's secret mechanism. InMemory storage is not shared across processes; +worker-service does not support `KVStoreSqlite`. + +### Configuring ingress + +Use an ingress with an HTTP/2 cleartext upstream (**h2c prior knowledge**), preserve +per-stream resets, and disable request/response buffering for streaming routes. +The gateway also accepts HTTP/1, but does not support `Upgrade: h2c`. + +To accept public-origin headers from a TLS-terminating proxy, list its direct peer +IP in worker-service configuration: + +```toml +[route_resolver] +trusted_ingress_addresses = ["10.0.0.10"] +``` + +The default list is empty. Configure that proxy to remove client forwarding fields +and write exactly one valid `X-Forwarded-Proto` and one `X-Forwarded-Host`. Both are +required; comma-separated proxy chains and `Forwarded` are not alternative origin +sources. Keep the listener private to the intended ingress. + +Test disconnect/reset through the actual proxy, including while waiting for a +response head. A client timeout alone does not prove upstream cancellation. Keep +HTTP/2 enabled: some pending live-file requests over HTTP/1 detect disconnect only +at the exchange deadline. + +### Setting timeouts and resource budgets + +Tune worker-service's session settings to your workload. The defaults are: + +```toml +[http_session] +exchange_timeout = "5m" +retry_deadline = "10s" +retry_delay = "100ms" +max_resume_attempts = 4 +cleanup_timeout = "2s" +retained_input_bytes = 4194304 +retained_input_frames = 32 +event_queue_frames = 2 +command_queue_frames = 16 +max_event_bytes = 17825792 +``` + +Use `exchange_timeout` to bound the whole handler or live-file exchange, including +initialization, queueing, and streaming. There is no separate executor file-read +deadline. Expiry returns 504 before commitment or aborts an already-started response. +Recovery uses the same surviving invocation, with four reattachments allowed over +the exchange's lifetime; it cannot replace a lost executor invocation. + +Use ingress concurrency limits to control live-file load: there is no dedicated +per-agent read queue cap or executor-wide outstanding-read limit. Slow readers can +delay an agent's other work. `durable_streams.load` does not configure these reads. + +The byte/frame values bound encoded session buffering, **not total upload size**. +Input encoding includes schema and frame overhead. Queued output is bounded by +`event_queue_frames × max_event_bytes`, not all per-request allocations. + +OpenAPI generation uses [separate limits](/next/concepts/http-routers#caching-and-limits) +and is not bounded by `exchange_timeout`. Route lookup caching +(`route_resolver.router_cache_ttl = "10m"`, capacity 1024, eviction period 1 minute) +is also separate from the OpenAPI document cache. + +## Troubleshooting + +- **Unexpected handler or 404:** check concrete code-first routes, the selected + mount, and declaration order. Only absent files fall through; permission, + initialization, and storage failures do not. +- **Static file returns 500:** confirm it is provisioned read-only and that + worker-service can read the indexed blob from shared storage. +- **Live read stalls:** check agent initialization and preceding work, then test + cancellation through the ingress and review the exchange deadline. +- **OpenAPI returns 502:** check provider JSON, size limits, local references, and + conflicting paths/components/operation IDs. A provider failure does not disable + ordinary router traffic. + +Use status codes, safe error categories, and router/deployment identities in +diagnostics. Do not log HTTP bodies, provider documents, secrets, or internal paths. + +Complete examples are available in the repository for +[TypeScript](https://github.com/golemcloud/golem/blob/main/cli/golem-cli/tests/app/typescript_http_router.ts), +[Rust](https://github.com/golemcloud/golem/blob/main/test-components/agent-rpc/golem-it-agent-rpc-rust/src/http_router.rs), +[Scala](https://github.com/golemcloud/golem/blob/main/sdks/scala/test-agents/src/main/scala/example/integrationtests/HttpRouterExample.scala), +[MoonBit](https://github.com/golemcloud/golem/blob/main/sdks/moonbit/golem_sdk_example1/golem_moonbit_examples/http_router.mbt), +and [Effect](https://github.com/golemcloud/golem/blob/main/cli/golem-cli/tests/app/effect_http_router.ts). diff --git a/docs/src/content/next/invoke/making-custom-apis.mdx b/docs/src/content/next/invoke/making-custom-apis.mdx index a55adec45b..266ddca068 100644 --- a/docs/src/content/next/invoke/making-custom-apis.mdx +++ b/docs/src/content/next/invoke/making-custom-apis.mdx @@ -7,11 +7,15 @@ but also provides a way to expose agent methods as HTTP APIs. In Golem 1.5, HTTP endpoints are defined **directly in agent code** using language-native decorators and macros — no YAML route definitions or scripting languages needed. +For raw streaming request/response handling, framework-owned routing, or static and +live file mappings, see [HTTP routers and file serving](/next/invoke/http-routers). +Complete code-first endpoints take precedence over those mounted fallback behaviors. + ## Mount Points Every agent that wants to expose HTTP endpoints must first define a **mount point** — the base URL prefix under which all its endpoints are available. The mount path can contain placeholders like `{name}` that map to the agent's constructor parameters. - + ```typescript export const Tasks = defineAgent({ @@ -25,6 +29,19 @@ export const Tasks = defineAgent({ ``` +```typescript +export const Tasks = defineAgent({ + name: "Tasks", + mode: "durable", + id: { name: Schema.String }, + http: Http.mount("/task-agents/{name}"), + methods: { + // ... + }, +}) +``` + + ```rust #[agent_definition(mount = "/task-agents/{name}")] pub trait Tasks { @@ -58,7 +75,7 @@ If there are multiple agent constructor parameters, they all have to be mapped i Once the mount is defined, individual agent methods can be exposed as HTTP **endpoints**. Each endpoint specifies an HTTP method and a path suffix relative to the mount point. Path placeholders are mapped to method parameters. Parameters not mapped through the path or headers are taken from the request body. - + ```typescript createTask: method({ @@ -77,6 +94,23 @@ completeTask: method({ ``` +```typescript +createTask: method({ + input: { request: CreateTaskRequest }, + success: Task, + http: [Http.post("/tasks")], +}), + +getTasks: method({ input: {}, success: Schema.Array(Task), http: [Http.get("/tasks")] }), + +completeTask: method({ + input: { id: Schema.Number }, + success: Schema.NullOr(Task), + http: [Http.post("/tasks/{id}/complete")], +}), +``` + + ```rust #[endpoint(post = "/tasks")] fn create_task(&mut self, request: CreateTaskRequest) -> Task; @@ -126,7 +160,7 @@ Supported HTTP methods: `get`, `post`, `put`, `delete`, `patch`, `head`, `option Query parameters can be bound to method parameters using standard HTTP query syntax in the endpoint path: - + ```typescript search: method({ @@ -137,6 +171,15 @@ search: method({ ``` +```typescript +search: method({ + input: { category: Schema.String, limit: Schema.Number, offset: Schema.Number }, + success: Schema.Array(Item), + http: [Http.get("/search/{category}?limit={limit}&offset={offset}")], +}), +``` + + ```rust #[endpoint(get = "/search/{category}?limit={limit}&offset={offset}")] fn search(&self, category: String, limit: u64, offset: u64) -> Vec; @@ -162,7 +205,7 @@ pub fn MyAgent::search(self : Self, category : String, limit : UInt64, offset : Custom HTTP headers can be mapped to function parameters: - + ```typescript example: method({ @@ -173,6 +216,19 @@ example: method({ ``` +```typescript +example: method({ + input: { location: Schema.String, name: Schema.String }, + success: Schema.String, + http: [ + Http.get("/example", { + headers: { "X-Foo": "location", "X-Bar": "name" } as const, + }), + ], +}), +``` + + ```rust #[endpoint(get = "/example", headers("X-Foo" = "location", "X-Bar" = "name"))] fn example(&self, location: String, name: String) -> String; @@ -200,7 +256,7 @@ pub fn ExampleAgent::example(self : Self, location : String, name : String) -> S The `{*variable}` syntax captures all remaining path segments. It must be the last segment in the path: - + ```typescript serveFile: method({ @@ -211,6 +267,15 @@ serveFile: method({ ``` +```typescript +serveFile: method({ + input: { rest: Schema.String }, + success: FileData, + http: [Http.get("/files/{*rest}")], +}), +``` + + ```rust #[endpoint(get = "/rest/{*tail}")] fn catch_all(&self, tail: String) -> String; @@ -247,7 +312,7 @@ Return types control the HTTP response: Use the `UnstructuredBinary` type to receive raw binary data in the request body: - + ```typescript upload: method({ @@ -258,24 +323,31 @@ upload: method({ ``` +```typescript +upload: method({ + input: { + file: Unstructured.UnstructuredBinary({ + restrictions: [{ mimeType: "application/octet-stream" }], + }), + }, + success: Schema.Number, + http: [Http.post("/upload")], +}), +``` + + ```rust #[endpoint(post = "/upload")] -fn upload(&mut self, file: UnstructuredBinary) -> u64; +fn upload(&mut self, file: UnstructuredBinary) -> u64; ``` + +`String` implements `AllowedMimeTypes` and accepts any MIME type. Use an enum deriving `AllowedMimeTypes` to restrict accepted values. -```scala -@endpoint(method = "POST", path = "/upload") -def upload(file: UnstructuredBinary): Future[Long] -``` +The current Scala SDK does not expose the removed `UnstructuredBinary`/`BinarySegment` API. Use a supported structured byte-array parameter, or handle raw binary in another supported SDK. -```moonbit -#derive.endpoint(post = "/upload") -pub fn MyAgent::upload(self : Self, file : UnstructuredBinary) -> UInt64 { - // ... -} -``` +The current MoonBit SDK does not yet expose a user-facing unstructured-binary wrapper. `Bytes` is structured binary data and does not opt an endpoint into the raw unstructured-binary body mapping. @@ -283,7 +355,7 @@ pub fn MyAgent::upload(self : Self, file : UnstructuredBinary) -> UInt64 { CORS can be configured at the **mount level** (applies to all endpoints) and optionally overridden per **endpoint**: - + ```typescript export const MyAgent = defineAgent({ @@ -300,6 +372,23 @@ export const MyAgent = defineAgent({ ``` +```typescript +export const MyAgent = defineAgent({ + name: "MyAgent", + mode: "durable", + id: { name: Schema.String }, + http: Http.mount("/api/{name}", { cors: ["https://app.example.com"] }), + methods: { + getPublic: method({ + input: {}, + success: Data, + http: [Http.get("/public", { cors: ["*"] })], + }), + }, +}) +``` + + ```rust #[agent_definition(mount = "/api/{name}", cors = ["https://app.example.com"])] pub trait MyAgent { @@ -349,7 +438,7 @@ Per-endpoint CORS settings are unioned with the mount-level settings. Golem API gateway has built-in authentication support. Authentication can be enabled at the mount level or per endpoint using `auth = true`. See [the API authentication page](/next/invoke/making-custom-apis/authentication) for setup details. - + ```typescript export const SecureAgent = defineAgent({ @@ -370,6 +459,25 @@ adminOnly: method({ ``` +```typescript +export const SecureAgent = defineAgent({ + name: "SecureAgent", + mode: "durable", + id: { name: Schema.String }, + http: Http.mount("/secure/{name}", { auth: true }), + methods: { + adminOnly: method({ + input: {}, + success: AdminData, + http: [Http.get("/admin", { auth: true })], + }), + }, +}) +``` + +The handler can read the authenticated caller with `yield* Principal.Principal`; it does not need to be part of the method input. + + ```rust // Mount-level: all endpoints require auth #[agent_definition(mount = "/secure/{name}", auth = true)] @@ -423,7 +531,7 @@ When authentication is enabled, agent constructors and methods can optionally re Endpoints can be served by **phantom agents** — ephemeral, stateless agents that are created for each request and discarded afterward. This is useful for gateway-like agents that dispatch to internal agents, allowing requests to be processed completely in parallel. - + ```typescript export const Gateway = defineAgent({ @@ -437,6 +545,19 @@ export const Gateway = defineAgent({ ``` +```typescript +export const Gateway = defineAgent({ + name: "Gateway", + mode: "durable", + id: { name: Schema.String }, + http: Http.mount("/gateway/{name}", { phantomAgent: true }), + methods: { + // A new instance is created per request + }, +}) +``` + + ```rust #[agent_definition(mount = "/gateway/{name}", phantom_agent = true)] pub trait Gateway { diff --git a/docs/src/content/next/invoke/making-custom-apis/authentication.mdx b/docs/src/content/next/invoke/making-custom-apis/authentication.mdx index 4c5bccf008..ffeadf1837 100644 --- a/docs/src/content/next/invoke/making-custom-apis/authentication.mdx +++ b/docs/src/content/next/invoke/making-custom-apis/authentication.mdx @@ -57,7 +57,7 @@ By now, you are having a security scheme registered with `golem` which enables a Once the security scheme is registered, enable authentication using the `auth` option on your agent's mount or on individual endpoints: - + ```typescript import { z } from 'zod'; @@ -90,6 +90,50 @@ export const SecureAgentImpl = SecureAgent.implement({ ``` +```typescript +import { Effect, Schema } from "effect" +import { + defineAgent, + Http, + method, + Principal, +} from "@golemcloud/effect-golem" + +const Profile = Schema.Struct({ + email: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), +}) + +export const SecureAgent = defineAgent({ + name: "SecureAgent", + mode: "durable", + id: { name: Schema.String }, + http: Http.mount("/secure/{name}", { auth: true }), + methods: { + getProfile: method({ + input: {}, + success: Profile, + http: [Http.get("/profile")], + }), + }, +}).implement({ + init: () => Effect.succeed({}), + methods: () => ({ + getProfile: () => + Effect.gen(function* () { + const principal = yield* Principal.Principal + if (principal.tag !== "oidc") { + return yield* Effect.dieMessage("Expected an OIDC principal") + } + return { email: principal.val.email, name: principal.val.name } + }), + }), +}) +``` + +`Principal.Principal` provides the current invocation's authenticated caller without adding an HTTP input parameter. + + ```rust #[agent_definition(mount = "/secure/{name}", auth = true)] pub trait SecureAgent { diff --git a/docs/src/content/next/invoke/mcp.mdx b/docs/src/content/next/invoke/mcp.mdx index 7c8b0782d1..7690c3eda9 100644 --- a/docs/src/content/next/invoke/mcp.mdx +++ b/docs/src/content/next/invoke/mcp.mdx @@ -99,7 +99,7 @@ Singleton agents (those with no constructor parameters) expose parameterless met Method descriptions and prompt hints become MCP tool and resource metadata, helping AI models understand what each tool does and how to use it. TypeScript defines them with the `description` and `promptHint` properties of `method(...)`; other SDKs use the corresponding language-specific annotations. - + ```typescript import { z } from 'zod'; @@ -130,6 +130,31 @@ export const CounterAgentImpl = CounterAgent.implement({ ``` +```typescript +import { Effect, Ref, Schema } from "effect" +import { defineAgent, method } from "@golemcloud/effect-golem" + +export const CounterAgent = defineAgent({ + name: "CounterAgent", + mode: "durable", + id: { name: Schema.String }, + methods: { + incrementBy: method({ + input: { value: Schema.Number }, + success: Schema.Number, + description: "Add a value to the current counter", + promptHint: "Increments the counter by the given value and returns the new count", + }), + }, +}).implement({ + init: () => Ref.make(0), + methods: (count) => ({ + incrementBy: ({ value }) => Ref.updateAndGet(count, (current) => current + value), + }), +}) +``` + + ```rust #[agent_definition] pub trait CounterAgent { @@ -151,9 +176,9 @@ trait CounterAgent extends BaseAgent { ```moonbit +/// Add a value to the current counter #derive.agent -#derive.prompt("Increments the counter by the given value and returns the new count") -#derive.description("Add a value to the current counter") +#derive.prompt_hint("Increments the counter by the given value and returns the new count") pub fn CounterAgent::increment_by(self : Self, value : UInt64) -> UInt64 { // ... } @@ -188,6 +213,10 @@ Golem provides special data types that map naturally to MCP content types: These types allow agents to exchange rich content with MCP clients beyond simple JSON values. + +MoonBit currently supports schema-native multimodal values through `@multimodal.Multimodal[T]` and `#derive.multimodal`, but does not yet provide user-facing `UnstructuredText`, `UnstructuredBinary`, or multipart wrappers. Use supported structured schemas or multimodal values instead; the removed `#derive.text_languages` and `#derive.mime_types` annotations are not available. + + ## Testing with MCP Inspector You can test your MCP server using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector): diff --git a/docs/src/content/next/invoke/repl.mdx b/docs/src/content/next/invoke/repl.mdx index 62fbcdecf6..81eca10436 100644 --- a/docs/src/content/next/invoke/repl.mdx +++ b/docs/src/content/next/invoke/repl.mdx @@ -24,7 +24,11 @@ The REPL auto-generates TypeScript classes for all agents in the application. Yo 2 ``` -It is possible to use the TypeScript REPL with agents written in any language — you can use TypeScript syntax to invoke Rust, Scala, or MoonBit agents. +It is possible to use the TypeScript REPL with agents written in any language — including Effect TypeScript, Rust, Scala, and MoonBit agents. + + +Effect applications use this same generated TypeScript REPL client. The REPL does not expose an Effect runtime or return `Effect` programs; use the generated Promise-based TypeScript methods interactively, and use an Effect bridge library in an external application when you need Effect-native composition. + ## Built-in commands @@ -80,10 +84,11 @@ golem repl --script-file test.rs --language rust --yes | Language | Interactive REPL | Script mode | | ---------- | ------------------------- | ----------- | | TypeScript | ✅ Full support | ✅ | +| Effect TypeScript | ✅ Via TypeScript syntax | ✅ TypeScript scripts | | Rust | ⚠️ Slow (recompilation) | ✅ | | Scala | ❌ Not available | ❌ | | MoonBit | ❌ Not available | ❌ | -The TypeScript REPL can work with agents written in any language — you can use TypeScript syntax to invoke Scala, Rust, or MoonBit agents. +The TypeScript REPL can work with agents written in any language — you can use TypeScript syntax to invoke Effect TypeScript, Scala, Rust, or MoonBit agents. diff --git a/docs/src/content/next/operate/durable_streams.mdx b/docs/src/content/next/operate/durable_streams.mdx index b8aa48ec48..ecaa3fb81d 100644 --- a/docs/src/content/next/operate/durable_streams.mdx +++ b/docs/src/content/next/operate/durable_streams.mdx @@ -4,6 +4,8 @@ import { Callout } from "nextra/components" Streaming agent invocations use the same durability model as other durable agent work. Stream registrations, items, terminals, attachment epochs, consumer journals, and deletion barriers are committed to agent oplogs. Losing a CLI connection, worker process, executor, or service connection does not by itself cancel the invocation. +For agent code that reads or writes a Durable Streams service outside the invocation-session protocol, see [External Durable Streams](/next/develop/durable-streams). + ## Configuration Worker executors expose the protocol-v1 timing controls under `[durable_stream]`: diff --git a/docs/src/content/next/operate/logs.mdx b/docs/src/content/next/operate/logs.mdx index 754183b977..124150ef84 100644 --- a/docs/src/content/next/operate/logs.mdx +++ b/docs/src/content/next/operate/logs.mdx @@ -72,12 +72,23 @@ For both `file` and `stdout` logging, a set of boolean flags control the format ## Golem agent logs - + In TypeScript agents, use the standard `console` API to log messages. `console.log` writes to the agent's standard output, `console.error` writes to standard error, while `console.debug`, `console.info`, `console.warn`, etc. write log entries with the corresponding log level. + Effect agents use Effect's logging API. The SDK installs a Golem logger, so log levels, annotations, spans, and trace identifiers are emitted through WASI logging: + + ```typescript + import { Effect } from "effect" + + const program = Effect.logInfo("my_message").pipe( + Effect.annotateLogs({ operation: "example" }), + ) + ``` + + In Rust agents, anything written to the agent's standard output and error channels, for example with `println!` or `eprintln!`, is logged. In addition to that it is possible to use the `log` function in the `golem_rust::bindings::wasi::logging::logging` module directly to write log entries with specific log levels: diff --git a/docs/src/content/next/operate/persistence.mdx b/docs/src/content/next/operate/persistence.mdx index 3c4aae5c4b..d42e196a4c 100644 --- a/docs/src/content/next/operate/persistence.mdx +++ b/docs/src/content/next/operate/persistence.mdx @@ -6,23 +6,25 @@ The Golem Worker Executor uses a set of storage layers for persisting agent stat ## Storage types -The worker executor requires three types of storage backends: +The worker executor requires four types of storage backends: - A **blob storage** for storing and retrieving arbitrary sized binary blobs -- A **key-value storge** with some extra requirements for set/sorted-set-like operations +- A **key-value storage** with some extra requirements for set/sorted-set-like operations - An **indexed storage** for an append-only, indexable store for agent's operation log (oplog) +- A **scheduler storage** for durable scheduled actions Currently Golem provides the following implementations, configurable through the worker executor's config file: -| Storage type | Implementations | -| ------------------ | -------------------------- | -| Blob storage | S3, file system, in-memory | -| Key-values storage | Redis, in-memory | -| Indexed storage | Redis (streams), in-memory | +| Storage type | Implementations | +| --- | --- | +| Blob storage | S3, file system, SQLite, in-memory | +| Key-value storage | Redis, PostgreSQL, SQLite, multi-SQLite, namespace-routed, in-memory | +| Indexed storage | Redis, PostgreSQL, SQLite, multi-SQLite, in-memory | +| Scheduler storage | PostgreSQL, SQLite, in-memory | ## Compilation cache -The Golem **component service** stores the component WASM files in its own storage layer (configured to be either S3 or a persistent volume) and exposes them through its [download API](/next/rest-api/component) for the worker executor. The WASM files need to be **compiled** to native code before execution, which is a time-consuming task. To reduce the time required for instantiating agents, these compiled binaries are cached in the worker executor's **blob storage**. +The Golem **registry service** stores original component Wasm files in its configured blob storage, which supports S3, local filesystem, SQLite, and in-memory backends. It exposes them through its [download API](/next/rest-api/component) for the worker executor. The Wasm files must be **compiled** to native code before execution, which is time-consuming. To reduce agent startup time, these compiled binaries are cached in the worker executor's **blob storage**. The **component compilation service** is a special, horizontally scalable component of the Golem system which shares this storage with worker executors and is capable of precompiling components before they are being used by any of the executors. @@ -35,7 +37,7 @@ The oplog is stored in multiple configurable layers. The default configuration i - There is a **secondary layer** that is also persisted to the **indexed storage**, but on this level each entry holds a compressed set of oplog entries. - A **tertiary layer** holds even larger chunks of compressed oplog entries and stores them in the **blob storage**. -Oplogs are continuously moved towards the lower layers to prevent the primary oplog storage (Redis) from getting full. Agents that were not active for a while are fully stored in the blob storage only. +Oplogs are moved toward lower layers to prevent the primary indexed storage from filling up. With the default layers, inactive agents' oplog entries eventually reach blob storage. A small creation-fence key remains in primary storage. In addition to the above, there are oplog entries holding user-defined data (such as invocation parameters). As these user-defined payloads can have an arbitrary size, the worker executor has to protect its storage layer against putting too large entries into the primary oplog. For this reason large payloads are written to the **blob storage** and the corresponding oplog entry only stores a reference to them. Payloads below a configurable limit are stored inline in the oplog entry. @@ -59,7 +61,7 @@ These options have the following meaning: | Option | Description | | ------------------------------ | ------------------------------------------------------------------------------------------------------- | -| `archive_interval` | The period after the old sections of the oplog get moved to one layer down in the layered oplog storage | +| `archive_interval` | The delay between scheduled archival steps that move oplog sections down one layer | | `blob_storage_layers` | The number of archive layers using the **blob storage** | | `indexed_storage_layers` | The total number of layers (one primary + archives) using the **indexed storage** | | `entry_count_limit` | The number of oplog entries triggering the archivation of a chunk to the lower oplog layer | @@ -68,11 +70,11 @@ These options have the following meaning: ## Agent metadata -The **oplog** is the primary source of truth for everything we need to store about an agent, but for performance reasons the **key value store** is also used to store aggregated agent metadata for agents. This metadata is not always completely up-to-date; it store the last oplog index it was calculated from, and its latest version can be reproduced by reading and processing the newer oplog entries. For agents which has no longer data in the primary oplog, we don't store agent metadata either. When these agents have to be recovered, their metadata is reconstructed by reading the whole archived oplog. +The **oplog** is the primary source of truth for agent state, but the **key-value store** also caches aggregated agent metadata for performance. This metadata records the oplog index from which it was calculated, and Golem can refresh it by processing newer entries. Once archival is complete, Golem clears the cached status and its checkpoint. If the agent is recovered later, Golem reconstructs the metadata from the archived oplog. ## Promises and schedules -The worker executor also stores **promise information** in its **key-value storage**. This is simply an entry for each created Golem promise, storing it's completion state. Internally Golem also uses scheduled promise completion (for example for waking up agents after long sleeps). These scheduled events are also stored in the **key-value storage**. +The worker executor stores **promise information** in its **key-value storage**, with one entry for each promise's completion state. Scheduled actions, including promise completion used to wake agents after long sleeps, use the separate **scheduler storage**. ## WASI Blob store and Key Value store @@ -80,8 +82,9 @@ Golem implements (partially) the [WASI Blob Store](https://github.com/WebAssembl ## Adding new storage implementations -The open-source version of Golem can be easily extended to support alternative databases than the built-in Redis and S3 implementations. Take a look at the following traits: +The open-source version of Golem can be extended with additional storage backends beyond the built-in implementations. See these traits: -- [`BlobStorage`](https://github.com/golemcloud/golem/blob/main/golem-worker-executor-base/src/storage/blob/mod.rs) -- [`KeyValueStorage`](https://github.com/golemcloud/golem/blob/main/golem-worker-executor-base/src/storage/keyvalue/mod.rs) -- [`IndexedStorage`](https://github.com/golemcloud/golem/blob/main/golem-worker-executor-base/src/storage/indexed/mod.rs) +- [`BlobStorage`](https://github.com/golemcloud/golem/blob/main/golem-service-base/src/storage/blob/mod.rs) +- [`KeyValueStorage`](https://github.com/golemcloud/golem/blob/main/golem-worker-executor/src/storage/keyvalue/mod.rs) +- [`IndexedStorage`](https://github.com/golemcloud/golem/blob/main/golem-worker-executor/src/storage/indexed/mod.rs) +- [`SchedulerStorage`](https://github.com/golemcloud/golem/blob/main/golem-worker-executor/src/storage/scheduler/mod.rs) diff --git a/docs/src/content/next/quickstart.mdx b/docs/src/content/next/quickstart.mdx index 72f670e8e0..084b9b6524 100644 --- a/docs/src/content/next/quickstart.mdx +++ b/docs/src/content/next/quickstart.mdx @@ -87,7 +87,7 @@ The `golem` command line interface provides a set of commands to create, build, To get started, you create an application and a single component using one of the supported programming languages with the `golem new` command. If no additional parameters are provided, the command will interactively ask for all required information. - + Create a TypeScript agent using the default template: @@ -96,6 +96,16 @@ To get started, you create an application and a single component using one of th ``` Note: For TypeScript, you need to have `npm` installed on your system. + + + + Create an Effect TypeScript agent using the default template: + + ```shell copy + golem new --template effect --component-name example:counter --yes agent-examples + ``` + + Note: Effect agents require `npm`; keep the generated `effect` dependency version aligned with `@golemcloud/effect-golem`. @@ -139,7 +149,7 @@ This compiles the newly created application, which consists of a _single Golem c ### Write the code - + The default template's source code is located in the `src/counter-agent.ts` file, assuming that the `example:counter` name has been used in the `golem new` command. The directory structure allows applications to have multiple components, even implemented in multiple programming languages if needed. @@ -175,6 +185,35 @@ export const CounterAgentImpl = CounterAgent.implement({ + +The default Effect template's source code is in `src/counter-agent.ts`. +Replace that file with the following code and remove the generated `httpApi` section from the component's `golem.yaml` for now. The HTTP API section later in this guide adds a deployment matching the renamed `CounterAgent`. + +```typescript +import { Effect, Ref, Schema } from "effect" +import { defineAgent, method } from "@golemcloud/effect-golem" + +export const CounterAgent = defineAgent({ + name: "CounterAgent", + mode: "durable", + id: { name: Schema.String }, + methods: { + increment: method({ + input: {}, + success: Schema.Number, + promptHint: "Increase the count by one", + description: "Increases the count by one and returns the new value", + }), + }, +}).implement({ + init: () => Ref.make(0), + methods: (count) => ({ + increment: () => Ref.updateAndGet(count, (value) => value + 1), + }), +}) +``` + + The default template's source code is located in the `src/counter_agent.rs` file, assuming that the `example:counter` name has been used in the `golem new` command. @@ -358,7 +397,7 @@ Agents can be invoked from other agents, through the REPL or CLI, or Golem's Inv To finish the Quickstart example with an HTTP POST endpoint for tracking counts, first add a **mount point** and an **endpoint** decorator to the agent code: - + ```typescript import { z } from 'zod'; @@ -391,6 +430,34 @@ export const CounterAgentImpl = CounterAgent.implement({ ``` + +```typescript +import { Effect, Ref, Schema } from "effect" +import { defineAgent, Http, method } from "@golemcloud/effect-golem" + +export const CounterAgent = defineAgent({ + name: "CounterAgent", + mode: "durable", + id: { name: Schema.String }, + http: Http.mount("/counters/{name}"), + methods: { + increment: method({ + input: {}, + success: Schema.Number, + http: [Http.post("/increment")], + promptHint: "Increase the count by one", + description: "Increases the count by one and returns the new value", + }), + }, +}).implement({ + init: () => Ref.make(0), + methods: (count) => ({ + increment: () => Ref.updateAndGet(count, (value) => value + 1), + }), +}) +``` + + ```rust use golem_rust::{ diff --git a/docs/src/content/next/type-mapping.mdx b/docs/src/content/next/type-mapping.mdx index f1cc8b9265..5fbcfbe8d6 100644 --- a/docs/src/content/next/type-mapping.mdx +++ b/docs/src/content/next/type-mapping.mdx @@ -11,7 +11,7 @@ When calling the raw invoke endpoints, each argument and result is wrapped as a ## Language types to JSON - + The following TypeScript types are supported: @@ -43,6 +43,26 @@ When calling the raw invoke endpoints, each argument and result is wrapped as a | `Float32Array` | `[3.4028235e+38, 0, -3.4028235e+38]` | JSON array of numbers | | `Float64Array` | `[1.7976931348623157e+308, 0, -1.7976931348623157e+308]` | JSON array of numbers | + + Effect agents use Effect `Schema` values to define their wire types: + + | Effect Schema | JSON representation | Remarks | + | ------------- | ------------------- | ------- | + | `Schema.String` | `"hello world"` | JSON string | + | `Schema.Boolean` | `true`, `false` | JSON boolean | + | `Schema.Number` | `1234.0` | JSON number | + | `Schema.BigInt` | `1234` | JSON number | + | `Schema.Array(T)` | `["one", "two"]` | JSON array; decoded as a readonly array | + | `Schema.Struct({ ... })` | `{ "a": "hello", "b": 1234 }` | JSON object | + | `Schema.Tuple([T1, T2])` | `["hello", 1234]` | Fixed-order JSON array | + | `Schema.NullOr(T)` | `"value"` or `null` | Value or `null` | + | `Schema.optional(T)` | field value or `null` | The Effect-side property may be omitted; JSON includes the field with `null` | + | `Schema.Literals(["x", "y"])` | `"x"` | JSON string enum | + | `Schema.ReadonlyMap(K, V)` | `[["key", 1]]` | Array of key-value pairs | + | `WitTypes.Uint8` and other WIT numeric schemas | `0` to `255` | Use these when exact WIT integer width matters | + + Custom Effect schemas are supported when the Effect SDK's WIT codec can compile their encoded shape. + The following Rust types are supported: @@ -151,7 +171,7 @@ There are some special, Golem agent specific types that can be used in the agent ### Unstructured text Unstructured text is an arbitrary string, optionally annotated with a language code, that is either passed directly to/from the agent, or as an URL reference. The agent can optionally specify the set of supported language codes. - + ```typescript import { UnstructuredText } from '@golemcloud/golem-ts-sdk'; @@ -184,6 +204,23 @@ Unstructured text is an arbitrary string, optionally annotated with a language c ``` + + ```typescript + import { method, Schema, Unstructured } from "@golemcloud/effect-golem" + + const EnglishText = Unstructured.UnstructuredText({ + restrictions: [{ languageCode: "en" }], + }) + + const exampleMethod = method({ + input: { text: EnglishText }, + success: Schema.Void, + }) + ``` + + With no `restrictions`, `UnstructuredText()` accepts any language code. Values are `{ _tag: "inline", val, languageCode? }` or `{ _tag: "url", val }`. + + ```rust use golem_rust::agentic::UnstructuredText; @@ -249,108 +286,11 @@ Unstructured text is an arbitrary string, optionally annotated with a language c - ```scala - import golem.data.unstructured.{AllowedLanguages, TextSegment} - import golem.runtime.annotations.languageCode - import golem.runtime.macros.AllowedLanguagesDerivation - - import scala.concurrent.Future - - sealed trait MyLanguageCode - object MyLanguageCode { - @languageCode("en") - case object En extends MyLanguageCode - - @languageCode("es") - case object Spanish extends MyLanguageCode - - @languageCode("de") - case object German extends MyLanguageCode - - given AllowedLanguages[MyLanguageCode] = AllowedLanguagesDerivation.derived - } - - def exampleMethod( - unstructuredTextWithLanguageCode: TextSegment[MyLanguageCode] - ): Future[Unit] = - Future.successful(()) - ``` - - In Scala, unstructured text is represented by `TextSegment[Lang]`. To allow any language, use - `TextSegment[AllowedLanguages.Any]`. If we want to restrict the accepted languages, we define a language ADT - and provide an `AllowedLanguages` instance for it. - - The above code defines three allowed language codes: "en", "es", and "de". The `@languageCode` annotation - customizes the actual code name; without it, case names are lowercased and underscores are converted to hyphens. - - ```scala - trait AllowedLanguages[A] { - def codes: Option[List[String]] - } - - object AllowedLanguages { - sealed trait Any - } - - final case class TextSegment[Lang](value: UnstructuredTextValue) - - object TextSegment { - def inline[Lang](text: String, languageCode: Option[String] = None): TextSegment[Lang] = - TextSegment(UnstructuredTextValue.Inline(text, languageCode)) - - def url[Lang](value: String): TextSegment[Lang] = - TextSegment(UnstructuredTextValue.Url(value)) - } - ``` + The current Scala SDK does not expose the removed `TextSegment` or unstructured-text API. Use supported structured `String` values for text, without language restrictions or inline-or-URL semantics. - ```moonbit - ///| - #derive.agent - struct TextAgent { - name : String - } - - ///| - fn TextAgent::new(name : String) -> TextAgent { - { name, } - } - - ///| - #derive.text_languages("unstructured_text_with_language_code", "en") - pub fn TextAgent::example_method( - self : Self, - unstructured_text_with_language_code : @types.UnstructuredText, - ) -> Unit { - ignore(self) - ignore(unstructured_text_with_language_code) - } - ``` - - In MoonBit, unstructured text is represented by `@types.UnstructuredText`. Unlike the Rust and Scala SDKs, - allowed languages are not encoded in the type itself. Instead, use `#derive.text_languages("param", ...)` - on the agent method to restrict the accepted language codes for that parameter. - - The type itself represents either an inline string or a remote reference: - - ```moonbit - pub(all) enum UnstructuredText { - Url(String) - Inline(text~ : String, language_code~ : String?) - } derive(Show, Eq) - - pub fn UnstructuredText::from_url(url : String) -> UnstructuredText { - Url(url) - } - - pub fn UnstructuredText::from_inline( - text : String, - language_code? : String? = None, - ) -> UnstructuredText { - Inline(text~, language_code~) - } - ``` + The current MoonBit SDK does not yet provide a user-facing unstructured-text wrapper. Use `String` for structured text. The removed `@types.UnstructuredText` wrapper and `#derive.text_languages` annotation are not supported. @@ -359,7 +299,7 @@ Unstructured text is an arbitrary string, optionally annotated with a language c Unstructured binary data is similar to unstructured text, but it is annotated (and restricted by) MIME types. - + ```typescript import { @@ -391,6 +331,25 @@ Unstructured binary data is similar to unstructured text, but it is annotated (a }; ``` + + ```typescript + import { method, Schema, Unstructured } from "@golemcloud/effect-golem" + + const JsonOrPng = Unstructured.UnstructuredBinary({ + restrictions: [ + { mimeType: "application/json" }, + { mimeType: "image/png" }, + ], + }) + + const exampleMethod = method({ + input: { binary: JsonOrPng }, + success: Schema.Void, + }) + ``` + + Values are `{ _tag: "inline", val: Uint8Array, mimeType? }` or `{ _tag: "url", val }`. + ```rust use golem_rust::agentic::UnstructuredBinary; @@ -443,103 +402,10 @@ Unstructured binary data is similar to unstructured text, but it is annotated (a ``` - ```scala - import golem.data.unstructured.{AllowedMimeTypes, BinarySegment} - import golem.runtime.annotations.mimeType - import golem.runtime.macros.AllowedMimeTypesDerivation - - import scala.concurrent.Future - - sealed trait MyMimeType - object MyMimeType { - @mimeType("application/json") - case object ApplicationJson extends MyMimeType - - @mimeType("image/png") - case object ImagePng extends MyMimeType - - given AllowedMimeTypes[MyMimeType] = AllowedMimeTypesDerivation.derived - } - - def exampleMethod( - unstructuredBinaryWithMimeType: BinarySegment[MyMimeType] - ): Future[Unit] = - Future.successful(()) - ``` - - In Scala, unstructured binary is represented by `BinarySegment[Mime]`. To allow any MIME type, use - `BinarySegment[AllowedMimeTypes.Any]`. If we want to restrict the accepted MIME types, we define a MIME ADT - and provide an `AllowedMimeTypes` instance for it. - - The `@mimeType` annotation specifies the actual MIME type string for each allowed case. - - ```scala - trait AllowedMimeTypes[A] { - def mimeTypes: Option[List[String]] - } - - object AllowedMimeTypes { - sealed trait Any - } - - final case class BinarySegment[Mime](value: UnstructuredBinaryValue) - - object BinarySegment { - def inline[Mime](bytes: Array[Byte], mimeType: String): BinarySegment[Mime] = - BinarySegment(UnstructuredBinaryValue.Inline(bytes, mimeType)) - - def url[Mime](value: String): BinarySegment[Mime] = - BinarySegment(UnstructuredBinaryValue.Url(value)) - } - ``` + The current Scala SDK does not expose the removed `BinarySegment` or unstructured-binary API. Use a supported structured byte-array type, without MIME restrictions or inline-or-URL semantics. - ```moonbit - ///| - #derive.agent - struct BinaryAgent { - name : String - } - - ///| - fn BinaryAgent::new(name : String) -> BinaryAgent { - { name, } - } - - ///| - #derive.mime_types("unstructured_binary_with_mime_type", "application/json", "image/png") - pub fn BinaryAgent::example_method( - self : Self, - unstructured_binary_with_mime_type : @types.UnstructuredBinary, - ) -> Unit { - ignore(self) - ignore(unstructured_binary_with_mime_type) - } - ``` - - In MoonBit, unstructured binary is represented by `@types.UnstructuredBinary`. Unlike the Rust and Scala SDKs, - allowed MIME types are not encoded in the type itself. Instead, use `#derive.mime_types("param", ...)` - on the agent method to restrict the accepted MIME types for that parameter. - - The type itself represents either an inline byte array or a remote reference: - - ```moonbit - pub(all) enum UnstructuredBinary { - Url(String) - Inline(data~ : Bytes, mime_type~ : String) - } derive(Show, Eq) - - pub fn UnstructuredBinary::from_url(url : String) -> UnstructuredBinary { - Url(url) - } - - pub fn UnstructuredBinary::from_inline( - data : Bytes, - mime_type~ : String, - ) -> UnstructuredBinary { - Inline(data~, mime_type~) - } - ``` + The current MoonBit SDK does not yet provide a user-facing unstructured-binary wrapper. Use `Bytes` for structured binary data. The removed `@types.UnstructuredBinary` wrapper and `#derive.mime_types` annotation are not supported. @@ -555,7 +421,7 @@ There are three variants of multimodal values supported: * `MultimodalCustom`, allowing to pass an arbitrary number of values which is either an unstructured text or binary or a user defined structured type `T`. * `MultimodalAdvanced` , allowing to pass an arbitrary number of values of the user defined structured type `T`. - + ```typescript @@ -619,6 +485,32 @@ There are three variants of multimodal values supported: + + ```typescript + import { Schema } from "effect" + import { method, Multimodal, WitTypes } from "@golemcloud/effect-golem" + + const BasicContent = Multimodal.multimodalTextImage() + + const ContentWithMetadata = Multimodal.multimodalTextImageCustom( + Schema.Struct({ field1: Schema.String, field2: Schema.Number }), + { customName: "metadata" }, + ) + + const FullyCustom = Multimodal.multimodal({ + text: Schema.String, + image: Schema.Array(WitTypes.Uint8), + }) + + const exampleMethod = method({ + input: { content: BasicContent }, + success: Schema.Void, + }) + ``` + + `Multimodal.multimodal(...)` defines the named cases. Runtime values are ordered arrays of `{ _tag: caseName, value }`. The convenience constructors use Effect's supported unstructured text and binary element schemas. + + ```rust @@ -692,81 +584,12 @@ There are three variants of multimodal values supported: - - ```scala - import golem.data.multimodal.{Multimodal, MultimodalItems} - import golem.data.unstructured.{AllowedLanguages, AllowedMimeTypes, BinarySegment, TextSegment} - import zio.blocks.schema.Schema - - import scala.concurrent.Future - - def exampleMultimodalMethod( - input: MultimodalItems.Basic - ): Future[Unit] = - Future.successful(()) - - def exampleMultimodalCustomMethod( - input: MultimodalItems.WithCustom[MyStructuredData] - ): Future[Unit] = - Future.successful(()) - - final case class MyStructuredData(field1: String, field2: Int) - object MyStructuredData { - implicit val schema: Schema[MyStructuredData] = Schema.derived - } - - def exampleMultimodalAdvancedMethod( - input: Multimodal[TextAndImage] - ): Future[Unit] = - Future.successful(()) - - final case class TextAndImage( - text: TextSegment[AllowedLanguages.Any], - image: BinarySegment[AllowedMimeTypes.Any] - ) - object TextAndImage { - implicit val schema: Schema[TextAndImage] = Schema.derived - } - ``` - - In Scala, runtime-sized multimodal lists are modeled by `MultimodalItems`, while `Multimodal[A]` lifts a fixed structured payload into a multimodal schema. - - Here are the relevant multimodal types in the Scala SDK: - - - ```scala - final case class Multimodal[A](value: A) - - sealed trait Modality[+A] - object Modality { - sealed trait Basic extends Modality[Nothing] - - final case class Text(value: UnstructuredTextValue) extends Basic - final case class Binary(value: UnstructuredBinaryValue) extends Basic - final case class Custom[A](value: A) extends Modality[A] - } - - final case class MultimodalItems[+A](items: List[A]) - - object MultimodalItems { - type Basic = MultimodalItems[Modality.Basic] - type WithCustom[A] = MultimodalItems[Modality[A]] - } - ``` - + The current Scala SDK does not expose the removed `Multimodal`, `MultimodalItems`, `TextSegment`, or `BinarySegment` APIs. Use supported structured schemas instead. ```moonbit - ///| - #derive.golem_schema - pub(all) struct MyStructuredData { - field1 : String - field2 : Int - } - - ///| #derive.multimodal pub(all) enum TextOrImage { Text(String) @@ -785,37 +608,16 @@ There are three variants of multimodal values supported: } ///| - pub fn MultimodalAgent::example_multimodal_method( - self : Self, - input : @types.Multimodal[@types.TextOrBinary], - ) -> Unit { - ignore(self) - ignore(input) - } - - ///| - pub fn MultimodalAgent::example_multimodal_custom_method( - self : Self, - input : @types.Multimodal[@types.CustomModality[MyStructuredData]], - ) -> Unit { - ignore(self) - ignore(input) - } - - ///| - pub fn MultimodalAgent::example_multimodal_advanced_method( + pub fn MultimodalAgent::analyze( self : Self, - input : @types.Multimodal[TextOrImage], + input : @multimodal.Multimodal[TextOrImage], ) -> Unit { ignore(self) ignore(input) } ``` - In MoonBit, all three variants use the same `@types.Multimodal[T]` container. - `@types.TextOrBinary` is the built-in basic modality set, `@types.CustomModality[T]` - is the equivalent of `MultimodalCustom`, and `#derive.multimodal` lets you define - a fully custom modality enum such as `TextOrImage`. + In MoonBit, define the complete modality enum with `#derive.multimodal`, then use it as `T` in `@multimodal.Multimodal[T]`. There are no current `@types.TextOrBinary` or `@types.CustomModality[T]` wrappers; model the exact supported cases in your enum. Here are the relevant multimodal types in the MoonBit SDK: @@ -824,17 +626,6 @@ There are three variants of multimodal values supported: pub(all) struct Multimodal[T] { items : Array[T] } derive(Show, Eq) - - pub(all) enum TextOrBinary { - Text(UnstructuredText) - Binary(UnstructuredBinary) - } derive(Show, Eq) - - pub(all) enum CustomModality[T] { - Text(UnstructuredText) - Binary(UnstructuredBinary) - Structured(T) - } derive(Show, Eq) ``` diff --git a/docs/src/styles/globals.css b/docs/src/styles/globals.css index 3df44d7706..c00dba4aa2 100644 --- a/docs/src/styles/globals.css +++ b/docs/src/styles/globals.css @@ -6,3 +6,7 @@ -webkit-font-smoothing: antialiased; min-width: 0; } + +[role="tablist"] > [role="tab"] { + flex-shrink: 0; +} diff --git a/golem-skills/skills/common/golem-edit-manifest/SKILL.md b/golem-skills/skills/common/golem-edit-manifest/SKILL.md index 71be7152ef..7701503fef 100644 --- a/golem-skills/skills/common/golem-edit-manifest/SKILL.md +++ b/golem-skills/skills/common/golem-edit-manifest/SKILL.md @@ -72,10 +72,10 @@ components: dir: billing # Base directory (relative to golem.yaml). Use "." for single-component apps templates: # Parent template names (inherit build, env, plugins, files) - rust - componentWasm: target/wasm32-wasip1/debug/billing.wasm # Path to built WASM + componentWasm: target/wasm32-wasip2/debug/billing.wasm # Path to built WASM outputWasm: golem-temp/billing.wasm # Path to final output WASM build: # Build commands (see Build Commands below) - - command: cargo build --target wasm32-wasip1 + - command: cargo build --target wasm32-wasip2 env: # Environment variables LOG_LEVEL: info tools: # Owner authorization; inherited by the component's agents @@ -130,7 +130,7 @@ Templates define reusable property layers. Components reference them via `templa componentTemplates: rust: build: - - command: cargo build --target wasm32-wasip1 + - command: cargo build --target wasm32-wasip2 env: RUST_LOG: info @@ -252,14 +252,14 @@ The `build` array contains commands executed during `golem build`. Each entry is ```yaml build: - - command: cargo build --target wasm32-wasip1 + - command: cargo build --target wasm32-wasip2 dir: . # Optional working directory env: # Optional extra env vars RUSTFLAGS: "-C opt-level=2" rmdirs: [target/old] # Directories to delete before running (runs before mkdirs) mkdirs: [target/new] # Directories to create before running (runs after rmdirs) sources: ["src/**/*.rs"] # Inputs for up-to-date checks - targets: ["target/wasm32-wasip1/debug/*.wasm"] # Outputs for up-to-date checks + targets: ["target/wasm32-wasip2/debug/*.wasm"] # Outputs for up-to-date checks ``` ### TypeScript/QuickJS-specific commands @@ -289,10 +289,10 @@ Define CLI commands at the application or component level: ```yaml customCommands: test: - - command: cargo test --target wasm32-wasip1 + - command: cargo test --target wasm32-wasip2 dir: . lint: - - command: cargo clippy --target wasm32-wasip1 + - command: cargo clippy --target wasm32-wasip2 ``` Run with `golem exec ` (e.g., `golem exec test`). @@ -323,8 +323,7 @@ environments: componentPresets: [release] # Preset names to activate cli: format: json - redeployAgents: true - reset: true + redeployAgents: true # Delete and recreate agents; agent state is lost deployment: compatibilityCheck: true versionCheck: true @@ -357,8 +356,10 @@ auth: |-------|-------------| | `format` | Default output: `text`, `json`, `yaml`, `pretty`, `pretty-json`, `pretty-yaml`, `toon` | | `autoConfirm` | Auto-confirm prompts (`true`) | -| `redeployAgents` | Redeploy agents by default (`true`) | -| `reset` | Reset agents by default (`true`) | +| `redeployAgents` | Equivalent to `--redeploy-agents`: delete and recreate agents; agent state is lost | +| `reset` | Equivalent to `--reset`: delete existing agents for the deployed components after deployment, losing their state, and enable incompatibility-replacement fallbacks; the environment itself is retained | + +Configure at most one destructive default. If both are enabled, `reset` takes precedence. When `format: toon` is used, structured stdout is emitted as framed TOON documents. Parse exact `@toon` and `@end` marker lines, and treat the content between them as one TOON document. Stderr may still contain progress or diagnostics and should not be parsed as the structured payload. @@ -615,7 +616,7 @@ resourceDefaults: enforcementAction: reject unit: byte units: bytes - - name: connections + connections: limit: type: Concurrency value: 50 diff --git a/golem-skills/skills/common/golem-profiles-and-environments/SKILL.md b/golem-skills/skills/common/golem-profiles-and-environments/SKILL.md index e97ab1614d..f2f77535d6 100644 --- a/golem-skills/skills/common/golem-profiles-and-environments/SKILL.md +++ b/golem-skills/skills/common/golem-profiles-and-environments/SKILL.md @@ -138,10 +138,13 @@ environments: cli: format: json # Default output format autoConfirm: true # Auto-answer "yes" to prompts - redeployAgents: true # Equivalent to --reset on deploy - reset: true # Reset all state on deploy + redeployAgents: true # Delete/recreate agents; agent state is lost ``` +`reset: true` is equivalent to `--reset`: it deletes existing agents for the deployed components +after deployment, losing their state, and enables incompatibility-replacement fallbacks. The +environment itself is retained. It takes precedence over `redeployAgents` when both are configured. + ### Deployment options (`deployment:`) ```yaml @@ -161,7 +164,7 @@ environments: | `-L` / `--local` | Select the `local` environment (or `local` profile if no manifest) | | `-C` / `--cloud` | Select the `cloud` environment (or `cloud` profile if no manifest) | | `-e ` | Select a named environment from the manifest | -| *(none)* | Use the `default: true` environment, or fall back to active profile | +| *(none)* | Use the explicitly default environment, otherwise the first declared manifest environment | ### Managing environments @@ -252,10 +255,10 @@ components: presets: local: build: - - command: cargo build --target wasm32-wasip1 + - command: cargo build --target wasm32-wasip2 release: build: - - command: cargo build --target wasm32-wasip1 --release + - command: cargo build --target wasm32-wasip2 --release agents: MyAgent: diff --git a/golem-skills/skills/effect/golem-call-from-external-effect/SKILL.md b/golem-skills/skills/effect/golem-call-from-external-effect/SKILL.md index 97dd73477a..77c420099f 100644 --- a/golem-skills/skills/effect/golem-call-from-external-effect/SKILL.md +++ b/golem-skills/skills/effect/golem-call-from-external-effect/SKILL.md @@ -17,7 +17,8 @@ standalone Node.js process. ## Steps 1. Ensure the Effect agent has the required TypeScript name and method contract, then build it. -2. Configure a `ts` bridge in `golem.yaml`; there is no separate `effect` bridge target. +2. Configure a `ts` external bridge in `golem.yaml` for the Promise-based client used below. An + `effect` bridge target also exists, but has a different Effect-native API. 3. Run `golem build` to regenerate the bridge from the built agent metadata. 4. Deploy the built application so the external client can reach the current agent definition. 5. Install and build the generated npm package. @@ -33,20 +34,20 @@ Add or extend the top-level `bridge` section in `golem.yaml`: ```yaml bridge: ts: - agents: - - CounterAgent - outputDir: ./bridge-sdk/ts/counter-agent-client + external: + agents: + - CounterAgent + outputDir: ./bridge-sdk/ts ``` `agents` accepts `"*"` or a list containing agent type names and component names (`namespace:name`). Preserve any existing bridge languages and selected agents. -For this Golem manifest version: +For this Promise-based client: -- use `bridge.ts.agents`, not `bridge.effect`; -- do not add an `external:` level under `ts`; -- a custom `outputDir` is the generated package directory itself, not a parent directory for all - generated clients; +- use `bridge.ts.external`; use `bridge.effect.external` only when the caller will consume the + Effect-native generated API; +- `outputDir` is the parent directory for generated clients; - without `outputDir`, `CounterAgent` is generated under `golem-temp/bridge-sdk/ts/counter-agent-client/`. @@ -84,7 +85,7 @@ do not guess them from the component source. Create the external application outside the Golem component source. Add the generated package as a file dependency and install Effect v4. Keep the `effect` version exactly aligned with the root -Effect component's `package.json` (the pinned SDK uses `4.0.0-beta.98`): +Effect component's `package.json` (the current SDK uses `4.0.0-rc.117`): ```json { @@ -97,7 +98,7 @@ Effect component's `package.json` (the pinned SDK uses `4.0.0-beta.98`): }, "dependencies": { "counter-agent-client": "file:../bridge-sdk/ts/counter-agent-client", - "effect": "4.0.0-beta.98" + "effect": "4.0.0-rc.117" }, "devDependencies": { "@types/node": "^25", @@ -231,7 +232,7 @@ For a durable agent, `get` creates or gets the instance identified by its agent const getAgent = Effect.tryPromise(() => MyAgent.get("my-instance")); ``` -Every generated agent supports phantom instances: +Durable agents support known and fresh phantom instances: ```typescript const phantomProgram = Effect.gen(function* () { @@ -245,14 +246,16 @@ const phantomProgram = Effect.gen(function* () { }); ``` -When the agent declares local configuration, the generated package also provides -`getWithConfig`, `getPhantomWithConfig`, and `newPhantomWithConfig`. Read their generated -declarations because configuration arguments follow the agent id values and reflect the -agent's exact config schema. +For ephemeral agents, use `newPhantom`; they do not expose durable `getPhantom` constructors. When +a durable agent declares local configuration, the generated package provides `getWithConfig`, +`getPhantomWithConfig`, and `newPhantomWithConfig`. An ephemeral agent with local configuration +provides `newPhantomWithConfig`. Read the generated declarations because configuration arguments +reflect the agent's exact schema. -Generated remote methods are callable Promises for invoke-and-await. They also expose -`abortable(signal, ...args)`, `trigger(...args)`, and `schedule(isoTimestamp, ...args)`. The latter -two return `void`; do not wrap them as if they awaited a result. +Generated remote methods are callable Promises for invoke-and-await and expose +`abortable(signal, ...args)`. Durable non-streaming methods also expose `trigger(...args)` and +`schedule(isoTimestamp, ...args)`, which return `void`. Ephemeral trigger/schedule operations return +`Promise`. Streaming methods do not expose trigger or schedule variants. ## CLI Values for Effect Components @@ -268,7 +271,8 @@ checks, but the external application should use the generated bridge for typed c ## Key Constraints -- Generate `bridge.ts`; there is no Effect-specific external bridge target. +- Generate `bridge.ts.external` for the Promise-based adapter shown here; an Effect-specific + external target also exists and exposes a different API. - Use the generated `configure` function and generated class declarations as the source of truth. - Wrap generated Promise calls in deferred `Effect.tryPromise` thunks. - Yield stateful calls sequentially when order matters. diff --git a/golem-skills/skills/effect/golem-tools-middleware-effect/SKILL.md b/golem-skills/skills/effect/golem-tools-middleware-effect/SKILL.md index d525fd7119..32362aedfb 100644 --- a/golem-skills/skills/effect/golem-tools-middleware-effect/SKILL.md +++ b/golem-skills/skills/effect/golem-tools-middleware-effect/SKILL.md @@ -7,6 +7,6 @@ description: Defines and calls Golem tools and attaches Effect-native typed or u Build a definition with `Tool.toolDefinition(name).body(...)`. A provider finishes it with `.implement({ camelCaseName: handler })`; a caller uses `Tool.client(definition)`. Handlers and clients return Effects and stream stdin/stdout with Effect `Stream`. -Use `Middleware.typed({ name, presented, handler })` when the presented tool shape is known. Use the universal middleware API only when every tool must be intercepted. Forward input, output, permission cards, and streams exactly once to `underlying`; capability handles are affine. +Use `Middleware.typed({ name, parameters: Middleware.NoParameters, presented, handler })` when the presented tool shape is known. Use the universal middleware API only when every tool must be intercepted. Forward input, output, permission cards, and streams at most once; capability handles are affine. The default world supports ordinary, standalone-middleware, and combined components. Standalone middleware can be attached and deployed independently; unused agent and tool discovery returns empty lists. diff --git a/golem-skills/skills/moonbit/golem-add-http-endpoint-moonbit/SKILL.md b/golem-skills/skills/moonbit/golem-add-http-endpoint-moonbit/SKILL.md index 501134380e..b39ed58d88 100644 --- a/golem-skills/skills/moonbit/golem-add-http-endpoint-moonbit/SKILL.md +++ b/golem-skills/skills/moonbit/golem-add-http-endpoint-moonbit/SKILL.md @@ -209,7 +209,10 @@ Golem maps method return types to HTTP status codes and response bodies accordin | `T?` (`Option[T]`) | 200 OK if `Some`, 404 Not Found if `None` | JSON `T` or empty | | `Result[T, E]` | 200 OK if `Ok`, 500 Internal Server Error if `Err` | JSON `T` or JSON `E` | | `Result[Unit, E]` | 204 No Content if `Ok`, 500 if `Err` | empty or JSON `E` | -| `UnstructuredBinary` | 200 OK | Raw binary with Content-Type | + +The current MoonBit SDK has no high-level `UnstructuredText` or `UnstructuredBinary` endpoint +types. Endpoint values use the supported schema types and JSON mapping described by +`golem-http-params-moonbit`; do not add the removed wrappers or their old derive attributes. ## Complete Example @@ -230,7 +233,7 @@ pub(all) struct Task { id : String title : String priority : Priority - done : Bool + mut done : Bool } derive(ToJson, @json.FromJson) ///| diff --git a/golem-skills/skills/moonbit/golem-add-moonbit-package/SKILL.md b/golem-skills/skills/moonbit/golem-add-moonbit-package/SKILL.md index 9c6dac8d86..6aff82bdd3 100644 --- a/golem-skills/skills/moonbit/golem-add-moonbit-package/SKILL.md +++ b/golem-skills/skills/moonbit/golem-add-moonbit-package/SKILL.md @@ -1,61 +1,63 @@ --- name: golem-add-moonbit-package -description: "Adding MoonBit package dependencies to a Golem project. Use when the user asks to add a mooncakes dependency, library, or package to a MoonBit project." +description: "Adds a MoonBit module and package import to a Golem project. Use when adding a mooncakes dependency or importing one of its packages." --- -# Adding a MoonBit Package Dependency +# Add a MoonBit dependency -## Overview +Current MoonBit projects declare module dependencies in `moon.mod` and package imports in +`moon.pkg`. The JSON files `moon.mod.json` and `moon.pkg.json` are legacy formats; do not create +them for application source. -MoonBit projects manage dependencies through the `moon.mod.json` file at the project root. Dependencies are published on [mooncakes.io](https://mooncakes.io) and installed with the `moon` CLI. +## Add the module -## Steps +Use the package manager so it selects a version and updates `moon.mod`: -1. **Edit `moon.mod.json`** — add the package to the `"deps"` section -2. **Run `moon install`** — download and install the dependency -3. **Use the package** — import it in your `.mbt` files +```shell +moon add example/json-utils +``` -## Adding a Dependency +The resulting module declaration has this shape: -Edit `moon.mod.json` and add the package under the `"deps"` object with a version constraint: +```moonbit +name = "my-org/my-project" -```json -{ - "name": "my-org/my-project", - "version": "0.1.0", - "deps": { - "example/json-utils": "0.2.0" - } +import { + "example/json-utils@0.2.0", } ``` -Then install: - -```shell -moon install -``` +Use `moon add --upgrade example/json-utils` to update an existing dependency. `moon check` and +`moon build` fetch declared dependencies automatically; the old advice to run `moon install` for +project dependencies no longer applies. -## Version Constraints +## Import the package -Specify the version as a string in `moon.mod.json`. Use the exact version published on mooncakes.io. +Module dependencies make packages available, but each source package must explicitly import the +package it uses in `moon.pkg`: -## Using the Dependency +```moonbit +import { + "example/json-utils/parser" @json_parser, +} +``` -After installation, reference the package in your `moon.pkg.json` file's `"import"` section and use it in your `.mbt` source files. +Code can then call that package through `@json_parser`. -In `moon.pkg.json`: +For local modules, use a `moon.work` workspace and list each module directory as a member; local +path dependencies in the new `moon.mod` format are deprecated. -```json -{ - "import": [ - "example/json-utils" - ] -} -``` +Golem's stock MoonBit build template currently assumes a single-module artifact path. Adding +workspace members changes Moon's output path, so an unchanged `golem build` will not find the +component WASM. A Golem component that uses `moon.work` must override its debug and release build, +embed-source, and `componentWasm` paths. Do not present a workspace dependency as a drop-in change +to a generated Golem application. -## Key Constraints +Golem-generated MoonBit bridge modules are a deliberate exception: the bridge generator currently +emits self-contained modules with `moon.mod.json`. Do not rename or edit that generated file. Put +the application and generated module in `moon.work`, then import the generated module by its module +name from the application's `moon.mod` and import its packages from `moon.pkg`. -- Only packages published on [mooncakes.io](https://mooncakes.io) can be added as dependencies -- Ensure the package is compatible with the `wasm` / `wasm-gc` backend — some MoonBit packages may only support native or JS targets -- After adding a dependency, always run `moon install` before `golem build` -- Check the package's documentation on mooncakes.io for usage examples and API reference +For an ordinary registry dependency, run `moon check` and `golem build --yes`. Confirm that the +dependency supports the component's `wasm` target; native-only and JavaScript-only packages cannot +be linked into a Golem agent. diff --git a/golem-skills/skills/moonbit/golem-call-another-agent-moonbit/SKILL.md b/golem-skills/skills/moonbit/golem-call-another-agent-moonbit/SKILL.md index 0aaa08cc1f..851ec6cbf1 100644 --- a/golem-skills/skills/moonbit/golem-call-another-agent-moonbit/SKILL.md +++ b/golem-skills/skills/moonbit/golem-call-another-agent-moonbit/SKILL.md @@ -11,10 +11,12 @@ The `#derive.agent` code generation tool auto-generates a `Client` st ## Getting a Client (Scoped) -Use `Client::scoped(...)` with the target agent's constructor parameters and a callback. The client is automatically dropped when the callback returns: +Use `Client::scoped(...)` with the target agent's constructor parameters and an async +callback. Call it from an `async fn`; MoonBit has no `await` keyword. The client is automatically +dropped when the callback returns: ```moonbit -CounterClient::scoped("my-counter", fn(counter) raise @common.AgentError { +CounterClient::scoped("my-counter", async fn(counter) { counter.increment() counter.increment() let value = counter.get_value() @@ -44,7 +46,7 @@ This does **not** create the agent — the agent is created implicitly on its fi Call a method and block until the result returns: ```moonbit -CounterClient::scoped("my-counter", fn(counter) raise @common.AgentError { +CounterClient::scoped("my-counter", async fn(counter) { counter.increment() let count = counter.get_value() count @@ -58,7 +60,7 @@ The calling agent **blocks** until the target agent processes the request and re Agent methods accept custom types defined in your agent code: ```moonbit -TaskManagerClient::scoped(fn(tm) raise @common.AgentError { +TaskManagerClient::scoped(async fn(tm) { let count = tm.add_task({ title: "Build RPC support", priority: High, @@ -93,22 +95,19 @@ components: - example:weather/WeatherAgent ``` -`golem build` generates `golem-temp/bridge-sdk/moonbit/internal/weather-agent-guest-client`. Add it as a local module dependency in the application's `moon.mod.json`: - -```json -{ - "name": "example/weather-app", - "preferred-target": "wasm", - "deps": { - "golemcloud/golem_sdk": "0.5.1", - "weather-agent-guest-client": { - "path": "golem-temp/bridge-sdk/moonbit/internal/weather-agent-guest-client" - } - } -} -``` +`golem build` generates `golem-temp/bridge-sdk/moonbit/internal/weather-agent-guest-client`. The +generated module intentionally still contains `moon.mod.json`; do not rename or edit it. + +An application using current `moon.mod` would normally put that generated module and the +application in `moon.work`, import `weather-agent-guest-client@0.0.1` from `moon.mod`, and import its +client package from `moon.pkg`. This is **not currently a drop-in Golem build setup**: multiple +workspace members add the module name to Moon's output path, while Golem's stock MoonBit build +template embeds the single-module path. Use this bridge only after providing workspace-aware debug +and release build/embed overrides in `golem.yaml`; otherwise the build will not find the component +WASM. Do not revert the application to legacy `moon.mod.json` merely to add a path dependency. -Import its client package from the caller's `moon.pkg`: +With that custom build integration in place, import the generated client package from the caller's +`moon.pkg`: ```moonbit import { diff --git a/golem-skills/skills/moonbit/golem-call-from-external-moonbit/SKILL.md b/golem-skills/skills/moonbit/golem-call-from-external-moonbit/SKILL.md index 6028857fd7..1c1a3b21fe 100644 --- a/golem-skills/skills/moonbit/golem-call-from-external-moonbit/SKILL.md +++ b/golem-skills/skills/moonbit/golem-call-from-external-moonbit/SKILL.md @@ -37,32 +37,44 @@ The recommended approach is to declare the bridge in `golem.yaml` (as shown abov golem build --yes ``` -This produces a MoonBit module per agent type (e.g., `my-agent-client/`) in the configured output directory (or `golem-temp/bridge-sdk/moonbit/` by default). Re-running `golem build` after agent changes keeps the generated client in sync automatically. +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 as a Local Dependency +## Step 3: Add the Generated Module to a Workspace -The generated `my-agent-client/` directory is a self-contained `moon` module. Its `moon.mod.json` declares an agent-derived module name matching the generated directory name (for example, `my-agent-client`), so multiple generated bridge modules can be used from the same external project. The module bundles its own runtime, only depends on `moonbitlang/async` and the MoonBit core library, and builds for the `native` target. +The generated `my-agent-client/` directory is a self-contained module. The generator deliberately +emits its metadata as `moon.mod.json`; do not rename or edit that generated file. The application +itself should use the current `moon.mod` format. -Add it to your external MoonBit project as a local path dependency in `moon.mod.json`. Also depend on `moonbitlang/async` directly — your own `async fn main` entry point needs it imported (see the next step), and the version must match the one the generated module pins (`0.21.2`): +Create the external application in `external-client/`. Because local path dependencies in +`moon.mod` are deprecated, add `external-client/moon.work` with both modules: -```json -{ - "name": "my-org/external-client", - "preferred-target": "native", - "deps": { - "my-agent-client": { - "path": "../golem-temp/bridge-sdk/moonbit/my-agent-client" - }, - "moonbitlang/async": "0.21.2" - } -} +```moonbit +members = [ + ".", + "../bridge-sdk/moonbit/my-agent-client", +] ``` -The `path` points to the directory containing the generated module's `moon.mod.json`. Since the client uses the native HTTP transport, `preferred-target` is set to `native` for your standalone app. +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.json`: + +```moonbit +name = "my-org/external-client" +preferred_target = "native" + +import { + "my-agent-client@0.0.1", + "moonbitlang/async@0.21.2", +} +``` -Then run `moon install`. +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 @@ -181,7 +193,7 @@ let reply = agent.analyze([ ## Unstructured Text and Binary -Rich `text` and `binary` parameters and return values map to the ergonomic +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: @@ -195,7 +207,9 @@ which is either inline content or a URL reference: @runtime.BinaryUrl("https://example.com/cat.png") ``` -When the agent restricts the allowed language codes or MIME types, the generated +These wrappers do not represent arbitrary bare text or binary schema values. Bare binary maps to +`@runtime.AgentBinary`, and bare text is not supported by this bridge mapping. 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. @@ -213,4 +227,5 @@ Each agent type gets its own `moon` module directory containing: - 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 -- Add the generated module as a local path dependency and run `moon install` before using it +- Keep application metadata in `moon.mod`; leave the generated bridge's `moon.mod.json` unchanged +- Resolve the generated module through `moon.work`, then run `moon check --target native` diff --git a/golem-skills/skills/moonbit/golem-call-tool-moonbit/SKILL.md b/golem-skills/skills/moonbit/golem-call-tool-moonbit/SKILL.md new file mode 100644 index 0000000000..17e2459020 --- /dev/null +++ b/golem-skills/skills/moonbit/golem-call-tool-moonbit/SKILL.md @@ -0,0 +1,21 @@ +--- +name: golem-call-tool-moonbit +description: "Calls a generated typed Golem tool client from MoonBit. Use when invoking a tool provider and handling typed tool errors." +--- + +# Call a Golem tool from MoonBit + +`#derive.tool` generates `Client` in `golem_tool_clients.mbt`: + +```moonbit +async fn call_echo() -> Result[String, @tool.ToolError[@tool.NoToolError]] { + let client = EchoClient::new() + defer client.drop() + client.echo("hello") +} +``` + +Call generated tool methods from an `async fn`; MoonBit has no `await` keyword. +Use `new_for(name)` when targeting another registration name. Generated signatures preserve custom +errors and stream capabilities; handle the returned `@tool.ToolError[E]` rather than assuming every +failure is a domain error. Always call `drop()` when finished, and never edit the generated client. diff --git a/golem-skills/skills/moonbit/golem-define-tool-moonbit/SKILL.md b/golem-skills/skills/moonbit/golem-define-tool-moonbit/SKILL.md new file mode 100644 index 0000000000..090b3f0134 --- /dev/null +++ b/golem-skills/skills/moonbit/golem-define-tool-moonbit/SKILL.md @@ -0,0 +1,25 @@ +--- +name: golem-define-tool-moonbit +description: "Defines a typed Golem tool provider in MoonBit. Use when creating tool commands, metadata, arguments, or errors." +--- + +# Define a Golem tool in MoonBit + +Annotate an empty struct with `#derive.tool` and implement commands as public static methods: + +```moonbit +/// Echo text. +#derive.tool("echo", version="1.0.0") +struct Echo {} + +/// Echo one value. +#derive.arg("value", scope="positional") +pub fn Echo::echo(value : String) -> String { + value +} +``` + +Use `#derive.command`, `#derive.arg`, `#derive.constraint`, `#derive.result`, and `#derive.error` +for explicit command metadata. Custom schema values need `#derive.golem_schema`. `golem build` +generates registration and typed clients; never edit `golem_tool_clients.mbt` or other generated +files. diff --git a/golem-skills/skills/moonbit/golem-http-params-moonbit/SKILL.md b/golem-skills/skills/moonbit/golem-http-params-moonbit/SKILL.md index 4d8efe7007..420f23adfc 100644 --- a/golem-skills/skills/moonbit/golem-http-params-moonbit/SKILL.md +++ b/golem-skills/skills/moonbit/golem-http-params-moonbit/SKILL.md @@ -116,97 +116,12 @@ Each unmapped parameter becomes a top-level field in the expected JSON body obje > **⚠️ Important:** The request body is **always** a JSON object with parameter names as keys — even when there is only a single body parameter. For example, an endpoint `decide(self : Self, decision : String)` expects `{"decision": "approved"}`, **never** a bare string like `"approved"`. Sending a non-object JSON value or plain text will fail with `REQUEST_JSON_BODY_PARSING_FAILED`. -## Binary Request and Response Bodies +## Body Format -Use `UnstructuredBinary` from the SDK for raw binary payloads: - -```moonbit -///| -/// Accepting any binary content type -#derive.endpoint(post="/upload/{bucket}") -pub fn TaskAgent::upload(self : Self, bucket : String, payload : UnstructuredBinary) -> Int64 { - match payload { - Url(_) => -1L - Inline(data~, mime_type~) => data.length().to_int64() - } -} - -///| -/// Restricting to specific MIME types -#derive.endpoint(post="/upload-image/{bucket}") -#derive.mime_types("payload", "image/png", "image/jpeg") -pub fn TaskAgent::upload_image(self : Self, bucket : String, payload : UnstructuredBinary) -> Int64 { - match payload { - Url(_) => -1L - Inline(data~, mime_type~) => data.length().to_int64() - } -} - -///| -/// Returning binary data -#derive.endpoint(get="/download") -pub fn TaskAgent::download(self : Self) -> UnstructuredBinary { - UnstructuredBinary::from_inline(b"\x01\x02\x03\x04", mime_type="application/octet-stream") -} -``` - -`UnstructuredBinary` parameters cannot be bound to path, query, or header variables. - -## Plain Text Request and Response Bodies - -Use `UnstructuredText` from the SDK for raw `text/plain` payloads. Like -`UnstructuredBinary`, a method using `UnstructuredText` may have **only one -body parameter**, and that parameter cannot be bound to a path/query/header/mount -variable. The body is decoded as UTF-8. - -```moonbit -///| -/// Accepting any text/plain content -#derive.endpoint(post="/notes/{id}") -pub fn TaskAgent::add_note(self : Self, id : String, body : UnstructuredText) -> UInt64 { - match body { - Url(_) => 0UL - Inline(data~, language_code~) => data.length().to_uint64() - } -} - -///| -/// Restricting to specific language codes -#derive.endpoint(post="/translate/{id}") -#derive.text_languages("body", "en", "de") -pub fn TaskAgent::translate(self : Self, id : String, body : UnstructuredText) -> String { - match body { - Url(_) => "" - Inline(data~, ..) => data - } -} - -///| -/// Returning text/plain -#derive.endpoint(get="/notes/{id}") -pub fn TaskAgent::get_note(self : Self, id : String) -> UnstructuredText { - UnstructuredText::from_inline("hello", language_code=Some("en")) -} -``` - -HTTP-level rules: -- The request must have either no `Content-Type`, `text/plain`, or - `text/plain; charset=utf-8` (case-insensitive). Any other content type is - rejected with `415 Unsupported Media Type`. -- `Content-Language` is **always optional**, even when language codes are - restricted via `#derive.text_languages`. If present, it must be a single - value (multi-valued or comma-separated headers are rejected with - `400 Bad Request`). -- When restricted, the supplied `Content-Language` is matched - case-insensitively against the allowed list; otherwise `415 Unsupported Media Type`. -- A non-UTF-8 request body is rejected with `400 Bad Request`. -- `Content-Language` cannot also be bound as an endpoint header parameter when - the body is `UnstructuredText` — that header is reserved for declaring the - body language. - -The response is sent as `Content-Type: text/plain; charset=utf-8`. If the -returned `UnstructuredText` (inline form) carries a language code, it is -forwarded as the `Content-Language` response header. +The current MoonBit SDK maps endpoint bodies through schema-backed JSON values. It does not expose +the removed `@types.UnstructuredText` or `@types.UnstructuredBinary` wrappers, and +`#derive.text_languages` and `#derive.mime_types` no longer exist. Do not use those old APIs for +raw `text/plain` or binary request and response bodies. ## Return Type to HTTP Response Mapping @@ -217,8 +132,6 @@ forwarded as the `Content-Language` response header. | `T?` (`Option[T]`) | 200 OK if `Some`, 404 Not Found if `None` | JSON `T` or empty | | `Result[T, E]` | 200 OK if `Ok`, 500 Internal Server Error if `Err` | JSON `T` or JSON `E` | | `Result[Unit, E]` | 204 No Content if `Ok`, 500 if `Err` | empty or JSON `E` | -| `UnstructuredBinary` | 200 OK | Raw binary with Content-Type | -| `UnstructuredText` | 200 OK | `text/plain; charset=utf-8` (+ optional `Content-Language`) | ## Data Type to JSON Mapping diff --git a/golem-skills/skills/moonbit/golem-permission-card-moonbit/SKILL.md b/golem-skills/skills/moonbit/golem-permission-card-moonbit/SKILL.md new file mode 100644 index 0000000000..0227d788fb --- /dev/null +++ b/golem-skills/skills/moonbit/golem-permission-card-moonbit/SKILL.md @@ -0,0 +1,28 @@ +--- +name: golem-permission-card-moonbit +description: "Transfers opaque permission-card handles through MoonBit schema values. Use when delegated authority crosses a dynamic tool or middleware boundary." +--- + +# Permission cards in MoonBit + +Import the low-level schema model and core WIT types in `moon.pkg`: + +```moonbit +import { + "golemcloud/golem_sdk/schema_model" @model, + "golemcloud/golem_sdk/interface/golem/core/types" @types, +} +``` + +The carrier is `@model.GuestPermissionCardHandle`. It wraps a runtime-provided +`@types.PermissionCard` and can be placed in `@model.SchemaValue::PermissionCard` with a matching +`@model.SchemaTypeBody::PermissionCard(@types.PermissionCardSpec)`. + +Use `is_present()` to check whether ownership remains and `take()` only when transferring the raw +resource to a host API. `with_handle(...)` borrows it without transfer. Do not construct a fake raw +card, persist it, compare its contents, or call `take()` twice. + +The current high-level MoonBit agent/tool derives do not expose a permission-card field annotation +equivalent to TypeScript's `s.permissionCard`. Use this carrier only in APIs that already operate on +raw schema values, such as universal tool middleware; do not fabricate a high-level derived method +signature. diff --git a/golem-skills/skills/moonbit/golem-tools-middleware-moonbit/SKILL.md b/golem-skills/skills/moonbit/golem-tools-middleware-moonbit/SKILL.md new file mode 100644 index 0000000000..e9e382a0ae --- /dev/null +++ b/golem-skills/skills/moonbit/golem-tools-middleware-moonbit/SKILL.md @@ -0,0 +1,29 @@ +--- +name: golem-tools-middleware-moonbit +description: "Defines typed or universal Golem tool middleware in MoonBit. Use for validation, policy, auditing, or tool adapters." +--- + +# Tool middleware in MoonBit + +For typed middleware, annotate an empty struct and implement async methods whose first parameter is +the generated underlying. `Echo` must be a same-package `#derive.tool` declaration; see +`golem-define-tool-moonbit`. + +```moonbit +#derive.tool_middleware("echo-policy", presented="Echo") +struct EchoPolicy {} + +pub async fn EchoPolicy::echo( + underlying : EchoUnderlying, + value : String, +) -> Result[String, @toolMiddleware.ToolInvokeError[@tool.NoToolError]] { + underlying.echo(value) +} +``` + +Set `expected="OtherTool"` for an adapter and translate its values and errors. Use +`#derive.universal_tool_middleware` on an async free function only when arbitrary tool metadata and +raw carriers are required. MoonBit async functions use no `await` keyword. The underlying tool is +valid only during this middleware invocation and may be called zero, one, or multiple times. Raw +input/result carriers and owned stream or permission-card handles are take-once: do not reuse a +transferred carrier or let invocation-scoped resources escape. diff --git a/golem-skills/skills/rust/golem-add-llm-rust/SKILL.md b/golem-skills/skills/rust/golem-add-llm-rust/SKILL.md index 44b36110bd..b9d2b49d73 100644 --- a/golem-skills/skills/rust/golem-add-llm-rust/SKILL.md +++ b/golem-skills/skills/rust/golem-add-llm-rust/SKILL.md @@ -1,363 +1,51 @@ --- name: golem-add-llm-rust -description: "Adding LLM and AI capabilities to a Rust Golem agent. Use when the user wants to add LLM chat, embeddings, web search, vector DB, graph DB, document search, video generation, speech-to-text, text-to-speech, or any AI provider integration." +description: "Adds provider-backed AI capabilities to a Rust Golem agent. Use for LLMs, embeddings, search, vector or graph databases, speech, video, or code execution." --- -# Adding LLM and AI Capabilities (Rust) +# Add AI capabilities to a Rust agent -## Overview +The `golemcloud/golem-ai` project provides provider-neutral core crates and provider crates. Golem +agents now target `wasm32-wasip2` and WASI HTTP 0.3, so dependency selection matters. -Golem provides the **golem-ai** library collection — a set of Rust crates from [golemcloud/golem-ai](https://github.com/golemcloud/golem-ai) that provide unified, provider-agnostic APIs for AI capabilities. Each domain has a **core crate** (shared types and traits) plus **provider crates** (concrete backends). You add them as regular Cargo dependencies and call them directly from your agent code. +## Compatibility status -> **These crates are not on crates.io yet.** Use git dependencies pointing to the `v0.5.1` tag. +There is currently no verified `golem-ai` release or revision compatible with this repository's +Rust SDK. The latest crates.io release, `0.5.2`, predates the WASIp2/WASI P3 migration. The current +upstream source targets `wasm32-wasip2`, but still expects the older infallible Golem secret API and +does not compile against this SDK with its default Golem integration enabled. -## Available Libraries +Do not add `0.5.2`, pin the upstream migration commit, disable durability features, or copy an old +provider example merely to make the dependency resolve. Recheck upstream releases and build the +chosen version in a current scaffold before documenting or shipping it. Keep every selected core +and provider crate on the same verified release or revision. -### LLM (Chat Completions) +## Available crate families -Core crate: `golem-ai-llm` — unified chat completion API with blocking and streaming responses, multi-turn conversation, tool calling, and multimodal image inputs. +Choose one core crate and the provider crate needed by the application: -Provider crates (pick one): +| Capability | Core crate | Providers | +|---|---|---| +| LLM chat | `golem-ai-llm` | Anthropic, Bedrock, Grok, Ollama, OpenAI, OpenRouter | +| Embeddings/reranking | `golem-ai-embed` | Cohere, Hugging Face, OpenAI, VoyageAI | +| Web search | `golem-ai-web-search` | Brave, Google, Serper, Tavily | +| Document search | `golem-ai-search` | Algolia, Elasticsearch, Meilisearch, OpenSearch, Typesense | +| Graph database | `golem-ai-graph` | ArangoDB, JanusGraph, Neo4j | +| Vector database | `golem-ai-vector` | Milvus, pgvector, Pinecone, Qdrant | +| Video | `golem-ai-video` | Kling, Runway, Stability, Veo | +| Speech-to-text | `golem-ai-stt` | AWS, Azure, Deepgram, Google, Whisper | +| Text-to-speech | `golem-ai-tts` | AWS, Deepgram, ElevenLabs, Google | +| Code execution | `golem-ai-exec` | JavaScript and Python execution | -| Provider | Crate | Required Env Vars | -|----------|-------|-------------------| -| OpenAI | `golem-ai-llm-openai` | `OPENAI_API_KEY` | -| Anthropic | `golem-ai-llm-anthropic` | `ANTHROPIC_API_KEY` | -| Amazon Bedrock | `golem-ai-llm-bedrock` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION` | -| xAI / Grok | `golem-ai-llm-grok` | `XAI_API_KEY` | -| Ollama | `golem-ai-llm-ollama` | `GOLEM_OLLAMA_BASE_URL` (optional, defaults to `http://localhost:11434`) | -| OpenRouter | `golem-ai-llm-openrouter` | `OPENROUTER_API_KEY` | +The repository also contains `golem-ai-http`, the shared WASI HTTP transport. Provider crate names +follow `golem-ai--`. -### Embeddings & Reranking +## Safe alternative -Core crate: `golem-ai-embed` — generate vector embeddings from text/images and rerank documents by relevance. +Until a compatible release exists, call a provider's HTTPS API with the supported outgoing HTTP +client described by `golem-make-http-request-rust`. Store credentials with Golem secrets; load +`golem-add-secret-rust` for provisioning. External calls made through supported host APIs remain +durable, while a generic third-party HTTP client may not integrate correctly with replay. -Provider crates: - -| Provider | Crate | Required Env Vars | -|----------|-------|-------------------| -| OpenAI | `golem-ai-embed-openai` | `OPENAI_API_KEY` | -| Cohere | `golem-ai-embed-cohere` | `COHERE_API_KEY` | -| Hugging Face | `golem-ai-embed-hugging-face` | `HUGGING_FACE_API_KEY` | -| VoyageAI | `golem-ai-embed-voyageai` | `VOYAGEAI_API_KEY` | - -### Web Search - -Core crate: `golem-ai-web-search` — unified web search with one-shot and paginated session modes. - -Provider crates: - -| Provider | Crate | Required Env Vars | -|----------|-------|-------------------| -| Brave | `golem-ai-web-search-brave` | `BRAVE_API_KEY` | -| Google | `golem-ai-web-search-google` | `GOOGLE_API_KEY`, `GOOGLE_SEARCH_ENGINE_ID` | -| Serper | `golem-ai-web-search-serper` | `SERPER_API_KEY` | -| Tavily | `golem-ai-web-search-tavily` | `TAVILY_API_KEY` | - -### Document Search - -Core crate: `golem-ai-search` — full-text/document search with index management, document CRUD, faceted search. - -Provider crates: - -| Provider | Crate | Required Env Vars | -|----------|-------|-------------------| -| Algolia | `golem-ai-search-algolia` | `ALGOLIA_APPLICATION_ID`, `ALGOLIA_API_KEY` | -| Elasticsearch | `golem-ai-search-elasticsearch` | `ELASTICSEARCH_URL`, credentials | -| Meilisearch | `golem-ai-search-meilisearch` | `MEILISEARCH_BASE_URL`, `MEILISEARCH_API_KEY` | -| OpenSearch | `golem-ai-search-opensearch` | `OPENSEARCH_BASE_URL`, credentials | -| Typesense | `golem-ai-search-typesense` | `TYPESENSE_BASE_URL`, `TYPESENSE_API_KEY` | - -### Graph Databases - -Core crate: `golem-ai-graph` — vertex/edge CRUD, traversal, path-finding, transactions, schema management. - -Provider crates: - -| Provider | Crate | -|----------|-------| -| ArangoDB | `golem-ai-graph-arangodb` | -| JanusGraph | `golem-ai-graph-janusgraph` | -| Neo4j | `golem-ai-graph-neo4j` | - -### Vector Databases - -Core crate: `golem-ai-vector` — collection management, vector upsert/search, ANN queries, namespaces. - -Provider crates: - -| Provider | Crate | -|----------|-------| -| Qdrant | `golem-ai-vector-qdrant` | -| Milvus | `golem-ai-vector-milvus` | -| PgVector | `golem-ai-vector-pgvector` | -| Pinecone | `golem-ai-vector-pinecone` | - -### Video Generation - -Core crate: `golem-ai-video` — text-to-video, image-to-video, async job polling. - -Provider crates: - -| Provider | Crate | -|----------|-------| -| Google Veo | `golem-ai-video-veo` | -| Stability AI | `golem-ai-video-stability` | -| Kling | `golem-ai-video-kling` | -| Runway ML | `golem-ai-video-runway` | - -### Speech-to-Text - -Core crate: `golem-ai-stt` — audio transcription with speaker diarization, word-level timing. - -Provider crates: - -| Provider | Crate | -|----------|-------| -| OpenAI Whisper | `golem-ai-stt-whisper` | -| Deepgram | `golem-ai-stt-deepgram` | -| AWS Transcribe | `golem-ai-stt-aws` | -| Azure Speech | `golem-ai-stt-azure` | -| Google STT | `golem-ai-stt-google` | - -### Text-to-Speech - -Core crate: `golem-ai-tts` — voice discovery, batch/streaming synthesis, SSML support. - -Provider crates: - -| Provider | Crate | -|----------|-------| -| AWS Polly | `golem-ai-tts-aws` | -| Deepgram | `golem-ai-tts-deepgram` | -| ElevenLabs | `golem-ai-tts-elevenlabs` | -| Google Cloud TTS | `golem-ai-tts-google` | - -## Adding Dependencies - -Add the core crate plus your chosen provider to the component's `Cargo.toml`: - -```toml -[dependencies] -# LLM — core + provider -golem-ai-llm = "0.5.1" -golem-ai-llm-openai = "0.5.1" -``` - -Store the required API key as a **secret** using Golem's typed config system. Load the `golem-add-secret-rust` skill for full details. In brief: - -```rust -use golem_rust::ConfigSchema; -use golem_rust::agentic::{Config, Secret}; - -#[derive(ConfigSchema)] -pub struct MyAgentConfig { - #[config_schema(secret)] - pub api_key: Secret, -} -``` - -Then manage the secret via the CLI: - -```shell -golem secret create api_key --secret-type String --secret-value "sk-..." -``` - -## Usage: LLM Chat Completion - -```rust -use golem_ai_llm::model::*; -use golem_ai_llm::LlmProvider; - -// Pick a provider — type alias makes it easy to swap later -type Provider = golem_ai_llm_openai::DurableOpenAI; - -let config = Config { - model: "gpt-4o".to_string(), - temperature: None, - max_tokens: None, - stop_sequences: None, - tools: None, - tool_choice: None, - provider_options: None, -}; - -let events = vec![Event::Message(Message { - role: Role::User, - name: None, - content: vec![ContentPart::Text("Hello!".to_string())], -})]; - -// Blocking request -let response = Provider::send(events, config).expect("LLM call failed"); - -// Extract text from response -let text: String = response - .content - .iter() - .filter_map(|part| match part { - ContentPart::Text(txt) => Some(txt.clone()), - _ => None, - }) - .collect::>() - .join("\n"); -``` - -## Usage: Multi-turn Conversation (Session) - -```rust -use golem_ai_llm::model::*; -use golem_ai_llm::LlmProvider; - -type Provider = golem_ai_llm_openai::DurableOpenAI; - -// Keep events as agent state for multi-turn conversation -let mut events: Vec = vec![]; - -// Add system message -events.push(Event::Message(Message { - role: Role::System, - name: None, - content: vec![ContentPart::Text("You are a helpful assistant.".to_string())], -})); - -// Add user message -events.push(Event::Message(Message { - role: Role::User, - name: None, - content: vec![ContentPart::Text("What is Golem?".to_string())], -})); - -let config = Config { - model: "gpt-4o".to_string(), - temperature: None, - max_tokens: None, - stop_sequences: None, - tools: None, - tool_choice: None, - provider_options: None, -}; - -// Send and record the response for next turn -let response = Provider::send(events.clone(), config.clone()).expect("LLM call failed"); -events.push(Event::Response(response)); -``` - -## Usage: Web Search - -```rust -use golem_ai_web_search::model::types; -use golem_ai_web_search::model::web_search; - -type SearchProvider = golem_ai_web_search_google::DurableGoogleCustomSearch; - -let session = SearchProvider::start_search(&web_search::SearchParams { - query: "Golem distributed computing".to_string(), - language: Some("lang_en".to_string()), - safe_search: Some(types::SafeSearchLevel::Off), - max_results: Some(10), - time_range: None, - include_domains: None, - exclude_domains: None, - include_images: None, - include_html: None, - advanced_answer: Some(true), - region: None, -}).expect("Failed to start search"); - -let results = session.next_page().expect("Failed to get results"); -for result in results { - println!("{}: {}", result.title, result.url); -} -``` - -## Switching Providers - -To switch providers, change the type alias and dependency. The core API stays the same: - -```rust -// Switch from OpenAI to Anthropic: -// 1. In Cargo.toml: replace golem-ai-llm-openai with golem-ai-llm-anthropic -// 2. In code: -type Provider = golem_ai_llm_anthropic::DurableAnthropic; -// All other code stays the same -``` - -## Complete Agent Example - -```rust -use golem_ai_llm::model::*; -use golem_ai_llm::LlmProvider; -use golem_rust::{agent_definition, agent_implementation, endpoint}; - -type Provider = golem_ai_llm_openai::DurableOpenAI; - -#[agent_definition(mount = "/chats/{chat_name}")] -pub trait ChatAgent { - fn new(chat_name: String) -> Self; - - #[endpoint(post = "/ask")] - async fn ask(&mut self, question: String) -> String; -} - -struct ChatAgentImpl { - chat_name: String, - events: Vec, - config: Config, -} - -#[agent_implementation] -impl ChatAgent for ChatAgentImpl { - fn new(chat_name: String) -> Self { - let config = Config { - model: std::env::var("LLM_MODEL").unwrap_or_else(|_| "gpt-4o".to_string()), - temperature: None, - max_tokens: None, - stop_sequences: None, - tools: None, - tool_choice: None, - provider_options: None, - }; - let events = vec![Event::Message(Message { - role: Role::System, - name: None, - content: vec![ContentPart::Text(format!( - "You are a helpful assistant for chat '{}'", - chat_name - ))], - })]; - Self { chat_name, events, config } - } - - async fn ask(&mut self, question: String) -> String { - self.events.push(Event::Message(Message { - role: Role::User, - name: None, - content: vec![ContentPart::Text(question)], - })); - - let response = Provider::send(self.events.clone(), self.config.clone()) - .expect("LLM call failed"); - self.events.push(Event::Response(response.clone())); - - response - .content - .iter() - .filter_map(|part| match part { - ContentPart::Text(txt) => Some(txt.clone()), - _ => None, - }) - .collect::>() - .join("\n") - } -} -``` - -## Key Constraints - -- All golem-ai crates should use version `"0.5.1"` from crates.io -- Always add both the core crate and a provider crate (e.g., `golem-ai-llm` + `golem-ai-llm-openai`) -- Provider API keys should be stored as secrets using Golem's typed config system (load the `golem-add-secret-rust` skill) -- The `Durable*` provider types (e.g., `DurableOpenAI`) automatically integrate with Golem's durable execution — responses are recorded in the oplog and replayed on recovery -- To switch providers, change the type alias and Cargo dependency — the rest of the code stays the same -- These crates target `wasm32-wasip1` and work correctly in Golem's WebAssembly environment +When upstream compatibility is restored, verify the provider configuration, async call signature, +and secret integration from that exact release rather than relying on the examples from `0.5.2`. diff --git a/golem-skills/skills/rust/golem-add-rust-crate/SKILL.md b/golem-skills/skills/rust/golem-add-rust-crate/SKILL.md index e8baf7b1bd..2da77a9337 100644 --- a/golem-skills/skills/rust/golem-add-rust-crate/SKILL.md +++ b/golem-skills/skills/rust/golem-add-rust-crate/SKILL.md @@ -8,9 +8,11 @@ description: "Add a new Rust crate dependency to a Rust Golem project. Use when ## Important constraints - The compilation target is `wasm32-wasip2` — only crates that support this target will work. -- Crates that use threads, native system calls, `mmap`, networking via `std::net`, or platform-specific C libraries **will not compile**. -- Pure Rust crates and crates that support `wasm32-wasi` generally work. -- If unsure whether a crate compiles for WASM, add it and run `golem build` to find out. +- Crates requiring unsupported OS facilities may fail to compile or fail at runtime. +- A generic `wasm32-wasi` compatibility claim is insufficient; verify the exact features used + against `wasm32-wasip2` and exercise them in Golem. +- Pure Rust is not by itself proof of compatibility. Platform-specific C libraries, threading, + sockets, and memory-mapping APIs require particular scrutiny. ## Steps @@ -35,7 +37,7 @@ description: "Add a new Rust crate dependency to a Rust Golem project. Use when 3. **If the build fails** - - Check the error for unsupported target or missing C dependencies — these crates are incompatible with `wasm32-wasip1`. + - Check the error for unsupported target features or missing native dependencies. - Try enabling a `wasm` or `wasi` feature flag if the crate provides one. - Look for an alternative crate that supports WASM. @@ -50,8 +52,9 @@ These crates are already in the project's `Cargo.toml` — do NOT add them again ## HTTP and networking -Use `wstd::http` for HTTP requests. The standard `std::net` module is **not available** on WASM. +Prefer `wstd::http` for outgoing HTTP requests. Compile support for an API does not guarantee that +the runtime grants the required network capability, so test the actual operation in Golem. ## AI / LLM features -To add AI capabilities, add the relevant `golem-ai-*` provider crate (e.g., `golem-ai-llm-openai`) and configure the provider in the component's `golem.yaml` dependencies section. +To add AI capabilities, load the `golem-add-llm-rust` skill. The published `golem-ai` release and the current WASIp2-compatible source do not currently use the same API, so do not guess a crate version or copy an older example. diff --git a/golem-skills/skills/rust/golem-call-tool-rust/SKILL.md b/golem-skills/skills/rust/golem-call-tool-rust/SKILL.md new file mode 100644 index 0000000000..cfbf5f07bc --- /dev/null +++ b/golem-skills/skills/rust/golem-call-tool-rust/SKILL.md @@ -0,0 +1,25 @@ +--- +name: golem-call-tool-rust +description: "Calls a typed Golem tool from Rust. Use when invoking a tool provider and handling declared or protocol errors." +--- + +# Call a Golem tool from Rust + +`#[tool_definition]` generates a `Client`. Its async methods return +`Result>`, where `E` is the tool's declared error type: + +```rust +use golem_rust::agentic::ToolError; + +let client = EchoClient::default(); +match client.echo("hello".to_string()).await { + Ok(value) => println!("{value}"), + Err(ToolError::Tool(error)) => eprintln!("declared tool error: {error:?}"), + Err(error) => eprintln!("tool invocation failed: {error}"), +} +``` + +The default client targets the definition's tool name. Use the generated named-target constructor +only when deployment assigns another tool registration name. Do not treat protocol errors as the +tool's declared domain error. Drop or finish any stream handles according to the generated method +signature. diff --git a/golem-skills/skills/rust/golem-define-tool-rust/SKILL.md b/golem-skills/skills/rust/golem-define-tool-rust/SKILL.md new file mode 100644 index 0000000000..19c1febf6f --- /dev/null +++ b/golem-skills/skills/rust/golem-define-tool-rust/SKILL.md @@ -0,0 +1,32 @@ +--- +name: golem-define-tool-rust +description: "Defines and implements a typed Golem tool in Rust. Use when creating a tool provider, command schema, or callable tool component." +--- + +# Define a Golem tool in Rust + +Declare the public command surface with `#[tool_definition]`, then implement it with +`#[tool_implementation]`. The macro generates metadata, guest exports, and a typed client. + +```rust +use golem_rust::{tool_definition, tool_implementation}; + +#[tool_definition(version = "1.0.0")] +pub trait Echo { + async fn echo(&self, value: String) -> String; +} + +struct EchoImpl; + +#[tool_implementation] +impl Echo for EchoImpl { + async fn echo(&self, value: String) -> String { + value + } +} +``` + +Use `IntoSchema` and `FromSchema` for custom inputs, outputs, and error payload types. For a +declared command error in `Result`, derive `golem_rust::ToolError` on `E` and annotate its +variants with `#[tool_error(...)]`. Keep the definition and implementation in provider source; +never edit macro-generated bindings. A tool component can provide tools without defining an agent. diff --git a/golem-skills/skills/rust/golem-file-io-rust/SKILL.md b/golem-skills/skills/rust/golem-file-io-rust/SKILL.md index 1eaf4482f8..eeaa8a4262 100644 --- a/golem-skills/skills/rust/golem-file-io-rust/SKILL.md +++ b/golem-skills/skills/rust/golem-file-io-rust/SKILL.md @@ -7,7 +7,7 @@ description: "Reading and writing files from a Rust Golem agent. Use when the us ## Overview -Golem Rust agents compile to `wasm32-wasip1` which provides WASI filesystem access. Use the standard `std::fs` module for all filesystem operations — it works out of the box with WASI. +Golem Rust agents compile to `wasm32-wasip2`, which provides WASI filesystem access. Use the standard `std::fs` module for filesystem operations — it works with the directories mounted into the agent filesystem. To provision files into an agent's filesystem, load the `golem-add-initial-files` skill. @@ -38,6 +38,7 @@ Only files provisioned with `read-write` permission (or files in non-provisioned ```rust use std::fs; +fs::create_dir_all("/tmp").expect("Failed to create /tmp"); fs::write("/tmp/output.txt", "Hello, world!") .expect("Failed to write file"); ``` @@ -48,6 +49,7 @@ fs::write("/tmp/output.txt", "Hello, world!") use std::fs::OpenOptions; use std::io::Write; +std::fs::create_dir_all("/tmp").expect("Failed to create /tmp"); let mut file = OpenOptions::new() .append(true) .create(true) @@ -111,6 +113,7 @@ impl FileReaderAgent for FileReaderAgentImpl { } fn write_log(&mut self, message: String) { + std::fs::create_dir_all("/tmp").expect("Failed to create /tmp"); let mut file = std::fs::OpenOptions::new() .append(true) .create(true) diff --git a/golem-skills/skills/rust/golem-permission-card-rust/SKILL.md b/golem-skills/skills/rust/golem-permission-card-rust/SKILL.md new file mode 100644 index 0000000000..45876dfd95 --- /dev/null +++ b/golem-skills/skills/rust/golem-permission-card-rust/SKILL.md @@ -0,0 +1,25 @@ +--- +name: golem-permission-card-rust +description: "Transfers opaque permission cards through Rust agent and tool schemas. Use when delegated authority must cross an RPC or tool boundary." +--- + +# Permission cards in Rust + +Use `golem_rust::schema::wit::GuestPermissionCardHandle` in an agent or tool input/output. It has +the required `IntoSchema` and `FromSchema` implementations: + +```rust +use golem_rust::schema::wit::GuestPermissionCardHandle; + +async fn forward(card: GuestPermissionCardHandle) -> GuestPermissionCardHandle { + ReceiverClient::get("target".to_string()) + .accept(card) + .await +} +``` + +Cards are opaque affine capabilities. Encoding a call transfers the card, so do not serialize, +clone for reuse, persist, or send the same handle twice. This guide covers transferring an already +received handle. The SDK does not currently expose a supported high-level constructor for wrapping +cards returned by the lower-level permissions host bindings. Do not use hidden schema-codec +constructors as application APIs, invent authority locally, or log card internals. diff --git a/golem-skills/skills/rust/golem-recurring-task-rust/SKILL.md b/golem-skills/skills/rust/golem-recurring-task-rust/SKILL.md index c0a5e4d712..0ee0d33724 100644 --- a/golem-skills/skills/rust/golem-recurring-task-rust/SKILL.md +++ b/golem-skills/skills/rust/golem-recurring-task-rust/SKILL.md @@ -1,216 +1,106 @@ --- name: golem-recurring-task-rust -description: "Implementing a recurring (cron-like) task in a Rust Golem agent by self-scheduling future invocations. Use when the user asks about periodic tasks, recurring jobs, cron-like scheduling, polling loops, heartbeats, or self-scheduling agents." +description: "Implements recurring Rust agent work by self-scheduling future invocations. Use for periodic jobs, polling loops, heartbeats, cleanup, or retry backoff." --- -# Recurring Tasks via Self-Scheduling (Rust) +# Recurring tasks via self-scheduling (Rust) -## Overview +A durable agent can schedule its own next invocation after completing each tick. Scheduled +invocations survive recovery and execute sequentially with the agent's other invocations. -A Golem agent can act as its own scheduler by calling `schedule_` on itself at the end of each invocation. This creates a durable, crash-resilient recurring task — if the agent restarts, the scheduled invocation is still pending and will fire at the designated time. - -## Basic Pattern - -The agent schedules its own method to run again after a delay: +Use the current `#[agent_definition]` and `#[agent_implementation]` macros, construct a +`golem_rust::ScheduledTime`, pass method arguments before the scheduled time, and handle the +generated scheduling result: ```rust -use golem_rust::wasip2::clocks::wall_clock::Datetime; - -#[agent_definition] -pub trait PollerAgent: HasSchema { - fn new(name: String) -> Self; - fn start(&mut self); - fn poll(&mut self); -} - -impl PollerAgent for PollerAgentImpl { - fn new(name: String) -> Self { - Self { name } - } - - fn start(&mut self) { - // Kick off the first poll - self.poll(); - } - - fn poll(&mut self) { - // 1. Do the recurring work - do_work(); - - // 2. Schedule the next run (60 seconds from now) - let mut client = PollerAgentClient::get(self.name.clone()); - let now_secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - client.schedule_poll(Datetime { - seconds: now_secs + 60, - nanoseconds: 0, - }); +use golem_rust::{ScheduledTime, agent_definition, agent_implementation}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +fn after(delay: Duration) -> ScheduledTime { + let at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time went backwards") + + delay; + ScheduledTime { + seconds: at.as_secs() as i64, + nanoseconds: at.subsec_nanos(), } } -``` -## Exponential Backoff - -Increase the delay on repeated failures, reset on success: - -```rust -fn poll(&mut self) { - let success = try_work(); - - let delay = if success { - self.consecutive_failures = 0; - self.base_interval_secs // e.g. 60 - } else { - self.consecutive_failures += 1; - let backoff = self.base_interval_secs * 2u64.pow(self.consecutive_failures.min(6)); - backoff.min(self.max_interval_secs) // cap at e.g. 3600 - }; - - let mut client = PollerAgentClient::get(self.name.clone()); - let now_secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - client.schedule_poll(Datetime { - seconds: now_secs + delay, - nanoseconds: 0, - }); +#[agent_definition] +pub trait PollerAgent { + fn new(name: String) -> Self; + async fn poll(&mut self, endpoint: String); } -``` - -## Cancellation with CancellationToken -The Rust SDK generates `schedule_cancelable_{method}` variants that return a `CancellationToken`. Store the token and cancel it to stop the next scheduled invocation: - -```rust -fn poll(&mut self) { - if self.cancelled { - return; // stop the loop - } - - do_work(); - - // Schedule next run and store the cancellation token - let mut client = PollerAgentClient::get(self.name.clone()); - let now_secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - self.pending_token = Some(client.schedule_cancelable_poll(Datetime { - seconds: now_secs + 60, - nanoseconds: 0, - })); +struct PollerAgentImpl { + name: String, + stopped: bool, } -fn cancel(&mut self) { - self.cancelled = true; - // Cancel the pending scheduled invocation so it never fires - if let Some(token) = self.pending_token.take() { - token.cancel(); +#[agent_implementation] +impl PollerAgent for PollerAgentImpl { + fn new(name: String) -> Self { + Self { name, stopped: false } } -} -``` -### Cancellation via State Flag + async fn poll(&mut self, endpoint: String) { + if self.stopped { + return; + } -For simpler cases, just use a boolean flag — the next scheduled `poll` checks it and exits early: + do_work(&endpoint).await; -```rust -fn poll(&mut self) { - if self.cancelled { - return; + let me = PollerAgentClient::get(self.name.clone()); + me.schedule_poll(endpoint, after(Duration::from_secs(60))) + .expect("failed to schedule next poll"); } - do_work(); - self.schedule_next(60); } - -fn cancel(&mut self) { - self.cancelled = true; -} -``` - -### Cancellation from the CLI - -Schedule with an explicit idempotency key and cancel the pending invocation: - -```shell -# Schedule with a known idempotency key -golem agent invoke --trigger --schedule-at 2026-03-15T10:30:00Z -i 'poll-next' 'PollerAgent("my-poller")' poll - -# Cancel the pending invocation -golem agent invocation cancel 'PollerAgent("my-poller")' 'poll-next' ``` -## Common Use Cases +Generated durable-agent scheduling methods return +`Result<(), golem_rust::golem_agentic::golem::agent::host::RpcError>`. Never discard +that result: a failed enqueue breaks the recurring chain. For a method with arguments, the +signature is `schedule_(arguments..., scheduled_time)`; `ScheduledTime` is always last. -### Periodic Polling +## Backoff -Check an external API or queue for new work at regular intervals: +Store the consecutive failure count in agent state. Reset it after success; otherwise compute a +capped delay and schedule one next invocation. For example: ```rust -fn poll(&mut self) { - let items = fetch_pending_items(); - for item in items { - process(item); - } - self.schedule_next(60); // poll again in 60s -} +let delay = if succeeded { + self.consecutive_failures = 0; + 60 +} else { + self.consecutive_failures += 1; + (60 * 2u64.pow(self.consecutive_failures.min(6))).min(3600) +}; + +PollerAgentClient::get(self.name.clone()) + .schedule_poll(endpoint, after(Duration::from_secs(delay))) + .expect("failed to schedule retry"); ``` -### Periodic Cleanup +## Cancel the pending tick -Remove expired data or stale resources on a schedule: +`schedule_cancelable_(arguments..., scheduled_time)` returns +`Result` for a durable agent. Store the token in agent state +and call `cancel()` to prevent that scheduled invocation from starting: ```rust -fn cleanup(&mut self) { - self.entries.retain(|e| !e.is_expired()); - self.schedule_next(3600); // run hourly -} -``` +let token = PollerAgentClient::get(self.name.clone()) + .schedule_cancelable_poll(endpoint, after(Duration::from_secs(60))) + .expect("failed to schedule next poll"); +self.pending = Some(token); -### Heartbeat / Keep-Alive - -Periodically notify an external service that the agent is alive: - -```rust -fn heartbeat(&mut self) { - send_heartbeat(&self.service_url); - self.schedule_next(30); // every 30s +if let Some(token) = self.pending.take() { + token.cancel(); } ``` -## Helper for Scheduling Self - -Extract the scheduling logic into a helper to keep methods clean: - -```rust -impl PollerAgentImpl { - fn schedule_next(&self, delay_secs: u64) { - let mut client = PollerAgentClient::get(self.name.clone()); - let now_secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - client.schedule_poll(Datetime { - seconds: now_secs + delay_secs, - nanoseconds: 0, - }); - } -} -``` - -## Key Points - -- The agent is durable — if it crashes, the pending scheduled invocation still fires and the agent recovers -- Invocations are sequential — no concurrent executions of `poll` on the same agent -- Each `schedule_` call is a fire-and-forget enqueue; the current invocation completes immediately -- Use a state flag or generation counter to stop the loop gracefully -- Keep the scheduled method idempotent — it may be retried on recovery - -## Recovery & Oplog Growth - -Each scheduled tick (heartbeat, poll, cleanup) appends entries to the agent's oplog. For long-running or high-frequency recurring tasks, the oplog grows unboundedly, and recovery on crash will replay the full history — which becomes slow over time. +A state flag is still useful because a tick may already have started when cancellation races with +delivery. Keep only one pending tick unless overlapping schedules are intentional. -**You cannot opt out of oplog writes for a durable agent.** The fix is **snapshot-based recovery**: enable periodic snapshotting so recovery starts from the latest snapshot instead of replaying every prior tick. See [`golem-custom-snapshot-rust`](../golem-custom-snapshot-rust/SKILL.md) for the `snapshotting = "every(N)"` / `snapshotting = "periodic(...)"` attribute and serde-based or custom save/load implementations. +Recurring invocations grow the oplog. For long-lived or frequent loops, configure periodic +snapshots; do not try to bypass durability. Load `golem-custom-snapshot-rust` for that workflow. diff --git a/golem-skills/skills/rust/golem-schedule-future-call-rust/SKILL.md b/golem-skills/skills/rust/golem-schedule-future-call-rust/SKILL.md index e235a8596a..a55db0b744 100644 --- a/golem-skills/skills/rust/golem-schedule-future-call-rust/SKILL.md +++ b/golem-skills/skills/rust/golem-schedule-future-call-rust/SKILL.md @@ -1,58 +1,47 @@ --- name: golem-schedule-future-call-rust -description: "Scheduling a future agent invocation in a Rust Golem project. Use when the user asks about delayed invocations, scheduling calls for later, or timed agent execution." +description: "Schedules a future agent invocation from Rust. Use for delayed processing, reminders, retries, or timed agent execution." --- -# Scheduling a Future Agent Invocation (Rust) +# Schedule a future agent invocation (Rust) -## Overview - -A **scheduled invocation** enqueues a method call on the target agent to be executed at a specific future time. The call returns immediately; the target agent processes it when the scheduled time arrives. - -## Usage - -Every method on the generated `Client` has a corresponding `schedule_` variant that takes a `Datetime` as the first argument: +Generated agent clients expose `schedule_` and `schedule_cancelable_`. Pass the +method arguments first and a `golem_rust::ScheduledTime` last: ```rust -use golem_rust::wasip2::clocks::wall_clock::Datetime; - -let mut counter = CounterAgentClient::get("my-counter".to_string()); +use golem_rust::ScheduledTime; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +let at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time went backwards") + + Duration::from_secs(60); +let scheduled_time = ScheduledTime { + seconds: at.as_secs() as i64, + nanoseconds: at.subsec_nanos(), +}; -// Schedule increment to run 60 seconds from now -let now_secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - -counter.schedule_increment(Datetime { - seconds: now_secs + 60, - nanoseconds: 0, -}); - -// Schedule with arguments let reporter = ReportAgentClient::get("daily".to_string()); -reporter.schedule_generate_report( - "summary".to_string(), - Datetime { seconds: tomorrow_midnight, nanoseconds: 0 } -); +reporter + .schedule_generate_report("summary".to_string(), scheduled_time) + .expect("failed to schedule report"); ``` -## Datetime Type +`ScheduledTime` is an absolute Unix timestamp with signed seconds and nanosecond precision. Do not +use the removed `wasip2::clocks::wall_clock::Datetime` path. -The `Datetime` struct represents a point in time as seconds + nanoseconds since the Unix epoch: +For durable agents, `schedule_` returns `Result<(), RpcError>`, where `RpcError` is +`golem_rust::golem_agentic::golem::agent::host::RpcError`. The cancelable +variant returns a cancellation token: ```rust -use golem_rust::wasip2::clocks::wall_clock::Datetime; +let token = reporter + .schedule_cancelable_generate_report("summary".to_string(), scheduled_time) + .expect("failed to schedule report"); -Datetime { - seconds: 1700000000, // Unix timestamp in seconds - nanoseconds: 0, // Sub-second precision -} +// Cancel before the invocation starts. +token.cancel(); ``` -## Use Cases - -- **Periodic tasks**: Schedule the next run at the end of each invocation -- **Delayed processing**: Process an order after a cooling-off period -- **Reminders and notifications**: Send a reminder at a specific time -- **Retry with backoff**: Schedule a retry after a delay on failure +Scheduling is fire-and-forget: it confirms that the invocation was enqueued, not that the future +method succeeded. Keep the target method idempotent when retries or recovery may repeat effects. diff --git a/golem-skills/skills/rust/golem-tools-middleware-rust/SKILL.md b/golem-skills/skills/rust/golem-tools-middleware-rust/SKILL.md new file mode 100644 index 0000000000..9d057c47d6 --- /dev/null +++ b/golem-skills/skills/rust/golem-tools-middleware-rust/SKILL.md @@ -0,0 +1,36 @@ +--- +name: golem-tools-middleware-rust +description: "Defines typed Golem tool middleware in Rust. Use for validation, policy, auditing, or adapting one tool surface to another." +--- + +# Tool middleware in Rust + +Annotate an implementation of a generated middleware trait with `#[tool_middleware]`. A transparent +middleware presents and wraps the same tool: + +```rust +use golem_rust::{tool::ToolInvokeError, tool_middleware}; + +struct EchoPolicy; +impl EchoPolicy { fn new() -> Self { Self } } + +#[tool_middleware(name = "echo-policy", constructor = EchoPolicy::new)] +impl EchoMiddleware for EchoPolicy { + async fn echo( + &self, + underlying: &EchoUnderlying, + value: String, + ) -> Result> { + if value.is_empty() { + Err(ToolInvokeError::ConstraintViolation("value is empty".into())) + } else { + underlying.echo(value).await + } + } +} +``` + +For an adapter, implement `PresentedMiddleware` and translate inputs, outputs, +and custom errors explicitly. The supplied underlying handle is invocation-scoped: do not store it, +return it, or use it after the middleware method finishes. Forward affine permission cards and +streams exactly once. diff --git a/golem-skills/skills/scala/golem-add-http-endpoint-scala/SKILL.md b/golem-skills/skills/scala/golem-add-http-endpoint-scala/SKILL.md index a5c824dbf8..dc96436e00 100644 --- a/golem-skills/skills/scala/golem-add-http-endpoint-scala/SKILL.md +++ b/golem-skills/skills/scala/golem-add-http-endpoint-scala/SKILL.md @@ -182,7 +182,10 @@ Scala `Either[E, T]` is mapped to the WIT `result` type, with `Right` trea | `Future[Either[E, T]]` | 200 OK if `Right`, 500 Internal Server Error if `Left` | JSON `T` or JSON `E` | | `Future[Either[E, Unit]]` | 204 No Content if `Right`, 500 if `Left` | empty or JSON `E` | | `Future[Either[Unit, T]]` | 200 OK if `Right`, 500 if `Left` | JSON `T` or empty | -| `Future[UnstructuredBinary]` | 200 OK | Raw binary with Content-Type | + +The current Scala SDK does not expose `UnstructuredText` or `UnstructuredBinary`. Use ordinary +schema-backed values, which are JSON encoded, or choose an SDK that supports rich raw-body types +when an endpoint must return plain text or arbitrary binary bytes. ## Complete Example diff --git a/golem-skills/skills/scala/golem-call-tool-scala/SKILL.md b/golem-skills/skills/scala/golem-call-tool-scala/SKILL.md new file mode 100644 index 0000000000..9f729b6386 --- /dev/null +++ b/golem-skills/skills/scala/golem-call-tool-scala/SKILL.md @@ -0,0 +1,24 @@ +--- +name: golem-call-tool-scala +description: "Calls a generated typed Golem tool client from Scala. Use when invoking a tool provider and handling tool errors." +--- + +# Call a Golem tool from Scala + +`@toolDefinition` generates `Client`. Construct it with the generated factory and call the +typed asynchronous methods: + +```scala +import golem.tool.ToolError +import scala.concurrent.Future + +val client: EchoClient = EchoClient() +val result: Future[Either[ToolError[Nothing], String]] = + client.echo("hello") +``` + +Use the exact generated signature: a command with declared errors uses the error type declared by +the tool method instead of `Nothing`. Commands without stdout, including stdin-only commands, +return `Future[Either[ToolError[E], A]]`; stdin is a `ToolInputStream` parameter. A command with +stdout returns `Either[ToolError[E], ToolInvocation[E, A]]`, whose invocation exposes the result and +stream. Handle `ToolError.Tool` separately from protocol failures. Never edit generated clients. diff --git a/golem-skills/skills/scala/golem-define-tool-scala/SKILL.md b/golem-skills/skills/scala/golem-define-tool-scala/SKILL.md new file mode 100644 index 0000000000..651e288802 --- /dev/null +++ b/golem-skills/skills/scala/golem-define-tool-scala/SKILL.md @@ -0,0 +1,28 @@ +--- +name: golem-define-tool-scala +description: "Defines and implements a typed Golem tool in Scala. Use when creating a tool provider or command schema." +--- + +# Define a Golem tool in Scala + +Annotate the definition trait and implementation class. The build plugin generates registration, +metadata, and typed client sources: + +```scala +import golem.runtime.annotations.{toolDefinition, toolImplementation} + +@toolDefinition(version = "1.0.0") +trait Echo { + def echo(value: String): String +} + +@toolImplementation() +final class EchoImpl extends Echo { + override def echo(value: String): String = value +} +``` + +Use `@arg`, `@command`, `@constraint`, `@result`, and `@annotations` when the command-line surface +needs explicit metadata. Custom values require `zio.blocks.schema.Schema`. Keep +`scalacOptions += "-experimental"` enabled for macro annotations and never edit generated client +or registration sources. diff --git a/golem-skills/skills/scala/golem-permission-card-scala/SKILL.md b/golem-skills/skills/scala/golem-permission-card-scala/SKILL.md new file mode 100644 index 0000000000..6c69f16297 --- /dev/null +++ b/golem-skills/skills/scala/golem-permission-card-scala/SKILL.md @@ -0,0 +1,21 @@ +--- +name: golem-permission-card-scala +description: "Transfers opaque permission cards through Scala schemas. Use when delegated authority must cross an agent, tool, or middleware boundary." +--- + +# Permission cards in Scala + +The carrier is `golem.schema.GuestPermissionCardHandle`. Because the handle is opaque, choose its +schema explicitly with `GuestPermissionCardHandle.intoSchema(PermissionCardSpec(...))`: + +```scala +import golem.schema.{GuestPermissionCardHandle, IntoSchema, PermissionCardSpec} + +given IntoSchema[GuestPermissionCardHandle] = + GuestPermissionCardHandle.intoSchema(PermissionCardSpec(polymorphic = false)) +``` + +Use the handle in an agent or tool input/output only where that `IntoSchema` is in scope. Decoding +uses the SDK's provided `FromSchema`. Guest code cannot construct or re-wrap a raw card through the +public Scala API; it receives one from the runtime or another call. Encoding transfers ownership, +so never serialize, persist, duplicate, or reuse a transferred handle. diff --git a/golem-skills/skills/scala/golem-tools-middleware-scala/SKILL.md b/golem-skills/skills/scala/golem-tools-middleware-scala/SKILL.md new file mode 100644 index 0000000000..c1e649c16e --- /dev/null +++ b/golem-skills/skills/scala/golem-tools-middleware-scala/SKILL.md @@ -0,0 +1,30 @@ +--- +name: golem-tools-middleware-scala +description: "Defines typed or universal Golem tool middleware in Scala. Use for validation, policy, auditing, or adapting tool surfaces." +--- + +# Tool middleware in Scala + +The build plugin generates `Middleware` and `Underlying` from each tool definition. +Extend the generated middleware trait and annotate the concrete no-argument class. The example +assumes the `Echo` definition from `golem-define-tool-scala`. + +```scala +import golem.runtime.annotations.toolMiddleware +import golem.tool.ToolInvokeError +import scala.concurrent.Future + +@toolMiddleware(name = "echo-policy") +final class EchoPolicy extends EchoMiddleware { + def echo( + underlying: EchoUnderlying, + value: String + ): Future[Either[ToolInvokeError[Nothing], String]] = + underlying.echo(value).toMiddlewareResult +} +``` + +For adapters, extend `PresentedMiddleware.Adapter[ExpectedUnderlying]` and translate values and +custom errors. Use `@universalToolMiddleware` with `UniversalToolMiddleware` only for arbitrary tool +metadata and raw `TypedSchemaValue` carriers. The underlying is invocation-scoped; never store or +return it. Forward each stream or permission card at most once. diff --git a/golem-skills/skills/ts/golem-call-tool-ts/SKILL.md b/golem-skills/skills/ts/golem-call-tool-ts/SKILL.md new file mode 100644 index 0000000000..52a9a4d570 --- /dev/null +++ b/golem-skills/skills/ts/golem-call-tool-ts/SKILL.md @@ -0,0 +1,34 @@ +--- +name: golem-call-tool-ts +description: "Calls a typed Golem tool from TypeScript. Use when invoking a tool provider and handling tool or RPC failures." +--- + +# Call a Golem tool from TypeScript + +Bind a client from the same definition, or use `toolClientDefinition` for a caller-owned subset: + +```typescript +import { ToolCallError, toolClientDefinition, toolDefinition } from '@golemcloud/golem-ts-sdk'; +import { z } from 'zod/v4'; + +const echo = toolDefinition('echo').body((body) => + body.positional('value', z.string()).returns(z.string()), +); +const client = toolClientDefinition(echo).client('echo'); + +try { + const value = await client.echo({ value: 'hello' }); + console.log(value); +} catch (error) { + if (error instanceof ToolCallError && error.cause.tag === 'tool') { + console.error(error.cause.error); + } else { + throw error; + } +} +``` + +Commands without declared stdout return a Promise of their result. Commands with required or +optional stdout return a `StartedToolInvocation`; consume its `stdout` and await its `result`, or +use `.collect()` to collect both. `ToolCallError.cause.tag` is `tool`, `rpc`, or `unknown-error`. +Protocol errors use `rpc` with `cause.error.tag === 'protocol-error'`. diff --git a/golem-skills/skills/ts/golem-define-tool-ts/SKILL.md b/golem-skills/skills/ts/golem-define-tool-ts/SKILL.md new file mode 100644 index 0000000000..712721ca3f --- /dev/null +++ b/golem-skills/skills/ts/golem-define-tool-ts/SKILL.md @@ -0,0 +1,28 @@ +--- +name: golem-define-tool-ts +description: "Defines and implements a typed Golem tool in TypeScript. Use when creating a tool provider or command schema." +--- + +# Define a Golem tool in TypeScript + +Build a definition with `toolDefinition(...).body(...)`, then register one implementation object: + +```typescript +import { ok, toolDefinition } from '@golemcloud/golem-ts-sdk'; +import { z } from 'zod/v4'; + +export const echo = toolDefinition('echo') + .version('1.0.0') + .body((body) => + body.positional('value', z.string()).returns(z.string()), + ); + +echo.implement({ + echo: async ({ value }) => ok(value), +}); +``` + +The implementation key is the generated command name. Use the body builder for positional, +option, flag, tail, stdin/stdout, result, and declared-error metadata; use Standard Schema values +such as Zod schemas for typed data. Provider handlers return `ok(result)` or a declared `err(...)`. +Call `.implement(...)` only once per definition. diff --git a/golem-skills/skills/ts/golem-permission-card-ts/SKILL.md b/golem-skills/skills/ts/golem-permission-card-ts/SKILL.md new file mode 100644 index 0000000000..8eb0236c09 --- /dev/null +++ b/golem-skills/skills/ts/golem-permission-card-ts/SKILL.md @@ -0,0 +1,24 @@ +--- +name: golem-permission-card-ts +description: "Transfers opaque permission cards through TypeScript agent and tool schemas. Use when delegated authority crosses a call boundary." +--- + +# Permission cards in TypeScript + +Declare the capability with `s.permissionCard({ polymorphic })`: + +```typescript +import { s, toolDefinition } from '@golemcloud/golem-ts-sdk'; + +const delegate = toolDefinition('delegate').body((body) => + body + .positional('card', s.permissionCard({ polymorphic: false })) + .returns(s.permissionCard({ polymorphic: false })), +); +``` + +The runtime value is an opaque raw permission-card resource received from a host or another call. +The SDK does not expose a public constructor for forging one. Successful encoding transfers +ownership before the call completes. Never reuse a transferred handle, even if the invocation +subsequently fails. Do not inspect, stringify, persist, or duplicate permission-card values. Set +`polymorphic: true` only when the card schema may contain owner or resource-id slots. diff --git a/golem-skills/skills/ts/golem-tools-middleware-ts/SKILL.md b/golem-skills/skills/ts/golem-tools-middleware-ts/SKILL.md new file mode 100644 index 0000000000..c61cdfd345 --- /dev/null +++ b/golem-skills/skills/ts/golem-tools-middleware-ts/SKILL.md @@ -0,0 +1,41 @@ +--- +name: golem-tools-middleware-ts +description: "Defines typed or universal Golem tool middleware in TypeScript. Use for validation, policy, auditing, or tool adapters." +--- + +# Tool middleware in TypeScript + +Call `.middleware(...)` on the presented definition. Omit `wraps` for transparent middleware; +provide another definition in `wraps` for an adapter: + +```typescript +import { ToolInvokeError, toolDefinition } from '@golemcloud/golem-ts-sdk'; +import { z } from 'zod/v4'; + +const echo = toolDefinition('echo').body((body) => + body.positional('value', z.string()).returns(z.string()), +); + +echo.middleware({ + name: 'echo-policy', + implementation: { + echo: async ({ value }, { underlying }) => { + if (value.length === 0) { + throw new ToolInvokeError({ + tag: 'constraint-violation', + val: 'value is empty', + }); + } + return underlying.echo({ value }); + }, + }, +}); +``` + +Use `universalToolMiddleware(...)` only when middleware must inspect arbitrary tool metadata and +raw typed values. Prefer typed middleware whenever the surface is known. The exact handler context +and result shape follows the command's arguments, declared errors, and streams; let TypeScript +infer it rather than casting. Use `underlying` only during the middleware handler; it may make +multiple calls before the handler settles. Do not retain it for later invocations. Do not reuse +transferred streams or permission-card handles. Returned stdout may be consumed lazily after the +handler returns.