npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@golemcloud/effect-golem

v2.0.3

Published

Write Golem agents on top of Effect v4: Effect Schema-typed methods, durable execution, snapshots, sagas, RPC, HTTP, webhooks, KV/blob, and RDBMS adapters compiled to a Golem WASM component.

Readme

@golemcloud/effect-golem

Effect-native TypeScript SDK for Golem 1.6 agents. It uses Effect 4 schemas and effects while targeting Golem's WASI Preview 3 agent, tool, and tool-middleware contracts.

Keep the effect version generated for an application aligned with the SDK.

Install and define an agent

npm install @golemcloud/effect-golem effect

The 1.6 vocabulary is id for agent identity, input for method input, and the client attached to the definition. defineAgent creates an inert specification; .implement({ init, methods }) registers it.

import { Effect, Ref, Schema } from "effect"
import { defineAgent, method } from "@golemcloud/effect-golem"

export const Counter = defineAgent({
  name: "Counter",
  mode: "durable",
  id: { name: Schema.String },
  methods: {
    value: method({ input: {}, success: Schema.Number, readOnly: true }),
    add: method({ input: { by: Schema.Number }, success: Schema.Number }),
  },
})

Counter.implement({
  init: () => Ref.make(0),
  methods: (count) => ({
    value: () => Ref.get(count),
    add: ({ by }) => Ref.updateAndGet(count, (value) => value + by),
  }),
})

method optionally accepts error, description, promptHint, HTTP endpoints, and readOnly. Read-only metadata may be true or { cache }; principal-aware caching is derived from a declared PrincipalSchema input. Public modules are namespaces from the root (Agent, Client, Config, Durability, Snapshot, Tool, etc.); only defineAgent, defineAgentClient, defineConfig, and method are flat DSL aliases. Database adapters and standalone middleware use the documented sub-imports. internal/* and host/* are not public imports.

Typed clients

Keep a definition in a module without calling .implement when another component only needs its client. Durable definitions expose client.get, getPhantom, and newPhantom; ephemeral definitions expose getPhantom and newPhantom, but not ordinary get. Calls, triggers, and schedules take one typed input object.

const program = Effect.scoped(
  Effect.gen(function* () {
    const counter = yield* Counter.client.get({ name: "orders" })
    yield* counter.add({ by: 2 })
    yield* counter.add.trigger({ by: 3 })
    const scheduled = yield* counter.add.schedule("10 minutes", { by: 1 })
    // yield* scheduled.cancel()
  }),
)

Awaited RPC is interruptible: fiber interruption cancels the result handle. Cancellation is best effort; remote side effects may already have happened. Triggering and scheduling methods whose schema contains a live stream is rejected because streams require an awaited invocation. An ephemeral call returns { metadata, value }, and an ephemeral trigger/schedule exposes invocation metadata. Config overrides are passed as the second argument to client.get/newPhantom.

Use defineAgentClient for caller-owned clients. A full client definition has a declared name, constructor schema, lifecycle mode, and methods; it exposes agentId and client factories without registering an implementation. A method-only { methods } definition has neither factory nor identity constructor; it binds without discovery and assumes durable result semantics. An unimplemented defineAgent spec remains usable as a shared implementation/caller definition. identity.client(clientDefinition) validates the declared name and constructor schema before opening RPC. Client.bind(identity, clientDefinition) and DynamicClient.bind(identity) remain lower-level functions.

import { Effect, Schema } from "effect"
import { AgentIdentity, defineAgentClient, method } from "@golemcloud/effect-golem"
import type * as CoreTypes from "golem:core/[email protected]"

const Target = defineAgentClient({
  name: "Target",
  id: { name: Schema.String },
  methods: { echo: method({ input: { message: Schema.String }, success: Schema.String }) },
})

const calls = (inputTree: CoreTypes.SchemaValueTree) =>
  Effect.scoped(
    Effect.gen(function* () {
      const identity = yield* Target.agentId({ name: "main" })
      const full = yield* identity.client(Target)
      const first = yield* full.echo({ message: "full" })
      const methods = defineAgentClient({ methods: Target.methods })
      const second = yield* (yield* identity.client(methods)).echo({ message: "method only" })
      const parsed = yield* AgentIdentity.parse(identity.encoded)
      const dynamic = yield* parsed.dynamicClient()
      const raw = yield* dynamic.method("echo").invoke(inputTree)
      // Dynamic methods accept native SchemaValueTree inputs and return metadata plus native values.
      return { first, second, raw }
    }),
  )

Complete ephemeral specs require a phantom ID for agentId(input, phantomId) and reject generic existing-ID binding. Address known or fresh phantoms through their factories instead.

For runtime-selected targets, use Reflection.getAgentType(name), getAgentTypeByAgentId(parsedIdentity), or getAllAgentTypes. Each immutable registration exposes constructor/method SchemaRef values with JSON/value validation and JSON Schema rendering. Narrow type.mode to select its lifecycle factory:

import { Effect } from "effect"
import { Reflection } from "@golemcloud/effect-golem"

const invokeCounter = Effect.scoped(
  Effect.gen(function* () {
    const type = yield* Reflection.getAgentType("Counter")
    if (type === undefined || type.mode !== "durable") return undefined
    const client = yield* type.client.get({ name: "main" })
    const add = yield* client.method("add")
    return yield* add.invoke({ by: 1 })
  }),
)

Reflected invocation results contain metadata and, for non-unit outputs, value. Ephemeral newPhantom returns a client whose actual identity arrives in invocation metadata; getPhantom(input, phantomId) addresses a known phantom without offering ordinary existing-ID binding. Durable newPhantom returns { client, agentId, phantomId }, with a parsed agentId. Durable reflected types also bind through Client.bind(identity, reflectedType) after name and constructor validation. Discovery misses remain undefined; host and malformed-schema failures enter the typed error channel. invokeValue, triggerValue, and scheduleValue accept native WIT schema-value trees when JSON cannot represent capabilities. parsedIdentity.dynamicClient() binds without discovery and uses value-only methods; callers must supply the correct remote method definitions. Both APIs use scopes and fiber interruption and expose the same structured remote-call errors as typed clients.

The four client approaches are: Normal RPC through the ordinary client for a shared source definition; method-only or full caller-defined static clients; discovered clients backed by an immutable deployed schema snapshot; and fully dynamic schema-native clients for existing durable identities. Method-only clients do not own identity, lifecycle, mode, or config declarations. Full clients own those declarations and the matching durable, phantom, or ephemeral factories. Dynamic clients neither discover nor create.

Normal RPC and caller-defined static clients encode through their local schemas before opening RPC; full binding also checks the declared name and constructor shape. Discovered clients apply all snapshot restrictions and validate declared result cardinality and shape. The host remains authoritative for visibility, authorization, effective configuration, durable identity resolution, and deployed input schemas. Typed inputs use Schema.optional(...) normally. Canonical reflected JSON records contain every field and use null for an absent option. Wide integers are decimal strings: duration is { nanoseconds: "..." } and quantity uses a decimal-string mantissa; integers through 32 bits remain numbers. Streams and opaque capabilities require *Value APIs and transfer ownership once.

Unstructured outputs and HTTP responses

Unstructured.UnstructuredText() and Unstructured.UnstructuredBinary() declare unstructured values. A method returns them as TextReferenceValue or BinaryReferenceValue: either { _tag: "inline", val, ... } with the data, or { _tag: "url", val }. Wrap an element in Unstructured.Option(...) to return an Option of the reference. Unstructured.Option is only a method success; it is not an input parameter or a multimodal member. Each form combines with a typed error:

import { Effect, Option, Schema } from "effect"
import { defineAgent, Http, method, Method, Unstructured } from "@golemcloud/effect-golem"

const Pending = Schema.Struct({ _tag: Schema.Literal("Pending") })

export const Files = defineAgent({
  name: "Files",
  id: { bucket: Schema.String },
  http: Http.mount("/files/{bucket}"),
  methods: {
    download: method({
      input: { id: Schema.String },
      success: Unstructured.Option(
        Unstructured.UnstructuredBinary({ restrictions: [{ mimeType: "image/png" }] }),
      ),
      error: Pending,
    }).pipe(Method.withHttp(Http.get("/{id}"))),
  },
})

Files.implement({
  init: () => Effect.void,
  methods: () => ({
    download: ({ id }) =>
      id === "missing"
        ? Effect.succeed(Option.none())
        : id === "pending"
          ? Effect.fail({ _tag: "Pending" as const })
          : Effect.succeed(
              Option.some({ _tag: "inline", val: new Uint8Array(), mimeType: "image/png" }),
            ),
  }),
})

The output schema is the element's variant { inline, url } with an unstructured-text or unstructured-binary role. Unstructured.Option wraps it in option<_>, and a typed error wraps the success in result<_, error>. The role stays on the nested variant. Typed clients decode the same reference and Option values and return the typed error in the Effect error channel. Reflection and DynamicClient see the schema value with the role intact.

The Golem HTTP host maps the output of an HTTP endpoint method as follows:

| Output | Response | | ---------------------------- | --------------------------------------------------------------- | | element | inline → 200 with the raw body; url → redirect | | option<element> | none → 404; some → as the element | | result<element, E> | ok → as the element; err → 500 with the error as JSON | | result<option<element>, E> | ok(none) → 404; ok(some) → as the element; err → 500 JSON |

Other success schemas use the same option and result rules with a JSON body in place of the raw body.

Configuration and opaque secrets

Schema.Redacted(inner) declares a secret. The config wire value is an opaque host handle; config.apiKey.get asks the secrets host to reveal and decode it only at read time, then returns an Effect containing Redacted<inner>. Do not log, stringify, cache, snapshot, or put secret values in RPC overrides.

import { Effect, Redacted, Schema } from "effect"
import { defineAgent, defineConfig, method } from "@golemcloud/effect-golem"

class ServiceConfig extends defineConfig("Service.Config", {
  endpoint: Schema.String,
  apiKey: Schema.Redacted(Schema.String),
  tuning: Schema.Struct({ retries: Schema.Number }),
}) {}

const Service = defineAgent({
  name: "Service",
  id: { tenant: Schema.String },
  config: ServiceConfig,
  methods: { endpoint: method({ input: {}, success: Schema.String }) },
})

Service.implement({
  init: () =>
    Effect.gen(function* () {
      const config = yield* ServiceConfig
      const endpoint = yield* config.endpoint
      const apiKey = yield* config.apiKey.get
      // Reveal only at the boundary that needs plaintext:
      void Redacted.value(apiKey)
      return endpoint
    }),
  methods: (endpoint) => ({ endpoint: () => Effect.succeed(endpoint) }),
})

Plain leaves are invocation-local cached; secret reads are not cached. Schema.optional(...) declares optional values. Nested structs map to path segments. RPC overrides include only non-secret leaves:

yield *
  Service.client.get(
    { tenant: "acme" },
    { overrides: { endpoint: "https://example.test", tuning: { retries: 2 } } },
  )

Config is never stored in snapshots. Restoration reads current host values.

Snapshots and restoration

Snapshotting agents initialize runtime state with init(id). The implementation's snapshot strategy restores that state before methods(state) constructs handlers. Custom restoration receives a SnapshotRestorationContext containing id, principal, phantomId, agentId, parsedAgentId, and current config; it must not perform fresh-instance initialization effects.

import { Effect, Ref, Schema } from "effect"
import { defineAgent, method, Snapshot } from "@golemcloud/effect-golem"

const definition = defineAgent({
  name: "SnapshotCounter",
  id: { name: Schema.String },
  snapshotting: Snapshot.define({
    schema: Schema.Struct({ count: Schema.Number }),
    policy: Snapshot.policy.everyN(10),
  }),
  methods: { value: method({ input: {}, success: Schema.Number }) },
})

const handlers = (state: Ref.Ref<{ readonly count: number }>) => ({
  value: () => Ref.get(state).pipe(Effect.map((value) => value.count)),
})

definition.implement({
  init: () => Ref.make({ count: 0 }),
  methods: handlers,
  snapshot: Snapshot.ref<{ count: number }>(),
})

For SQLite snapshots, add databases: state => ({ main: state.database }) to the snapshot strategy; the SDK hydrates in-memory/temp images before constructing methods. DDL must be idempotent. A Snapshot.custom(...) implementation uses { save(state), restore(saved, context) } with raw bytes. Snapshot schema evolution remains the application's responsibility.

Auto snapshots can declare databases: ["main"] as const; expose the corresponding SDK SqliteClient or node:sqlite DatabaseSync from the implementation snapshot strategy. Every declared database must be present, be in autocommit mode, and have no extra attached schemas. External Postgres/MySQL/Ignite data is not part of a worker snapshot.

For schema state plus arbitrary named bytes, opt into Snapshot.multipart({ schema, policy, databases? }) and provide an explicit strategy. save(ref) returns an Effect of { state, parts: new Map([["index", { bytes, contentType: "application/octet-stream" }]]) }; restore(saved, context) receives decoded state and the complete user-part Map. yield* Snapshot.requirePart(saved.parts, "index", "application/octet-stream") checks both presence and MIME type. Binary-only applications use Schema.Null state. Logical names are case-sensitive [A-Za-z0-9_][A-Za-z0-9_.-]*; content types are bare ASCII MIME types without parameters. The SDK owns the envelope and principal, always emits multipart (including zero parts), and never interprets user parts as resources. Existing JSON and byte-only modes are unchanged.

Optional managed SQLite composition uses the same databases accessor in both Snapshot.define and Snapshot.multipart. DatabaseSync.location() classifies each handle: null means an in-memory/temp image; a non-null location is recorded in fileDatabases metadata only. Host filesystem snapshots exclusively own file-backed contents. The SDK never captures those contents as application parts or hydrates them from bytes, even when filesystem snapshotting is disabled. Filesystem snapshot availability is controlled by the host; without it, changed files prevent periodic snapshots and snapshot-based manual updates.

The SDK validates the complete, disjoint image/location inventory and checks recorded files exist before restore. That factory must reopen the recorded locations. All returned handles must match their recorded categories and locations. Memory/temp images hydrate and connection caches warm before methods; defer database-dependent reconstruction until then. Small nonempty file databases may be read in full during cache warming, with the bytes discarded rather than stored in the application snapshot. Use scoped database acquisition for cleanup if restoration fails. Raw handles and SqliteClient.fromDatabase wrappers remain externally owned unless you register a finalizer or request closeOnScopeClose: true. Arbitrary user parts remain application-owned. Snapshot hooks run unpersisted and may be retried. Whole-buffer copies amplify peak memory use; the host's 64 KiB inline/blob-spill threshold is not a size cap. See the multipart examples for all SDKs.

Agent streams

Use WitTypes.AgentStream(itemSchema) inside any input or output schema and pass native Effect Stream<T> values. Local streams are reusable. Streams received from Preview 3 endpoints are demand-driven, single-reader, and affine: consuming or forwarding one transfers ownership, so a received stream must not be reused.

Early break, return(), or Effect interruption closes the readable endpoint and awaits local cleanup. A downstream drop stops future source pulls and eventually calls the producer iterator's return() once, but P3 cannot interrupt an arbitrary pending next() or promise cleanup before a later invocation. Terminal errors are not transported; model recoverable failures as stream items (for example Result<T, E>). Streams cannot be snapshotted, triggered, or scheduled.

Effect HTTP routers

HttpRouter.define registers a parameterless ephemeral HTTP router, without snapshots or an ordinary agent client. Supply a real Effect application factory; it runs inside each request's scope. Normal typed agent clients remain available inside handlers:

import { Effect, Layer } from "effect"
import { HttpRouter as Routes, HttpServerRequest, HttpServerResponse } from "effect/http"
import { Http, HttpRouter } from "@golemcloud/effect-golem"

const routes = Layer.mergeAll(
  Routes.add(
    "GET",
    "/count",
    Effect.gen(function* () {
      const counter = yield* Counter.client.get({ name: "web" })
      return HttpServerResponse.text(String(yield* counter.value({})))
    }),
  ),
  Routes.add(
    "POST",
    "/echo",
    Effect.gen(function* () {
      const request = yield* HttpServerRequest.HttpServerRequest
      return HttpServerResponse.stream(request.stream)
    }),
  ),
)

HttpRouter.define("Web", {
  mount: Http.mount("/web", {
    fileResponseHeaders: { "content-security-policy": "default-src 'self'" },
  }),
  static: [{ route: "/assets/*", path: "/public/$1" }],
}).implement(Routes.toHttpEffect(routes))

fileResponseHeaders applies the supplied headers whenever the host serves live-agent exposed files or router static files.

The framework sees mount-relative URLs. yield* HttpRouter.request exposes the original scheme, authority, path, optional query, ordered byte headers, and the same single-consumer body. An absent query is undefined; a present empty query is "". Explicit text, json, and arrayBuffer accessors buffer with Effect's HttpIncomingMessage.MaxBodySize; ordinary stream handlers do not. Normalized Effect headers cannot represent every original occurrence. For repeated response fields or opaque bytes, apply HttpRouter.withRawHeaders(response, headers) after response combinators. Those overrides survive pre-response hooks. Effect cookies become separate set-cookie occurrences.

For an Effect HttpApi, use HttpApiBuilder.layer and HttpRouter.toHttpEffect for the application, and a lazy openApi: Effect.sync(() => OpenApi.fromApi(Api)) provider on the same definition. Provide the application's normal Effect layers, including the HttpPlatform, FileSystem, Path, and Etag requirements of HttpApiBuilder; static file mappings themselves are served by Golem, not those layers. OpenAPI paths stay mount-relative. The shared TypeScript serializer preserves the generated document; the host validates its semantics and handles mount composition. The optional handlerMethod and openapiProviderMethod change ordinary metadata method names. Use .register() instead of .implement(...) for static-only or provider-only routers.

Static mappings use { route, path } objects. Files must also be included in the component's deployment. For a regular durable non-phantom agent, use Http.mount("/files/{id}", { exposeFiles: [{ route: "/*", path: "/data/$1" }] }) to expose its filesystem. Mapping parsing and metadata compilation are shared with the TypeScript SDK.

Request resources stay scoped through response consumption. Endpoints decoded during a request are adopted by that request even when their streams are transformed. HEAD and 204/205/304 never start body programs; they release already-acquired endpoints and request resources. Put eager application resources in Effect.acquireRelease/Effect.addFinalizer, not solely in an unopened stream's ensuring. Scope finalizers receive the response's success, failure, or interruption. Effect Respondable failures retain their HTTP responses, including HttpApi validation failures. Other application failures remain invocation failures; router misses and request parsing failures become 404 and 400 responses. A late body failure must not be interpreted as successful invocation EOF. Raw response bodies other than Uint8Array, response FormData, and WebSocket upgrades are not supported by this adapter.

Runtime limitation: local Effect interruption joins finalizers, but current host epoch interruption does not deliver cooperative cancellation to JavaScript. The P3 output pump also cannot interrupt a pending first pull. These paths require runtime work and deployed acceptance tests; unit tests and artifact checks do not establish HTTP lifecycle conformance.

External Durable Streams

DurableStreams.readJson(schema, options) and readBytes(options) return lazy native Effect streams. They read finite host batches, starting with catch-up and following with long-poll (default) or SSE. Offsets and cursors are opaque. offset: "now" is resolved once per stream execution; an empty live batch is not EOF, and a closed batch emits its payload before EOF. Each execution of a local stream starts at its configured offset. Native agent-stream forwarding works without an iterator or Promise adapter.

Each Stream execution acquires one journaled reader resource and releases it at EOF, failure, interruption, or downstream termination. Writers acquire one journaled resource in the caller's Effect scope; keep that scope open through appends and retries. Constructors record immutable connection descriptors and the borrowed secret's pinned identity without making HTTP requests or revealing plaintext. Reads carry only checkpoint/transport/content-type; appends carry only payload/sequence/close. Repeated attempts reuse the same resource.

Scope interruption returns promptly. If a finite WIT attempt is still in flight, actual resource disposal waits for its Promise to settle, because the method still borrows the native handle. No new operations may start after scope exit; interrupting an append within an open writer scope still permits retryPending on the same resource.

Before passing a host-dependent stream to an agent method (or returning it through an agent-stream schema), capture its services within the invocation:

const forward = Effect.gen(function* () {
  const source = DurableStreams.readBytes({ url })
  const context = yield* Effect.context<Stream.Services<typeof source>>()
  return yield* sink.collect({ bytes: source.pipe(Stream.provideContext(context)) })
})
import { Effect, Schema, Stream } from "effect"
import { DurableStreams, defineConfig } from "@golemcloud/effect-golem"

class StreamConfig extends defineConfig("Stream.Config", {
  bearer: Schema.Redacted(Schema.String),
}) {}

const roundtrip = Effect.gen(function* () {
  const cfg = yield* StreamConfig
  const auth = yield* cfg.bearer.borrow // opaque handle; does not reveal the token
  const options = { url: "https://streams.example/events", auth }
  const writer = yield* DurableStreams.makeJsonWriter(Schema.String, options)
  yield* writer.append(["first", "last"], { close: true })
  return yield* DurableStreams.readJson(Schema.String, options).pipe(Stream.runCollect)
}).pipe(Effect.scoped)

Writers serialize concurrent effects and retain one immutable producer ID/epoch/sequence, body, and close flag after a failed or interrupted append. New data is rejected while hasPending is true: explicitly run writer.retryPending to resolve the same request. writer.close consumes a sequence too. The pending operation also retains its automatic retry budget and interrupted backoff; an explicit retry does not reset either. A receipt's nextOffset may be absent on a duplicate acknowledgement; there is no duplicate boolean. Finite host attempts have bounded retries (maxRetries, retryDelayMs, timeoutMs) and honor Retry-After. Fiber cancellation stops waiting but does not prove the remote append was cancelled; its retained request must still be resolved.

JSON uses the SDK's schema codecs and canonical JSON representation. Arrays inside a message stay arrays. The default codec rejects unsafe integers rather than rounding them. For exact unquoted 64-bit integers, use WitTypes.Uint64 with deterministic decode: BigInt / encode: String callbacks; callbacks receive one complete message and schema validation still applies.

Producer identity generation uses the runtime's durable randomness. Replay must reproduce the same construction and operation order; a fork retains its producer identity and must not be treated as an independent producer. These stateful writer objects and live readers are not snapshot values. Unit-test host fakes validate SDK state transitions, not executor crash recovery.

Generated Effect bridges

Effect components receive Effect-native guest clients for their manifest dependencies.agents and dependencies.tools during golem build. These clients preserve the target's schema without requiring a duplicate defineAgent or tool definition. Generated methods take positional arguments, unlike definition-derived clients, which take an input object.

For example, a generated guest client for a peer with a nested streaming method is used inside an agent handler as follows:

import { Effect, Stream } from "effect"
import { TsPeer } from "ts-peer-guest-client"

const program = Effect.scoped(
  Effect.gen(function* () {
    const peer = yield* TsPeer.get("peer-1")
    const input = Stream.make({ id: 1, values: [9, 12] })
    const output = yield* peer.nestedStream("fx", input)
    return yield* output.items.pipe(Stream.runCollect)
  }),
)

Guest bridges use the same affine received streams and capability handles as the SDK. Their host service requirements flow through the Effect environment; the agent dispatcher supplies live services. Stream-free methods also expose .trigger(...) and .schedule(...), with cancelable scheduling. Streaming methods cannot be triggered or scheduled.

Generated tool clients expose independent Effect stdin/stdout/stderr streams and an Effect result. Consume both declared outputs and the result concurrently when the tool can block writing output:

import { Effect, Stream } from "effect"
import { TsCrossStreamingClient } from "ts-cross-streaming-tool-guest-client"

const program = Effect.scoped(
  Effect.gen(function* () {
    const invocation = yield* TsCrossStreamingClient.create().ts_cross_streaming(
      "label",
      Stream.make(new TextEncoder().encode("hello")),
    )
    return yield* Effect.all(
      [invocation.result, invocation.stdout.pipe(Stream.decodeText(), Stream.runCollect)],
      { concurrency: "unbounded" },
    )
  }),
)

Scope closure detaches result observation and releases owned output streams without cancelling the invocation. Run invocation.cancel explicitly to cancel accepted work. Tool failures retain their declared payloads separately from RPC failures, and decoding validates the exact schema graph.

For an external Effect application, request an external bridge in the manifest and run golem generate-bridge --language effect:

bridge:
  effect:
    external:
      agents: my-app:producer
      outputDir: bridge/effect

Each generated <agent>-client directory is a buildable npm package. Its configure function uses the same configuration as the TypeScript bridge. Constructors and calls return Effects, streaming values are Effect Streams (including nested streams), and streaming calls require a scope. Fiber interruption aborts pending calls; successful streaming calls keep their transport alive until scope closure. External stream-free trigger/schedule methods use generated suffixes such as echotrigger(...) and echoschedule(...). External bridges use the canonical TypeScript HTTP and WebSocket transport and do not depend on guest WIT modules. As with the TypeScript generator, external bridges support agents; tool bridges are guest-only.

Tools and permission-card transfer

Tool.toolDefinition(name) builds nested, typed commands. .implement(...) registers a tool guest; Tool.client(definition) derives a camel-cased Effect client. Bodies define positional/named input, declared errors, a structured return, and optional stdin/stdout/stderr byte streams.

import { Effect, Schema } from "effect"
import { Tool } from "@golemcloud/effect-golem"

const Echo = Tool.toolDefinition("echo").body((body) =>
  body.positional("message", Schema.String).returns(Schema.String),
)

Echo.implement({
  echo: ({ message }) => Effect.succeed(message),
})

const echo = Tool.client(Echo)
const result = yield * echo({ message: "hello" })

Use Tool.err(name, value) for a declared tool error and Tool.ok(value) where an explicit success carrier is needed. After success or a declared error, implicit output completion attempts both writers independently; a failed finish or release never replaces that structured outcome. Explicit unhandled output failures and cancellation still fail the invocation. Handler exceptions and defects trap; malformed returned results and undeclared returned errors report invalid-result.

For a tool selected at runtime, Reflection.getToolType(name) and getAllToolTypes discover caller-visible registrations. The selected command exposes its canonical path, aliases, ordered arguments, input schema, result schema, and child commands. Namespace-only nodes remain visible but have no callable body.

import { Effect } from "effect"
import { Reflection } from "@golemcloud/effect-golem"

const call = Effect.gen(function* () {
  const tool = yield* Reflection.getToolType("echo")
  if (tool) {
    const command = tool.client.command([])
    return yield* command.invokeJson({ message: "hello" })
  }
})

invokeJson and invokeValue validate inputs before opening RPC and check declared outputs. Canonical JSON records include every argument key; use null for absent optional values. startJson and startValue expose scoped stdout, stderr, result, concurrent collection, cancellation, and detach. Generated streaming clients expose the same operations. The result is single-use: concurrent or repeated observation fails without opening another host get. detach stops result delivery and releases unclaimed capabilities, including a result arriving after detach. Physical observer disposal waits for an active host get to settle; cancel after physical disposal is a no-op. Neither detach nor closing one output cancels the invocation or its sibling output. Closing an output is permanent; there is no resumable pending-read guarantee. Whole-call interruption of typed clients, reflected invoke*, or generated .collect still cancels the invocation. Use started calls when either output is required and drain both outputs concurrently. Reflection.DynamicToolClient accepts a caller-packed value when the deployed schema is unavailable and does not infer validation rules. Reflected failures are typed Effect errors, including ToolReflectionError for malformed output.

WitTypes.PermissionCard (permission-card { polymorphic: false }) represents a concrete permission card, whose grants name concrete owners and resources. WitTypes.PolymorphicPermissionCard (polymorphic: true) represents a template card, whose grants may contain slot variables (?agent, ?env, ...) that resolve for each holder. A schema encodes only cards of its own kind, and a decoded card takes the type that the parameter or result declares. The host refuses an agent call input or output whose card does not match the declared kind. Cards are opaque affine capabilities: successful encoding transfers the exact handle across agent RPC, tool calls, and middleware. The sender must not inspect or reuse a transferred card. Transactional graph encoding does not consume it if validation or another sibling conversion fails, so a corrected retry remains possible. Use Permissions to inspect, derive, scope, revoke, and install cards.

Decoding never turns plain data into a card: the card schemas, Bridge.permissionCardFromSchemaValue, and Bridge.polymorphicPermissionCardFromSchemaValue accept only cards that the SDK lifted from a host value. The SDK has no conversion API between PermissionCard, PolymorphicPermissionCard, and ScopeCard. A card that a schema decodes takes the type that the schema declares, also after an encode in the same component; the host keeps the real kind of the card, so this gives no authority. A ScopeCard never encodes. The SDK keeps its card registry on globalThis, because the main and middleware bundles share cards. Code in the same component can therefore reach the registry and alias a host resource into two card objects. This does not create authority: the host checks every use, and the second use of a moved resource fails.

Permission-card management

Permissions wraps golem:[email protected]. Every function returns an Effect that requires PermissionsClient, which the agent runtime provides. Expected host failures are PermissionError values whose kind is the WIT case, such as "not-permitted" or "install-recipient-mismatch". Moved cards, invalid holders, and unexpected host exceptions are PermissionsHostError values. The id, parents, and expiresAt accessors trap in the host when the card is unknown or revoked.

import { Effect } from "effect"
import { Permissions } from "@golemcloud/effect-golem"

const grant = {
  class: "kv",
  owner: "?env",
  recipient: "*",
  verb: "read",
  resourceId: "store.key",
}

const program = Effect.gen(function* () {
  const card = yield* Permissions.deriveFromWallet({
    lowerPositive: [grant],
    upperPositive: [grant],
  })
  const view = yield* Permissions.inspect(card)
  const child = yield* Permissions.derive(
    card,
    { lowerPositive: view.lowerPositive, upperPositive: view.upperPositive },
    Permissions.Lifespan.fromDuration("1 hour"),
  )
  const wallet = yield* Permissions.walletScoped
  const scope = yield* Permissions.deriveScope(
    wallet.map((entry) => entry.card),
    {
      lowerPositive: [grant],
      upperPositive: [grant],
    },
  )
  // Reader is a defineAgent spec whose read method needs the kv grant;
  // ReaderClient is the matching defineAgentClient definition.
  const reader = yield* Reader.client.get({ name: "reader" })
  yield* reader.read.withScopeCard(scope)({ key: "key" })
  const holder = yield* Permissions.agentHolder(yield* ReaderClient.agentId({ name: "reader" }))
  yield* Permissions.install(child, holder)
  return yield* Permissions.revoke(card)
}).pipe(Effect.scoped)
  • wallet returns WalletCard entries, a closed union labelled with the card kind: { _tag: "Concrete", card: PermissionCard }, { _tag: "Template", card: PolymorphicPermissionCard }, or { _tag: "Scope", card: ScopeCard }. A Scope entry is the scope card lent to the current invocation; the host lists it only when the agent holds card:inspect.
  • derive and deriveFromWallet return PersistentCard (PermissionCard | PolymorphicPermissionCard). The host determines the child kind. Use Schema.is(WitTypes.PermissionCard) or Schema.is(WitTypes.PolymorphicPermissionCard) before passing it to a kind-specific schema. Parent templates resolve in the caller's wallet. Child slots name the eventual holder and the host validates them universally, not just against the caller. Slot tool owners do not resolve aliases using caller metadata.
  • deriveScope returns a ScopeCard, which is a separate type from PermissionCard. Its parents are persistent cards or scope cards of the current invocation. A scope card supports id, parents, expiresAt, inspect, release, and use as a deriveScope parent. It cannot be revoked, installed, derived from with derive, or sent as an agent-method argument or result. Scope derivation rejects slots with invalid-pattern; scope cards remain concrete.
  • expiresAt of a scope card is the earliest expiry of the root cards of this scope card, or none when no root card expires. The scope card stops authorizing then.
  • A scope card is valid only during the invocation that derived it. If you keep one, or a client whose default is one, for a later invocation, the host denies its awaited calls with an RpcCallError whose cause is denied.
  • derive treats the positive bounds as the parent grants to retain and the negative bounds as denials to add. deriveFromWallet and deriveScope take the complete requested lattice. Omitted bound lists are empty.
  • Lifespan.fromIso8601("P30D") and Lifespan.fromDuration(...) build lifespans. The host accepts durations in days and time units (PnDTnHnMnS) and rejects calendar units. fromDuration rejects infinite, zero, and negative durations and sends whole and fractional seconds.
  • id, parents, expiresAt, inspect, derive, and deriveScope borrow the card. revoke, install, and release move the card before the host call, because the host drops the handle even when the call fails. Any later use fails with PermissionsHostError.
  • CardId is opaque. Compare IDs with Equal.equals or use them in Effect collections. Printing, logging, and JSON serialization show CardId(<redacted>).
  • wallet returns owned cards; release the ones that you do not keep with release, or use walletScoped to release them when the scope closes.
  • install accepts Holder.agent(...) or agentHolder(identity). The host rejects account and app holders with not-permitted.
  • Scope cards attach only to awaited calls: method.withScopeCard(scope)(input) for one call, or the scopeCard option of get, getPhantom, newPhantom, and bindWithConfig for every awaited call of a client. DynamicMethod.invoke and the reflected invoke and invokeValue take a { scopeCard } option for one call. Generated Effect bridges have client.withScopeCard(scope) for a client default and client.method.withScopeCard(scope)(...args) for one call. Attaching borrows the card. Triggers, schedules, and tool calls never carry a scope card.
  • The SDK checks the scope card before it encodes the call input. A released scope card fails the call with ScopeCardUnavailableError, and no argument card is moved.
  • When an awaited call fails or is interrupted before the host receives its input, the SDK gives the permission cards, secrets, and quota tokens that it encoded back to their handles. A card released while it was encoded is dropped instead. Streams in the input stay moved, and the SDK closes their wire side. After the host call starts, nothing is given back, even when the call fails. DynamicMethod.invoke never changes a caller-supplied tree; on failure the caller still owns its resources.

Restrictive evaluator permissions

Pure agent initialization does not read self metadata and does not require self-view permission. For an ephemeral evaluator, keep init and the handler independent of host services, declare no tools or secrets, and keep agent operations outside the initialCard upper bound. Grant only the operations needed by the evaluator runtime; for nested JavaScript runtime startup, this can include filesystem stat on the agent's root. The upper bound is a ceiling, not a grant: each required operation must also be admitted by the effective permission card.

Self metadata is read in these cases:

| Operation | Self-view required? | | -------------------------------------------------------------------------- | ---------------------------------------------------- | | Pure initialization and methods without metadata or identity reads | No | | Agents.getSelfMetadata | Yes | | SelfAgentId.SelfAgentId, including when used in init | Yes | | Restoration with an explicit snapshot strategy, including Snapshot.ref() | Yes, to populate the restoration context's agentId | | Plain schema-state restoration without a snapshot strategy | No implicit self-metadata read |

Every snapshot load also reads GOLEM_AGENT_ID through the WASI environment API, including plain schema-state restoration. This requires environment-read permission for that variable independently of self-view; neither permission grants the other.

SelfAgentId.SelfAgentId extracts the identity from the full metadata response; it is not an identity-only host operation. If self-view is needed for trusted code, the permission target is agent(?agent) @ ?agent : view : *. The effective card must admit that operation and the initialCard upper bound must permit it. Do not add this grant merely to initialize a pure evaluator.

Self metadata includes stored environment and configuration fields. The metadata host operation checks agent view permission, but does not apply the per-key env/config permission filters used by WASI reads. An empty or denied WASI environment/configuration read therefore does not establish that those fields are inaccessible through metadata. Keep self-view denied for untrusted code that must not read those fields; do not treat self-view as a harmless bootstrap permission.

Agents.getSelfMetadata returns an AgentsHostError in the Effect error channel and retains the host failure as its cause. SelfAgentId.SelfAgentId preserves its defect-based failure contract, including during restoration with an explicit snapshot strategy. Neither path changes the card or retries with broader permissions. Sagas retain original failures when compensation fails.

Tool middleware

Standalone middleware imports only the middleware-safe entry point:

import { NoParameters, universal } from "@golemcloud/effect-golem/middleware"

universal({
  name: "audit",
  parameters: NoParameters,
  handler: (invocation, underlying) =>
    underlying.invoke(invocation.commandPath, invocation.input, invocation.stdin),
})

universal transparently handles any tool using wire values. Every middleware declares an Effect Schema for installation parameters; decoded values are available as invocation.parameters or typed-handler context.parameters. NoParameters is the explicit empty-record schema. typed({ parameters, presented, expected?, handler }) projects typed input/output/error and a definition-derived context.underlying client; it can present a different definition from the wrapped tool. Both support aliases, docs, and a per-invocation Effect layer. Underlying access and streams are affine and valid only for that invocation.

parameters must be a static Effect Schema (streams, secrets, and other installation-time-ineligible capabilities are rejected). underlying.start(...) and typed command .start(...) return scoped started invocations whose get, optional stdout and stderr, and cancel effects are independent. Calls may overlap; consume both outputs and get concurrently when needed. Scope closure disposes the observer and owned streams but does not cancel the tool call—run started.cancel explicitly to cancel it.

When a handler returns, the underlying rejects new admissions but does not implicitly cancel calls already admitted. Cleanup releases their observers after pending observation is safe. For a command declaring stdout or stderr, return/select each stream with the matching typed context.stdout/context.stderr callback (or universal result field). The SDK forwards each into its host-provided writer concurrently, calls finish after clean EOF, and calls fail on forwarding failure; middleware never owns those writers directly.

The single agent-guest build world exports agents, tools, snapshots, and tool middleware. It supports ordinary, standalone-middleware, and combined components; discovery returns empty lists for categories a component does not define. Middleware authoring APIs remain available from @golemcloud/effect-golem/middleware, and invocation-scoped underlying access still advances the pinned middleware chain. The CLI accepts middleware metadata and manifest attachment; runtime traversal and invocation behavior remain separate deployment concerns.

Durability 1.6

Durability.wrap and wrapInfallible use golem:durability/[email protected] custom durable invocation resources. A live call begins with its typed request and finishes exactly once with a typed response. forcedCommit: true asks the host to commit after finish. Replay returns the recorded response without executing the body.

Sequential execution only. Both wrappers support properly nested operations and a body that awaits host calls when no unrelated guest work runs during the wait. Do not overlap a custom operation with other fibers, concurrent effects, or JavaScript promises, even if they only use ordinary host APIs. The custom scope remains installed while waiting, so sibling effects can be attributed to the wrong operation and cause incorrect replay or invocation failure. Misuse is not reliably detected. Await each nested operation and do not detach work. Stop and join outstanding work before the body exits, including on failure or interruption; structured finalization alone does not make concurrent custom scopes supported. Ordinary built-in durability is not subject to this restriction when no custom operation is active.

On typed success or typed failure, wrap encodes the response and calls finishCustomDurableInvocation. On defect, interruption, response encoding failure, or any other unfinished exit, scope cleanup drops the live invocation instead of finishing it. This is the 1.6 recovery contract: never manufacture a terminal result for work that did not complete. The four function types are readLocal, writeLocal, readRemote, and writeRemote. The host records nesting in Start.parent_start_index, not in a function-type payload.

Other durability controls remain under Durability: oplog commits and FunctionType constructors. Saga provides fallible/infallible compensating transactions; compensations run in reverse order.

Saga.infallibleTransaction compensates typed failures, caught defects and interruption, then asks the host for a bounded retry decision. Attempts append new durable calls without deleting prior calls or restoring JavaScript state. Compensate local state explicitly. Do not reuse a compensated external operation's idempotency key for a new attempt: the remote peer can suppress that operation. The host resolves the saga policy for the agent URI after each compensated failure, using attempt and error-kind properties. No selected policy uses config.retry. The host rejects policy changes and unbounded policies. Exhaustion, policy errors and incomplete compensation return SagaRetryFailure in the typed error channel. Compensation runs under an interruption mask. Saga.fallibleTransaction reports complete or partial compensation instead of retrying, and preserves defects and interruption after complete rollback. Uncatchable guest traps cannot run guest compensation. Agents.revertAgent remains an external management operation; management self-revert is unsupported.

Existing Effect integrations

  • Http: agent HTTP mount/endpoint metadata, auth/CORS/header/query bindings, and verb helpers. The host serves requests. Webhook.create creates a host webhook and offers typed payload decode.
  • Websocket: scoped host WebSocket client integrated with Effect streams.
  • KeyValue and Blobstore: scoped WASI storage clients with schema-typed views; listing is a stream. Stored decode errors remain typed failures.
  • @golemcloud/effect-golem/sqlite: node:sqlite adapter implementing Effect's unstable SqlClient. Native N-API drivers do not run in the WASM runtime.
  • @golemcloud/effect-golem/postgres, /mysql, /ignite2: host-backed Effect SQL clients with tagged templates, transactions, streaming (for RDBMS), dialect helper values, and optional temporal-to-Date decoding. Postgres/MySQL nested transactions use savepoints; Ignite rejects nesting.
  • Quota: scoped reservation/commit helpers and affine quota token split/merge.
  • Retry, Oplog, Logging, and Tracing: Effect wrappers over their host APIs. Logging/tracing layers are applied automatically to constructors, restoration, handlers, and custom snapshots.

These APIs are covered individually, but complete cross-language 1.6 acceptance is not claimed until the final parity matrix passes.

Build and verify in the monorepo

The base WASM retains the single full agent-guest world and a shared Effect runtime. The CLI's Rollup configuration uses @golemcloud/effect-golem/build to discover retained capability modules without executing user code, then generates a static entrypoint. Agent, tool, and middleware hooks import their implementations only when needed; absent capabilities have explicit empty discovery and error bodies. No roles or world selection are required. The component bundle includes only reachable SDK code and adapters, rather than importing the full SDK from the base WASM. capabilities.json beside the bundle records the selection. Selection is conservative: a retained definition can keep its capability even when its registration is conditional or never executed.

Agent, config, and tool definitions are ordinary runtime functions. The build does not evaluate their metadata or rewrite DSL calls; schemas, descriptors, and codecs are constructed by the SDK when the application runs. Metadata may use imported declarations and normal JavaScript computation.

Prerequisites are Node/npm, Rust with wasm32-wasip2, wasm-rquickjs, WASI SDK, and Golem's normal build prerequisites. From sdks/effect:

The private sdks/http-contract source package is installed by npm ci and bundled by this SDK's build. No TypeScript SDK installation or sibling SDK build is required. Published packages contain the helper code and declarations, not a runtime dependency on that private package.

npm ci
npm run lint
npm run format:check
npm run typecheck
npm test
npm run build
npm run build:bundle
$env.WASI_SDK_PATH = "/opt/wasi-sdk"
npm run build-agent-template # builds the default world
npm run check:dts
npm run check:contracts
npm run check:artifacts

build-agent-template creates/checks agent_guest.wasm. check:artifacts compares committed/generated provenance and fails on stale artifacts.

Canonical WIT dependencies live at repository-root wit/deps; never edit sdks/effect/wit/deps by hand. From the repository root:

cargo make wit          # mirror canonical WIT into every SDK
cd sdks/effect
npm run generate-dts    # regenerate declarations for the default world
npm run check:dts       # fail if generated declarations drift
npm run check:artifacts # fail if bundles/templates/WASM drift

For a focused real-runtime check, build the default template first, then run the relevant harness case under integration-test. Unit tests use injectable host-service layers under src/host; they do not replace a real WASM integration check.

Durable Streams runtime fixture

The focused durable-streams harness case starts an in-process HTTP peer on an ephemeral local port. Run it with a local worker executor exposing golem:agent/[email protected]; no Docker or external stream server is needed. The peer commits the first JSON append but drops its response, checks the retry's identical producer tuple/body/close, and acknowledges the duplicate without an offset. The bytes writer appends and then closes on the same resource; the reader follows an opaque checkpoint across two batches. The case verifies exact JSON/byte payloads and eleven authenticated HTTP requests, including the cancellation check below.

cancelRead starts a scoped native read whose response the peer holds. A separate control read confirms that the HTTP request arrived before the guest interrupts the fiber. Interruption must finish within two seconds with no defect, before the guest asks the peer to release its response. The peer bounds the hold to ten seconds. readAfterCancel then reads through a fresh resource in the same agent instance, checking that delayed resource cleanup did not leave the guest unusable.

durable-streams.golem.yaml builds only the fixture and its native-stream sink. It provisions the test-only secret durableStreamToken. Each method borrows that capability once and shares it between append and read without revealing it. After building fresh SDK bundles and all templates, run from sdks/effect/integration-test against a local Golem server:

npm ci
export GOLEM_APP_MANIFEST_PATH="$PWD/durable-streams.golem.yaml"
golem --local --yes build
golem --local --yes deploy
npm run test:integration -- --filter '^durable-streams$' --no-infra --no-server --no-build

To prepare the consumer without the Golem CLI, bundle only its entrypoint and inject it directly (from sdks/effect/integration-test):

(cd components/agents && \
  GOLEM_APP_ROOT="$PWD/../.." GOLEM_TEMP="$PWD/../../golem-temp" \
  GOLEM_COMPONENT_NAME=effect-golem-durable-streams \
  GOLEM_COMPONENT_ENTRY=./src/durable-streams-agent.ts \
  npx --no rollup -- -c ../../rollup.config.component.mjs)
mkdir -p golem-temp/agents
wasm-rquickjs inject-js --input ../wasm/agent_guest.wasm \
  --js golem-temp/ts-dist/effect-golem-durable-streams/main.js \
  --output golem-temp/agents/effect_golem_durable_streams.dynamic.wasm

Expected results are [["first", "a,b"], ["last"]] and [3, 249, 17]. forwardBytes consumes the external source in a separate agent through native Preview 3 stream RPC. The cancellation and subsequent invocation return [29] and [41, 203]. The harness creates fresh peer state and agent names on every run. This case exercises real host calls and uncertain acknowledgements, not executor crash recovery. Restart/replay acceptance additionally needs peer request counters and oplog inspection; returning the same values alone does not prove HTTP was not repeated. The unit reconstruction test only checks the SDK's deterministic-call assumptions.

Packaging and release convention

The committed package manifest follows the TypeScript SDK convention and stays at 0.0.0; release automation derives the publish version from a trusted root tag named golem-effect-v<semver>. The root workflow (not an SDK-local .github workflow) performs a clean install, all quality gates, all-world builds, declaration/artifact checks, and npm trusted publishing with provenance.

Before publishing, check:package must pack the package and smoke-test the tarball from a clean consumer directory, including every public entry point and all three WASM artifacts. A release dry run validates the tarball without publishing. Publishing remains a separately authorized action.

Host testing seam

Every WIT call is behind a Context.Service wrapper in src/host, assembled once by HostLive. Public combinators let those requirements flow through their Effect environment; the dispatcher provides the live layer and erases host services before user handlers are exposed. Tests provide fake layers rather than mutating module globals. src/host is an internal seam, not a supported package import.

Compatibility

This is the direct Golem 1.6 API. There are no 1.5 source aliases, old wire fallbacks, or persisted format compatibility promises.