Skip to content

Platforms

Node.js

@yielded/agent-platform-node stores thread history and pending work in SQLite. A bounded worker pool executes registered agents and recovers work after a restart.

bun add @yielded/agent-platform-node@beta effect

Keep framework packages at one release and use compatible Effect and provider packages.

Save this as node-agent.ts. The model’s client reads OPENAI_API_KEY from the environment. The version declarations identify the agent, model, and tools used by accepted work.

node-agent.ts
import {
import OpenAiClient
OpenAiClient
,
import OpenAiLanguageModel
OpenAiLanguageModel
} from "@effect/ai-openai";
import {
import Agent
Agent
} from "@yielded/agent";
import {
import Config
Config
,
import Layer
Layer
,
import Schema
Schema
} from "effect";
import {
import Toolkit
Toolkit
} from "effect/ai";
import {
import FetchHttpClient
FetchHttpClient
} from "effect/http";
export const
const planner: Agent.Definition<Schema.String, Schema.Struct<{
readonly itinerary: Schema.$Array<Schema.String>;
}>, "Plan a trip with one itinerary entry per day.", Toolkit.Toolkit<{}>, undefined, undefined, undefined> & {
readonly id: Brand<"@effect-agent/core/AgentId"> & "trip-planner";
}
planner
=
import Agent
Agent
.
function make<"trip-planner", Schema.String, Schema.Struct<{
readonly itinerary: Schema.$Array<Schema.String>;
}>, "Plan a trip with one itinerary entry per day.", Toolkit.Toolkit<{}>, undefined>(id: "trip-planner", options: Agent.DefinitionOptions<Schema.String, Schema.Struct<{
readonly itinerary: Schema.$Array<Schema.String>;
}>, "Plan a trip with one itinerary entry per day.", Toolkit.Toolkit<{}>, undefined, undefined, undefined> & {
readonly inputPrompt?: undefined;
readonly runDisposition?: undefined;
}): Agent.Definition<...> & {
...;
} (+3 overloads)

Validate an agent ID and return a shallowly frozen, model-agnostic definition.

make
("trip-planner", {
DefinitionOptions<String, Struct<{ readonly itinerary: $Array<String>; }>, "Plan a trip with one itinerary entry per day.", Toolkit<{}>, undefined, undefined, undefined>.input: Schema.String
input
:
import Schema
Schema
.
const String: Schema.String

Type-level representation of

String

.

Schema for string values. Validates that the input is typeof "string".

@category ― models

@since ― 4.0.0

@category ― schemas

@since ― 4.0.0

String
,
DefinitionOptions<String, Struct<{ readonly itinerary: $Array<String>; }>, "Plan a trip with one itinerary entry per day.", Toolkit<{}>, undefined, undefined, undefined>.output: Schema.Struct<{
readonly itinerary: Schema.$Array<Schema.String>;
}>
output
:
import Schema
Schema
.
function Struct<{
readonly itinerary: Schema.$Array<Schema.String>;
}>(fields: {
readonly itinerary: Schema.$Array<Schema.String>;
}): Schema.Struct<{
readonly itinerary: Schema.$Array<Schema.String>;
}>

Defines a struct schema from a map of field schemas.

Details

Each field value is a schema. Use

optionalKey

or

optional

to mark fields as optional, and

mutableKey

to mark them as mutable.

The resulting schema's Type is a readonly object type with the fields' decoded types. The Encoded form mirrors the field schemas' encoded types. Declared fields may be inherited and are copied to own properties in the output. The __proto__ field is accepted only when it is an own property. Parsing does not guarantee that output keys retain their input order.

Example (Defining a basic struct)

import { Schema } from "effect"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number,
email: Schema.optionalKey(Schema.String)
})
// { readonly name: string; readonly age: number; readonly email?: string }
type Person = typeof Person.Type
Schema.decodeUnknownSync(Person)({ name: "Alice", age: 30 }) // => { name: "Alice", age: 30 }

@category ― constructors

@since ― 3.10.0

Struct
({
itinerary: Schema.$Array<Schema.String>
itinerary
:
import Schema
Schema
.
Array<Schema.String>(self: Schema.String): Schema.$Array<Schema.String>
export Array

Defines a ReadonlyArray schema for a given element schema.

Example (Defining an array of strings)

import { Schema } from "effect"
const schema = Schema.Array(Schema.String)
Schema.decodeUnknownSync(schema)(["a", "b", "c"]) // => ["a", "b", "c"]

@category ― constructors

@since ― 4.0.0

Array
(
import Schema
Schema
.
const String: Schema.String

Type-level representation of

String

.

Schema for string values. Validates that the input is typeof "string".

@category ― models

@since ― 4.0.0

@category ― schemas

@since ― 4.0.0

String
) }),
DefinitionOptions<String, Struct<{ readonly itinerary: $Array<String>; }>, "Plan a trip with one itinerary entry per day.", Toolkit<{}>, undefined, undefined, undefined>.instructions: "Plan a trip with one itinerary entry per day."
instructions
: "Plan a trip with one itinerary entry per day.",
DefinitionOptions<String, Struct<{ readonly itinerary: $Array<String>; }>, "Plan a trip with one itinerary entry per day.", Toolkit<{}>, undefined, undefined, undefined>.toolkit: Toolkit.Toolkit<{}>
toolkit
:
import Toolkit
Toolkit
.
const empty: Toolkit.Toolkit<{}>

An empty toolkit with no tools.

When to use

Use when you need an empty starting point for building toolkits or a default toolkit value that can be extended with merge.

@stability ― unstable

@category ― constructors

@since ― 4.0.0

empty
,
});
export const
const ModelLive: Model<"openai", LanguageModel, OpenAiClient.OpenAiClient>
ModelLive
=
import OpenAiLanguageModel
OpenAiLanguageModel
.
const model: (model: (string & {}) | OpenAiLanguageModel.Model, config?: Omit<{
readonly metadata?: {
readonly [x: string]: string;
} | undefined;
readonly top_logprobs?: number | undefined;
readonly temperature?: number | undefined;
readonly top_p?: number | undefined;
readonly user?: string | undefined;
readonly prompt_cache_key?: string | undefined;
readonly prompt_cache_options?: {
readonly mode?: "explicit" | "implicit" | undefined;
readonly ttl?: "30m" | undefined;
} | undefined;
... 17 more ...;
readonly useItemReferences?: boolean | undefined;
}, "model">) => Model<"openai", LanguageModel, OpenAiClient.OpenAiClient>

Creates an OpenAI model descriptor that can be provided with Effect.provide.

When to use

Use when you want an OpenAI language model value that carries provider and model metadata and can be supplied directly to an Effect program.

@see ― layer for creating a LanguageModel.LanguageModel layer directly

@see ― make for constructing the language model service effectfully

@stability ― unstable

@category ― constructors

@since ― 4.0.0

model
("gpt-6-luna");
export const
const OpenAiLive: Layer.Layer<OpenAiClient.OpenAiClient, Config.ConfigError, never>
OpenAiLive
=
import OpenAiClient
OpenAiClient
.
const layerConfig: (options?: {
readonly apiKey?: Config.Config<Redacted<string> | undefined> | undefined;
readonly apiUrl?: Config.Config<string> | undefined;
readonly organizationId?: Config.Config<Redacted<string> | undefined> | undefined;
readonly projectId?: Config.Config<Redacted<string> | undefined> | undefined;
readonly transformClient?: ((client: HttpClient) => HttpClient) | undefined;
}) => Layer.Layer<OpenAiClient.OpenAiClient, Config.ConfigError, HttpClient>

Creates a layer for the OpenAI client from provided Config values.

When to use

Use when you need client settings for OpenAI-compatible APIs to be read from Effect Config values while providing OpenAiClient as a Layer.

Details

Only config values supplied in options are loaded. Omitted fields are passed to make as undefined, and transformClient is forwarded as a plain option.

@see ― make for constructing the client service effectfully

@see ― layer for providing the client from already-resolved options

@stability ― unstable

@category ― layers

@since ― 4.0.0

layerConfig
({
apiKey?: Config.Config<Redacted<string> | undefined> | undefined

The config value to load for the API key.

apiKey
:
import Config
Config
.
function Redacted(name?: string): Config.Config<import("effect/Redacted").Redacted<string>>

Creates a config for a redacted string value. The parsed result is wrapped in a Redacted container that hides the value from logs and toString.

When to use

Use to read secret string settings that should not be exposed in logs or string output.

Details

Shortcut for Config.schema(Schema.Redacted(Schema.String), name).

Example (Reading a secret)

import { Config, ConfigProvider, Effect } from "effect"
const program = Config.Redacted("API_KEY").pipe(Effect.map(String))
const provider = ConfigProvider.fromEnv({
env: {
API_KEY: "sk-1234567890abcdef"
}
})
Effect.runSync(
program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider))
) // => "<redacted>"

@see ― String for non-secret string settings

@category ― constructors

@since ― 2.0.0

Redacted
("OPENAI_API_KEY"),
}).
Pipeable.pipe<Layer.Layer<OpenAiClient.OpenAiClient, Config.ConfigError, HttpClient>, Layer.Layer<OpenAiClient.OpenAiClient, Config.ConfigError, never>>(this: Layer.Layer<OpenAiClient.OpenAiClient, Config.ConfigError, HttpClient>, ab: (_: Layer.Layer<OpenAiClient.OpenAiClient, Config.ConfigError, HttpClient>) => Layer.Layer<OpenAiClient.OpenAiClient, Config.ConfigError, never>): Layer.Layer<...> (+21 overloads)
pipe
(
import Layer
Layer
.
const provide: <never, never, HttpClient>(that: Layer.Layer<HttpClient, never, never>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, E2, Exclude<RIn2, HttpClient>> (+3 overloads)

Feeds the output services of the dependency layer into the requirements of this layer, returning a layer that only provides the services from this layer.

When to use

Use when you need to hide an implementation dependency layer from callers.

Details

In serviceLayer.pipe(Layer.provide(dependencyLayer)), the dependency layer is built first and is used to satisfy the requirements of serviceLayer.

Example (Providing layer dependencies)

import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
class UserService extends Context.Service<UserService, {
readonly getUser: (id: string) => Effect.Effect<{
id: string
name: string
}>
}>()("UserService") {}
class Logger extends Context.Service<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>()("Logger") {}
// Create dependency layers
const databaseLayer = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`))
})
const logs: Array<string> = []
const loggerLayer = Layer.succeed(Logger, {
log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => logs.push(`[LOG] ${msg}`)))
})
// UserService depends on Database and Logger
const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() {
const database = yield* Database
const logger = yield* Logger
return {
getUser: Effect.fn("UserService.getUser")(function*(id: string) {
yield* logger.log(`Looking up user ${id}`)
const result = yield* database.query(
`SELECT * FROM users WHERE id = ${id}`
)
return { id, name: result }
})
}
}))
// Provide dependencies to UserService layer
const userServiceWithDependencies = userServiceLayer.pipe(
Layer.provide(Layer.mergeAll(databaseLayer, loggerLayer))
)
// Now UserService layer has no dependencies
const program = Effect.gen(function*() {
const userService = yield* UserService
return yield* userService.getUser("123")
}).pipe(
Effect.provide(userServiceWithDependencies)
)
Effect.runSync(program) // => { id: "123", name: "DB: SELECT * FROM users WHERE id = 123" }
logs // => ["[LOG] Looking up user 123"]

@see ― provideMerge for retaining the dependency services

@category ― providing services

@since ― 2.0.0

provide
(
import FetchHttpClient
FetchHttpClient
.
const layer: Layer.Layer<HttpClient, never, never>

Layer that provides an HttpClient implementation backed by the configured Fetch function.

When to use

Use when an Effect program should execute HttpClient requests through the platform fetch implementation, especially in browser, edge, or Node.js runtimes with globalThis.fetch.

Details

The layer uses the current Fetch reference and optional RequestInit service for each request. Request-specific method, headers, body, and abort signal are supplied by the client and override matching RequestInit fields.

Gotchas

Fetch behavior comes from the runtime's implementation, so CORS, cookies, redirects, abort handling, and streaming support can vary by platform. Stream request bodies are sent as Web streams with duplex: "half", and any content-length header is removed before calling fetch.

@see ― Fetch for supplying the fetch implementation used by this layer

@see ― RequestInit for default RequestInit options applied before request-specific fields

@stability ― unstable

@category ― layers

@since ― 4.0.0

layer
));
export const
const definitions: {
agent: string;
model: string;
tools: string;
}
definitions
= {
agent: string
agent
: "v1",
model: string
model
: "gpt-6-luna",
tools: string
tools
: "v1" };

Use a persistent database path with one live host per SQLite file. Give each replacement host incarnation a distinct producerId. The automatic host holds SQLite’s exclusive connection lock for its entire Scope. Another host fails startup, and independent readers cannot access the database while that connection is alive. Use a local filesystem with working SQLite locks; do not replace or unlink a live database file. New files are initialized in WAL mode; existing files must already use WAL mode. The host option synchronous: "NORMAL" opts its connection into WAL NORMAL; the default is FULL. NORMAL survives process crashes, but power loss or an OS crash can lose acknowledged commits and cause external effects to repeat during recovery. Managed storage rejects custom SQLite triggers because the host owns all journal and ownership mutations. workerConcurrency limits concurrently processed threads and defaults to one. The managed host dispatches wake hints through one bounded queue, coalescing repeated hints for pending or active threads. A shared periodic ledger scan recovers missed hints. The scan stops when the last subscriber leaves and restarts when another subscribes. NodeDurableHost.layer checks storage, recovers pending work, and starts the worker pool when the Layer is acquired. Save this as node-host.ts, replacing producerId for each process start:

node-host.ts
import {
import NodeDurableHost
NodeDurableHost
} from "@yielded/agent-platform-node";
import {
import Layer
Layer
} from "effect";
import {
const definitions: {
agent: string;
model: string;
tools: string;
}
definitions
,
const ModelLive: Model<"openai", LanguageModel, OpenAiClient>
ModelLive
,
const OpenAiLive: Layer.Layer<OpenAiClient, ConfigError, never>
OpenAiLive
,
const planner: Definition<String, Struct<{
readonly itinerary: $Array<String>;
}>, "Plan a trip with one itinerary entry per day.", Toolkit<{}>, undefined, undefined, undefined> & {
readonly id: Brand<"@effect-agent/core/AgentId"> & "trip-planner";
}
planner
} from "./node-agent.ts";
export const
const HostLive: Layer.Layer<DurableAgentRuntime | MessageDeliveryStore | ThreadReader | NodeDurableAgentRuntimeConfig | (Crypto & DurableAgentRuntime) | (Crypto & MessageDeliveryStore) | (Crypto & ThreadReader) | (Crypto & NodeDurableAgentRuntimeConfig) | (SqlClient & DurableAgentRuntime) | (SqlClient & MessageDeliveryStore) | ... 6 more ... | NodeDurableHost.NodeDurableHost, ConfigError | ... 3 more ... | NodePlatformConfigError, never>
HostLive
=
import NodeDurableHost
NodeDurableHost
.
const layer: <readonly [{
readonly agent: Definition<String, Struct<{
readonly itinerary: $Array<String>;
}>, "Plan a trip with one itinerary entry per day.", Toolkit<{}>, undefined, undefined, undefined> & {
readonly id: Brand<"@effect-agent/core/AgentId"> & "trip-planner";
};
readonly model: Model<"openai", LanguageModel, OpenAiClient>;
readonly definitions: {
agent: string;
model: string;
tools: string;
};
}], never, never, never, never, never, never>(registrations: readonly [{
readonly agent: Definition<String, Struct<{
readonly itinerary: $Array<String>;
}>, "Plan a trip with one itinerary entry per day.", Toolkit<{}>, undefined, undefined, undefined> & {
readonly id: Brand<"@effect-agent/core/AgentId"> & "trip-planner";
};
readonly model: Model<"openai", LanguageModel, OpenAiClient>;
readonly definitions: {
agent: string;
model: string;
tools: string;
};
}], options: NodeDurableAgentRuntimeOptions<...>) => Layer.Layer<...>

Acquire a complete Node host and start one bounded, scoped worker pool after recovery. Own the SQLite file exclusively until the host and its storage close. A second connection fails construction; after process death the replacement retires abandoned claims before recovery, without waiting for their leases. Use this host's services for live inspection. Provide model, tool, instruction, and schema dependencies to this Layer. Reusing the Layer shares the same pool. A worker failure closes admission; observe it with run at the process boundary so the application exits and releases the host instead of remaining idle.

layer
([{
agent: Definition<String, Struct<{
readonly itinerary: $Array<String>;
}>, "Plan a trip with one itinerary entry per day.", Toolkit<{}>, undefined, undefined, undefined> & {
readonly id: Brand<"@effect-agent/core/AgentId"> & "trip-planner";
}
agent
:
const planner: Definition<String, Struct<{
readonly itinerary: $Array<String>;
}>, "Plan a trip with one itinerary entry per day.", Toolkit<{}>, undefined, undefined, undefined> & {
readonly id: Brand<"@effect-agent/core/AgentId"> & "trip-planner";
}
planner
,
model: Model<"openai", LanguageModel, OpenAiClient>
model
:
const ModelLive: Model<"openai", LanguageModel, OpenAiClient>
ModelLive
,
definitions: {
agent: string;
model: string;
tools: string;
}
definitions
}], {
NodeDurableAgentRuntimeOptions<ContextError = never, ContextRequirements = never, AuthorizationError = never, AuthorizationRequirements = never, ReconcilerError = never, ReconcilerRequirements = never>.filename: string
filename
: "./agents.sqlite",
NodeDurableAgentRuntimeOptions<ContextError = never, ContextRequirements = never, AuthorizationError = never, AuthorizationRequirements = never, ReconcilerError = never, ReconcilerRequirements = never>.deploymentId: string
deploymentId
: "travel-planner",
NodeDurableAgentRuntimeOptions<ContextError = never, ContextRequirements = never, AuthorizationError = never, AuthorizationRequirements = never, ReconcilerError = never, ReconcilerRequirements = never>.producerId: string
producerId
: "worker-start-001",
NodeDurableAgentRuntimeOptions<ContextError = never, ContextRequirements = never, AuthorizationError = never, AuthorizationRequirements = never, ReconcilerError = never, ReconcilerRequirements = never>.workerConcurrency?: number | undefined

Default 1; bounded to 1..64.

workerConcurrency
: 4,
}).
Pipeable.pipe<Layer.Layer<DurableAgentRuntime | MessageDeliveryStore | ThreadReader | NodeDurableAgentRuntimeConfig | (Crypto & DurableAgentRuntime) | (Crypto & MessageDeliveryStore) | (Crypto & ThreadReader) | (Crypto & NodeDurableAgentRuntimeConfig) | (SqlClient & DurableAgentRuntime) | (SqlClient & MessageDeliveryStore) | ... 6 more ... | NodeDurableHost.NodeDurableHost, DurableWorkerFailure | ... 2 more ... | NodePlatformConfigError, OpenAiClient>, Layer.Layer<...>>(this: Layer.Layer<...>, ab: (_: Layer.Layer<...>) => Layer.Layer<...>): Layer.Layer<...> (+21 overloads)
pipe
(
import Layer
Layer
.
const provide: <never, ConfigError, OpenAiClient>(that: Layer.Layer<OpenAiClient, ConfigError, never>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, ConfigError | E2, Exclude<RIn2, OpenAiClient>> (+3 overloads)

Feeds the output services of the dependency layer into the requirements of this layer, returning a layer that only provides the services from this layer.

When to use

Use when you need to hide an implementation dependency layer from callers.

Details

In serviceLayer.pipe(Layer.provide(dependencyLayer)), the dependency layer is built first and is used to satisfy the requirements of serviceLayer.

Example (Providing layer dependencies)

import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
class UserService extends Context.Service<UserService, {
readonly getUser: (id: string) => Effect.Effect<{
id: string
name: string
}>
}>()("UserService") {}
class Logger extends Context.Service<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>()("Logger") {}
// Create dependency layers
const databaseLayer = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`))
})
const logs: Array<string> = []
const loggerLayer = Layer.succeed(Logger, {
log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => logs.push(`[LOG] ${msg}`)))
})
// UserService depends on Database and Logger
const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() {
const database = yield* Database
const logger = yield* Logger
return {
getUser: Effect.fn("UserService.getUser")(function*(id: string) {
yield* logger.log(`Looking up user ${id}`)
const result = yield* database.query(
`SELECT * FROM users WHERE id = ${id}`
)
return { id, name: result }
})
}
}))
// Provide dependencies to UserService layer
const userServiceWithDependencies = userServiceLayer.pipe(
Layer.provide(Layer.mergeAll(databaseLayer, loggerLayer))
)
// Now UserService layer has no dependencies
const program = Effect.gen(function*() {
const userService = yield* UserService
return yield* userService.getUser("123")
}).pipe(
Effect.provide(userServiceWithDependencies)
)
Effect.runSync(program) // => { id: "123", name: "DB: SELECT * FROM users WHERE id = 123" }
logs // => ["[LOG] Looking up user 123"]

@see ― provideMerge for retaining the dependency services

@category ― providing services

@since ― 2.0.0

provide
(
const OpenAiLive: Layer.Layer<OpenAiClient, ConfigError, never>
OpenAiLive
));

TypeScript infers the registration’s model, tool, instruction, and schema service requirements. Provide those services to the Layer, as OpenAiLive does here. Node supplies Crypto.

Save this as node-main.ts and run it with Node’s TypeScript transform support:

node-main.ts
import {
import NodeRuntime
NodeRuntime
} from "@effect/platform-node";
import {
import NodeDurableHost
NodeDurableHost
} from "@yielded/agent-platform-node";
import {
import Effect
Effect
} from "effect";
import {
const HostLive: Layer<DurableAgentRuntime | MessageDeliveryStore | ThreadReader | NodeDurableAgentRuntimeConfig | (Crypto & DurableAgentRuntime) | (Crypto & MessageDeliveryStore) | (Crypto & ThreadReader) | (Crypto & NodeDurableAgentRuntimeConfig) | (SqlClient & DurableAgentRuntime) | (SqlClient & MessageDeliveryStore) | ... 6 more ... | NodeDurableHost.NodeDurableHost, DurableWorkerFailure | ... 3 more ... | ConfigError, never>
HostLive
} from "./node-host.ts";
import NodeRuntime
NodeRuntime
.
const runMain: <BindingUnavailable | DurableWorkerFailure | MessageDeliveryError | SqliteStorageInitializationError | NodePlatformConfigError | ConfigError, void>(effect: Effect.Effect<void, BindingUnavailable | DurableWorkerFailure | MessageDeliveryError | SqliteStorageInitializationError | NodePlatformConfigError | ConfigError, never>, options?: {
readonly disableErrorReporting?: boolean | undefined;
readonly teardown?: Teardown | undefined;
}) => void (+1 overload)

Helps you run a main effect with built-in error handling, logging, and signal management.

When to use

Use to run a Node.js application's main Effect with structured error handling, log management, interrupt support, or advanced teardown capabilities.

Details

This function launches an Effect as the main entry point, setting exit codes based on success or failure, handling interrupts (e.g., Ctrl+C), and optionally logging errors. By default, it logs errors and uses a "pretty" format, but both behaviors can be turned off. You can also provide custom teardown logic to finalize resources or produce different exit codes.

The optional configuration object can include:

  • disableErrorReporting: Turn off automatic error logging.
  • teardown: Provide custom finalization logic.

@category ― running

@since ― 4.0.0

runMain
(
import NodeDurableHost
NodeDurableHost
.
const run: Effect.Effect<void, BindingUnavailable | DurableWorkerFailure, NodeDurableHost.NodeDurableHost>

Supervise the host's existing workers without starting another pool. Use with Effect.provide(HostLive) and NodeRuntime.runMain; race it with a server Effect when the same process also serves requests. Unlike Layer.launch, this observes worker failures.

run
.
Pipeable.pipe<Effect.Effect<void, BindingUnavailable | DurableWorkerFailure, NodeDurableHost.NodeDurableHost>, Effect.Effect<void, BindingUnavailable | DurableWorkerFailure | MessageDeliveryError | SqliteStorageInitializationError | NodePlatformConfigError | ConfigError, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, BindingUnavailable | DurableWorkerFailure, NodeDurableHost.NodeDurableHost>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const provide: <DurableAgentRuntime | MessageDeliveryStore | ThreadReader | NodeDurableAgentRuntimeConfig | (Crypto & DurableAgentRuntime) | (Crypto & MessageDeliveryStore) | (Crypto & ThreadReader) | (Crypto & NodeDurableAgentRuntimeConfig) | (SqlClient & DurableAgentRuntime) | (SqlClient & MessageDeliveryStore) | ... 6 more ... | NodeDurableHost.NodeDurableHost, DurableWorkerFailure | ... 3 more ... | ConfigError, never>(layer: Layer<...>, options?: {
readonly local?: boolean | undefined;
} | undefined) => <A, E, R>(self: Effect.Effect<...>) => Effect.Effect<...> (+5 overloads)

Provides dependencies to an effect using layers or a context. Use options.local to build the layer every time; by default, layers are shared between provide calls.

Example (Providing dependencies with a layer)

import { Context, Effect, Layer } from "effect"
interface Database {
readonly query: (sql: string) => Effect.Effect<string>
}
const Database = Context.Service<Database>("Database")
const DatabaseLayer = Layer.succeed(Database)({
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result for: ${sql}`))
})
const program = Effect.gen(function*() {
const db = yield* Database
return yield* db.query("SELECT * FROM users")
})
const provided = Effect.provide(program, DatabaseLayer)
await Effect.runPromise(provided) // => "Result for: SELECT * FROM users"

@category ― providing services

@since ― 2.0.0

provide
(
const HostLive: Layer<DurableAgentRuntime | MessageDeliveryStore | ThreadReader | NodeDurableAgentRuntimeConfig | (Crypto & DurableAgentRuntime) | (Crypto & MessageDeliveryStore) | (Crypto & ThreadReader) | (Crypto & NodeDurableAgentRuntimeConfig) | (SqlClient & DurableAgentRuntime) | (SqlClient & MessageDeliveryStore) | ... 6 more ... | NodeDurableHost.NodeDurableHost, DurableWorkerFailure | ... 3 more ... | ConfigError, never>
HostLive
)));
node --experimental-transform-types node-main.ts

NodeDurableHost.run observes the existing pool; calling it again does not start more workers. If a worker fails, admission closes and run fails with the original typed error or defect. NodeRuntime.runMain then closes the host. Use run rather than Layer.launch(HostLive), which does not observe background worker failures. Interrupting only an observer leaves the pool running; closing the host’s Scope closes admission, stops and joins workers, releases ownership, and closes storage and application services.

If the process also serves requests, race the server Effect with NodeDurableHost.run using Effect.raceFirst, and provide the shared HostLive to that combined Effect. A worker failure then stops the server too. Creating two separate host Layers for the same SQLite file is unsupported.

To drive the durable runtime through an injected WorkflowEngine, follow the Effect Workflows guide. Its Node.js setup uses SQLite and a single-process Cluster runner.

Use NodeDurableAgentRuntime.layerRegistered when you own execution, as in the Workflow assembly. It captures registrations and acquires storage without starting workers. layerWithBindings accepts precompiled ResolvedBinding values whose application Scope you own; layer constructs an unregistered runtime for explicit admission and execution.

The service class’s existing NodeDurableHost.layerRegistered, layerStack, and layer constructors remain available for manually managed hosts. Import the class from @yielded/agent-platform-node/node-durable-host when using these APIs; their workers start only when you run host.runResolvedWorkers. The module-level NodeDurableHost.layer shown above owns worker startup and is the default for an application.

These manual assemblies retain lease-based recovery and do not acquire the automatic host’s exclusive authority or retire claims on startup. They remain suitable for explicit runtime composition; a producer name alone never permits reclaiming a live lease.

Registrations carry application version declarations. Update them when behavior changes, including tool implementations that JSON cannot represent. Register one current binding per stable agentId; queued and resumed work uses that binding without requiring historical agent or toolbox versions. Accepted inputs and prepared deliveries retain their original identities and payloads. digestDefinitions computes the digests for explicit submissions; DurableWorkerBinding.make(agent, digests) accepts precomputed digests.

An unresolved tool effect remains a parked Unknown Outcome with its settlement obligation intact. Later input in the same Thread can run without replaying that effect. Approval waits and joined input still preserve their ordering barriers, and live ownership prevents another claim. Inspect the parked operation through explainThread, then use authorized resolution or abort when needed.

Pass service layers in the options to NodeDurableHost.layer or NodeDurableAgentRuntime.layer:

Option Service Default
runContext RunContextPreparation No prompt transform or transient reference context
toolAuthorization RunToolAuthorization Allow all tool calls
toolReconciler ToolReconciler Keep unconfirmed tool outcomes unknown

Add these options to the host assembly above. Use { runContext: RunContextLive } for prompt preparation, or { toolAuthorization: SearchOnlyLive } for a tool policy. Use { toolReconciler: SupplierReconcilerLive } for supplier-backed recovery of unconfirmed tool outcomes. Configure these services independently or together.

Select native compaction by providing its Layer directly to the host, for example HostLive.pipe(Layer.provide(ContextCompactor.layerRollover)). Without an injected ContextCompactor, the host uses the default pruning and summarization strategy.

The assembled layer retains each extension’s construction errors and application dependencies in its error and requirement types. The host supplies Crypto.Crypto. Provide the remaining dependencies through ordinary Layer.provide composition before running the application.

Let layer or layerStack infer the types from your options. When annotating reusable options, NodeDurableAgentRuntimeOptions<ContextError, ContextRequirements, AuthorizationError, AuthorizationRequirements, ReconcilerError, ReconcilerRequirements> preserves all three layers’ construction contracts.

The runtime captures services when the host layer is acquired. Keep their resources alive for its Scope. Providing replacements around a later worker call does not change the captured services. toolFailureObserver configures recovered tool failure reporting.

  1. Authenticate the caller and call host.submit(agent, input, options). Supply the thread ID, principal, idempotency key, and definition digests.
  2. Return the receipt after admission.
  3. Await completion with host.awaitSettlement(receipt), or stream records with host.observe(receipt).

Reuse the idempotency key when retrying the same request. Different input under that key fails with an admission conflict.

Closing the host’s Scope stops admission, releases ownership, and closes SQLite. After abrupt process death, including SIGKILL, the operating system releases the automatic host’s connection lock. Its replacement acquires that lock, checks storage compatibility, and atomically fences and retires retained claims before ordinary recovery. It does not wait for the old ownership lease. Startup, history validation, and provider work still take time. Lease, renewal, and wake-scan defaults remain 30 seconds, 10 seconds, and 1 second. Unconfirmed external tool outcomes require reconciliation or authorized resolution before replay. Startup recovery must succeed for every Thread before admission or workers open. A retained-history fault or recovery timeout fails host construction with RecoveryBlocked; accepted work stays pending.

Inspect host.startupRecovery, host.explain, host.verify, and host.scanObligations for recovery status while the host is running. The managed host exposes ThreadReader for canonical reads and MessageDeliveryStore for independent delivery obligations. Its SQLite client, canonical writes, and submission ownership ports are private. Each active Attempt owns a scoped storage session, so administrative writes and execution share one authority. Application SQL clients and instrumentation stay outside that private storage context. Use a manual runtime assembly when composing additional SQL adapters. Stop a managed host before using the standalone admin:durable CLI or another database reader. See operations for approvals, schedules, and backups.