@effect-agent/platform-node 0.1.0-beta.42 → 0.1.0-beta.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/host.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { ThreadId, SubmissionId } from "@effect-agent/core";
2
2
  import {
3
- compileRegistrations,
4
3
  type AgentRegistration,
5
4
  DurableAgentRuntime,
6
5
  type AbortCommand,
@@ -31,15 +30,14 @@ import {
31
30
  type RetryCommand,
32
31
  type Settlement,
33
32
  } from "@effect-agent/thread";
34
- import { NodeCrypto } from "@effect/platform-node";
35
33
  import { type Stream, Context, type Crypto, Effect, Layer, Ref, Schema } from "effect";
36
34
 
37
35
  import {
38
- NodeDurableRuntime,
39
- NodeDurableRuntimeConfig,
40
- type NodeDurableRuntimeInitializationError,
41
- type NodeDurableRuntimeOptions,
42
- type NodeDurableRuntimeServices,
36
+ NodeDurableAgentRuntime,
37
+ NodeDurableAgentRuntimeConfig,
38
+ type NodeDurableAgentRuntimeInitializationError,
39
+ type NodeDurableAgentRuntimeOptions,
40
+ type NodeDurableAgentRuntimeServices,
43
41
  } from "./layers.ts";
44
42
 
45
43
  /**
@@ -50,74 +48,74 @@ export class AdmissionClosed extends Schema.TaggedError<AdmissionClosed>()("Admi
50
48
  message: Schema.String,
51
49
  }) {}
52
50
 
53
- const makeHost = (bindings: ReadonlyArray<ResolvedBinding>) =>
54
- Effect.gen(function* () {
55
- const runtime = yield* DurableAgentRuntime;
56
- const config = yield* NodeDurableRuntimeConfig;
51
+ const makeHost = Effect.gen(function* () {
52
+ const runtime = yield* DurableAgentRuntime;
53
+ const config = yield* NodeDurableAgentRuntimeConfig;
57
54
 
58
- // Startup gate (deployment §5, plan §host): configuration decoding and storage compatibility
59
- // already gated this Layer's dependencies; the last gate before admission opens is recovering
60
- // EVERY nonterminal Submission. Work needing a live Agent Binding is reported `deferred` and
61
- // stays a visible obligation for `runWorkers`; lanes durably blocked on an Unknown Outcome are
62
- // reported `unknown` and wait for the authorized `resolveUnknown` path (DUR-017) — they consume
63
- // no worker permit while the settlement obligation stays owed.
64
- const startupRecovery = yield* runtime.runRecovery;
55
+ // Startup gate (deployment §5, plan §host): configuration decoding and storage compatibility
56
+ // already gated this Layer's dependencies; the last gate before admission opens is recovering
57
+ // EVERY nonterminal Submission. Work needing a live Agent Binding is reported `deferred` and
58
+ // stays a visible obligation for `runWorkers`; lanes durably blocked on an Unknown Outcome are
59
+ // reported `unknown` and wait for the authorized `resolveUnknown` path (DUR-017) — they consume
60
+ // no worker permit while the settlement obligation stays owed.
61
+ const startupRecovery = yield* runtime.runRecovery;
65
62
 
66
- const admission = yield* Ref.make(true);
67
- // Shutdown step 1 (DEPLOY-005): admission closes before the ownership drain in the runtime
68
- // Layer below releases claims and before the stores close in reverse acquisition order.
69
- yield* Effect.addFinalizer(() => Ref.set(admission, false));
63
+ const admission = yield* Ref.make(true);
70
64
 
71
- const requireAdmission: Effect.Effect<void, AdmissionClosed> = Ref.get(admission).pipe(
72
- Effect.flatMap((open) =>
73
- open
74
- ? Effect.void
75
- : Effect.fail(
76
- AdmissionClosed.make({ message: "The host is shutting down; admission is closed." }),
77
- ),
78
- ),
79
- );
65
+ // Shutdown step 1 (DEPLOY-005): admission closes before the ownership drain in the runtime
66
+ // Layer below releases claims and before the stores close in reverse acquisition order.
67
+ yield* Effect.addFinalizer(() => Ref.set(admission, false));
80
68
 
81
- const submit = <InputSchema extends Schema.Top>(
82
- agent: DurableSubmitAgent<InputSchema>,
83
- input: InputSchema["Type"],
84
- options: DurableSubmitOptions,
85
- ): Effect.Effect<
86
- Receipt,
87
- AdmissionClosed | DurableSubmitFailure,
88
- InputSchema["EncodingServices"]
89
- > => requireAdmission.pipe(Effect.andThen(runtime.submit(agent, input, options)));
69
+ const requireAdmission: Effect.Effect<void, AdmissionClosed> = Ref.get(admission).pipe(
70
+ Effect.flatMap((open) =>
71
+ open
72
+ ? Effect.void
73
+ : Effect.fail(
74
+ AdmissionClosed.make({ message: "The host is shutting down; admission is closed." }),
75
+ ),
76
+ ),
77
+ );
78
+
79
+ const submit = <InputSchema extends Schema.Top>(
80
+ agent: DurableSubmitAgent<InputSchema>,
81
+ input: InputSchema["Type"],
82
+ options: DurableSubmitOptions,
83
+ ): Effect.Effect<
84
+ Receipt,
85
+ AdmissionClosed | DurableSubmitFailure,
86
+ InputSchema["EncodingServices"]
87
+ > => requireAdmission.pipe(Effect.andThen(runtime.submit(agent, input, options)));
90
88
 
91
- const runWorkers = <A, E, R>(worker: Effect.Effect<A, E, R>): Effect.Effect<void, E, R> =>
92
- Effect.forEach(
93
- Array.from({ length: config.workerConcurrency }, (_, index) => index),
94
- () => worker,
95
- { concurrency: "unbounded", discard: true },
96
- );
89
+ const runWorkers = <A, E, R>(worker: Effect.Effect<A, E, R>): Effect.Effect<void, E, R> =>
90
+ Effect.forEach(
91
+ Array.from({ length: config.workerConcurrency }, (_, index) => index),
92
+ () => worker,
93
+ { concurrency: "unbounded", discard: true },
94
+ );
97
95
 
98
- // S2 multi-binding pool: every claimed head resolves its exact registered Binding through
99
- // the host's exact registrations, so one bounded
100
- // pool serves parent and child lanes — the spec §12 smallest-pool suspension/wakeup proof
101
- // runs `workerConcurrency: 1` over exactly this loop.
102
- const runResolvedWorkers = runWorkers(runtime.runResolvedWorker(bindings));
96
+ // S2 multi-binding pool: every claimed head resolves its exact registered Binding through
97
+ // the host's exact registrations, so one bounded
98
+ // pool serves parent and child lanes — the spec §12 smallest-pool suspension/wakeup proof
99
+ // runs `workerConcurrency: 1` over exactly this loop.
100
+ const runResolvedWorkers = runWorkers(runtime.runResolvedWorker);
103
101
 
104
- return NodeDurableHost.of({
105
- startupRecovery,
106
- admissionOpen: Ref.get(admission),
107
- submit,
108
- awaitSettlement: runtime.awaitSettlement,
109
- observe: runtime.observe,
110
- abort: runtime.abort,
111
- explain: runtime.explain,
112
- explainThread: runtime.explainThread,
113
- verify: runtime.verify,
114
- retry: runtime.retry,
115
- wake: runtime.wake,
116
- scanObligations: runtime.scanObligations,
117
- runWorkers,
118
- runResolvedWorkers,
119
- });
102
+ return NodeDurableHost.of({
103
+ startupRecovery,
104
+ admissionOpen: Ref.get(admission),
105
+ submit,
106
+ awaitSettlement: runtime.awaitSettlement,
107
+ observe: runtime.observe,
108
+ abort: runtime.abort,
109
+ explain: runtime.explain,
110
+ explainThread: runtime.explainThread,
111
+ verify: runtime.verify,
112
+ retry: runtime.retry,
113
+ wake: runtime.wake,
114
+ scanObligations: runtime.scanObligations,
115
+ runWorkers,
116
+ runResolvedWorkers,
120
117
  });
118
+ });
121
119
 
122
120
  /**
123
121
  * Operational host lifecycle for the DN runtime (deployment §2/§5/§6).
@@ -209,56 +207,53 @@ export class NodeDurableHost extends Context.Service<
209
207
  AuthorizationRequirements = never,
210
208
  >(
211
209
  registrations: Entries,
212
- options: NodeDurableRuntimeOptions<
210
+ options: NodeDurableAgentRuntimeOptions<
213
211
  ContextError,
214
212
  ContextRequirements,
215
213
  AuthorizationError,
216
214
  AuthorizationRequirements
217
215
  >,
218
216
  ) {
219
- return Layer.unwrap(
220
- Effect.map(compileRegistrations(registrations), (bindings) =>
221
- NodeDurableHost.layerStack({ ...options, bindings }),
222
- ),
223
- ).pipe(Layer.provide(NodeCrypto.layer));
217
+ return NodeDurableHost.layer.pipe(
218
+ Layer.provideMerge(NodeDurableAgentRuntime.layerRegistered(registrations, options)),
219
+ );
224
220
  }
225
221
 
226
222
  /**
227
- * Host gates over an assembled `NodeDurableRuntime` stack. Bindings must carry the exact
228
- * digests stored by submitters. Omission registers no Agents, so resolved work fails closed.
223
+ * Host gates over an assembled `NodeDurableAgentRuntime` stack. The runtime Layer owns
224
+ * executable registrations; omission registers no Agents, so resolved work fails closed.
229
225
  */
230
- static readonly layer = (
231
- bindings: ReadonlyArray<ResolvedBinding> = [],
232
- ): Layer.Layer<
226
+ static readonly layer: Layer.Layer<
233
227
  NodeDurableHost,
234
228
  DurableWorkerFailure,
235
- DurableAgentRuntime | NodeDurableRuntimeConfig
236
- > => Layer.effect(NodeDurableHost)(makeHost(bindings));
229
+ DurableAgentRuntime | NodeDurableAgentRuntimeConfig
230
+ > = Layer.effect(NodeDurableHost)(makeHost);
237
231
 
238
- /** The complete DN host: `NodeDurableRuntime.layer(options)` plus the host lifecycle gates. */
232
+ /** The complete DN host: `NodeDurableAgentRuntime.layer(options)` plus the host lifecycle gates. */
239
233
  static layerStack<
240
234
  ContextError = never,
241
235
  ContextRequirements = never,
242
236
  AuthorizationError = never,
243
237
  AuthorizationRequirements = never,
244
238
  >(
245
- options: NodeDurableRuntimeOptions<
239
+ options: NodeDurableAgentRuntimeOptions<
246
240
  ContextError,
247
241
  ContextRequirements,
248
242
  AuthorizationError,
249
243
  AuthorizationRequirements
250
244
  > & { readonly bindings?: ReadonlyArray<ResolvedBinding> },
251
245
  ): Layer.Layer<
252
- NodeDurableHost | NodeDurableRuntimeServices,
246
+ NodeDurableHost | NodeDurableAgentRuntimeServices,
253
247
  | DurableWorkerFailure
254
- | NodeDurableRuntimeInitializationError
248
+ | NodeDurableAgentRuntimeInitializationError
255
249
  | ContextError
256
250
  | AuthorizationError,
257
251
  Exclude<ContextRequirements | AuthorizationRequirements, Crypto.Crypto>
258
252
  > {
259
253
  const { bindings = [], ...runtimeOptions } = options;
260
- return NodeDurableHost.layer(bindings).pipe(
261
- Layer.provideMerge(NodeDurableRuntime.layer(runtimeOptions)),
254
+
255
+ return NodeDurableHost.layer.pipe(
256
+ Layer.provideMerge(NodeDurableAgentRuntime.layerWithBindings(bindings, runtimeOptions)),
262
257
  );
263
258
  }
264
259
  }
package/src/layers.ts CHANGED
@@ -19,6 +19,8 @@ import {
19
19
  type SqliteStorageInitializationError,
20
20
  } from "@effect-agent/storage-sqlite";
21
21
  import {
22
+ type AgentRegistration,
23
+ type ResolvedBinding,
22
24
  type ScheduleStore,
23
25
  type ThreadStore,
24
26
  type WakeScheduler,
@@ -42,6 +44,7 @@ import { NodeWakeSchedulerConfig, nodeWakeSchedulerLayer } from "./wake-schedule
42
44
 
43
45
  const PositiveMillis = Schema.Int.check(Schema.isGreaterThan(0));
44
46
  const NonNegativeMillis = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
47
+
45
48
  const WorkerConcurrency = Schema.Int.check(
46
49
  Schema.isGreaterThanOrEqualTo(1),
47
50
  Schema.isLessThanOrEqualTo(64),
@@ -61,8 +64,8 @@ export class NodePlatformConfigError extends Schema.TaggedError<NodePlatformConf
61
64
  * construction, exposed as a typed service). Every cadence is in milliseconds and every bound is
62
65
  * finite; `workerConcurrency` caps how many worker loops `NodeDurableHost.runWorkers` drives.
63
66
  */
64
- export class NodeDurableRuntimeConfigValue extends Schema.Class<NodeDurableRuntimeConfigValue>(
65
- "@effect-agent/platform-node/NodeDurableRuntimeConfigValue",
67
+ export class NodeDurableAgentRuntimeConfigValue extends Schema.Class<NodeDurableAgentRuntimeConfigValue>(
68
+ "@effect-agent/platform-node/NodeDurableAgentRuntimeConfigValue",
66
69
  )({
67
70
  /** SQLite database file backing BOTH the Thread Log and the Submission Ledger. */
68
71
  filename: Schema.NonEmptyString,
@@ -89,17 +92,17 @@ export class NodeDurableRuntimeConfigValue extends Schema.Class<NodeDurableRunti
89
92
  }) {}
90
93
 
91
94
  /** Explicit configuration authority for the assembled Node durable runtime. */
92
- export class NodeDurableRuntimeConfig extends Context.Service<
93
- NodeDurableRuntimeConfig,
94
- NodeDurableRuntimeConfigValue
95
- >()("@effect-agent/platform-node/NodeDurableRuntimeConfig") {}
95
+ export class NodeDurableAgentRuntimeConfig extends Context.Service<
96
+ NodeDurableAgentRuntimeConfig,
97
+ NodeDurableAgentRuntimeConfigValue
98
+ >()("@effect-agent/platform-node/NodeDurableAgentRuntimeConfig") {}
96
99
 
97
100
  /**
98
- * Raw (unvalidated) construction options for `NodeDurableRuntime.layer`. Optional fields default
101
+ * Raw (unvalidated) construction options for `NodeDurableAgentRuntime.layer`. Optional fields default
99
102
  * to the documented production values; everything is schema-decoded into
100
- * `NodeDurableRuntimeConfigValue` before any resource opens (deployment §5 gate 1).
103
+ * `NodeDurableAgentRuntimeConfigValue` before any resource opens (deployment §5 gate 1).
101
104
  */
102
- export interface NodeDurableRuntimeOptions<
105
+ export interface NodeDurableAgentRuntimeOptions<
103
106
  ContextError = never,
104
107
  ContextRequirements = never,
105
108
  AuthorizationError = never,
@@ -160,25 +163,25 @@ export interface NodeDurableRuntimeOptions<
160
163
  }
161
164
 
162
165
  /** Built-in construction failures. `layer` also preserves supplied service Layers' errors. */
163
- export type NodeDurableRuntimeInitializationError =
166
+ export type NodeDurableAgentRuntimeInitializationError =
164
167
  | NodePlatformConfigError
165
168
  | SqliteStorageInitializationError;
166
169
 
167
- /** The services `NodeDurableRuntime.layer` provides. */
168
- export type NodeDurableRuntimeServices =
170
+ /** The services `NodeDurableAgentRuntime.layer` provides. */
171
+ export type NodeDurableAgentRuntimeServices =
169
172
  | DurableAgentRuntime
170
173
  | SubmissionLedger
171
174
  | ThreadStore
172
175
  | ScheduleStore
173
176
  | WakeScheduler
174
177
  | DurableRuntimeConfig
175
- | NodeDurableRuntimeConfig;
178
+ | NodeDurableAgentRuntimeConfig;
176
179
 
177
- const decodeConfigValue = Schema.decodeUnknownEffect(NodeDurableRuntimeConfigValue);
180
+ const decodeConfigValue = Schema.decodeUnknownEffect(NodeDurableAgentRuntimeConfigValue);
178
181
 
179
182
  const configFromOptions = (
180
- options: Omit<NodeDurableRuntimeOptions, "runContext" | "toolAuthorization">,
181
- ): Effect.Effect<NodeDurableRuntimeConfigValue, NodePlatformConfigError> =>
183
+ options: Omit<NodeDurableAgentRuntimeOptions, "runContext" | "toolAuthorization">,
184
+ ): Effect.Effect<NodeDurableAgentRuntimeConfigValue, NodePlatformConfigError> =>
182
185
  decodeConfigValue({
183
186
  filename: options.filename,
184
187
  deploymentId: options.deploymentId,
@@ -203,26 +206,31 @@ const configFromOptions = (
203
206
  );
204
207
 
205
208
  /** SQLite storage configuration derived from the single validated Node configuration. */
206
- const sqliteStorageConfigLayer: Layer.Layer<SqliteStorageConfig, never, NodeDurableRuntimeConfig> =
207
- Layer.effect(SqliteStorageConfig)(
208
- Effect.gen(function* () {
209
- const config = yield* NodeDurableRuntimeConfig;
210
- return SqliteStorageConfigValue.make({
211
- observationPollInterval: config.observationPollInterval,
212
- busyTimeout: config.busyTimeout,
213
- ownershipLeaseDuration: config.ownershipLeaseDuration,
214
- verifyOnOpen: config.verifyOnOpen,
215
- });
216
- }),
217
- );
209
+ const sqliteStorageConfigLayer: Layer.Layer<
210
+ SqliteStorageConfig,
211
+ never,
212
+ NodeDurableAgentRuntimeConfig
213
+ > = Layer.effect(SqliteStorageConfig)(
214
+ Effect.gen(function* () {
215
+ const config = yield* NodeDurableAgentRuntimeConfig;
216
+
217
+ return SqliteStorageConfigValue.make({
218
+ observationPollInterval: config.observationPollInterval,
219
+ busyTimeout: config.busyTimeout,
220
+ ownershipLeaseDuration: config.ownershipLeaseDuration,
221
+ verifyOnOpen: config.verifyOnOpen,
222
+ });
223
+ }),
224
+ );
218
225
 
219
226
  /** Thread coordinator configuration derived from the single validated Node configuration. */
220
227
  const durableRuntimeConfigLayer = (
221
228
  estimateCostMicrousd: RunCostEstimator | undefined,
222
- ): Layer.Layer<DurableRuntimeConfig, never, NodeDurableRuntimeConfig> =>
229
+ ): Layer.Layer<DurableRuntimeConfig, never, NodeDurableAgentRuntimeConfig> =>
223
230
  Layer.effect(DurableRuntimeConfig)(
224
231
  Effect.gen(function* () {
225
- const config = yield* NodeDurableRuntimeConfig;
232
+ const config = yield* NodeDurableAgentRuntimeConfig;
233
+
226
234
  return DurableRuntimeConfig.make({
227
235
  deploymentId: config.deploymentId,
228
236
  producerId: config.producerId,
@@ -238,10 +246,11 @@ const durableRuntimeConfigLayer = (
238
246
  const wakeSchedulerConfigLayer: Layer.Layer<
239
247
  NodeWakeSchedulerConfig,
240
248
  never,
241
- NodeDurableRuntimeConfig
249
+ NodeDurableAgentRuntimeConfig
242
250
  > = Layer.effect(NodeWakeSchedulerConfig)(
243
251
  Effect.gen(function* () {
244
- const config = yield* NodeDurableRuntimeConfig;
252
+ const config = yield* NodeDurableAgentRuntimeConfig;
253
+
245
254
  return { scanInterval: Duration.millis(config.wakeScanInterval) };
246
255
  }),
247
256
  );
@@ -252,6 +261,7 @@ const releaseTrackedOwnership = (
252
261
  ): Effect.Effect<void> =>
253
262
  Effect.gen(function* () {
254
263
  const tracked = yield* Ref.getAndSet(registry, new Map<SubmissionId, OwnershipToken>());
264
+
255
265
  for (const [submissionId, ownershipToken] of tracked) {
256
266
  yield* ledger
257
267
  .releaseOwnership(ReleaseOwnershipRequest.make({ submissionId, ownershipToken }))
@@ -280,16 +290,20 @@ export const ownershipDrainLayer: Layer.Layer<SubmissionLedger, never, Submissio
280
290
  Layer.effect(SubmissionLedger)(
281
291
  Effect.gen(function* () {
282
292
  const ledger = yield* SubmissionLedger;
293
+
283
294
  const registry = yield* Ref.make<ReadonlyMap<SubmissionId, OwnershipToken>>(
284
295
  new Map<SubmissionId, OwnershipToken>(),
285
296
  );
286
297
 
287
298
  const track = (submissionId: SubmissionId, ownershipToken: OwnershipToken) =>
288
299
  Ref.update(registry, (tracked) => new Map(tracked).set(submissionId, ownershipToken));
300
+
289
301
  const untrack = (submissionId: SubmissionId) =>
290
302
  Ref.update(registry, (tracked) => {
291
303
  const next = new Map(tracked);
304
+
292
305
  next.delete(submissionId);
306
+
293
307
  return next;
294
308
  });
295
309
 
@@ -364,7 +378,7 @@ export const ownershipDrainLayer: Layer.Layer<SubmissionLedger, never, Submissio
364
378
  * verified during construction: an incompatible database file fails the Layer with
365
379
  * `SqliteStorageCompatibilityError` before anything is mutated (DEPLOY-008).
366
380
  */
367
- export class NodeDurableRuntime {
381
+ export class NodeDurableAgentRuntime {
368
382
  /** Validated configuration Layer; fails typed when the supplied options are out of bounds. */
369
383
  static configLayer<
370
384
  ContextError = never,
@@ -372,14 +386,14 @@ export class NodeDurableRuntime {
372
386
  AuthorizationError = never,
373
387
  AuthorizationRequirements = never,
374
388
  >(
375
- options: NodeDurableRuntimeOptions<
389
+ options: NodeDurableAgentRuntimeOptions<
376
390
  ContextError,
377
391
  ContextRequirements,
378
392
  AuthorizationError,
379
393
  AuthorizationRequirements
380
394
  >,
381
- ): Layer.Layer<NodeDurableRuntimeConfig, NodePlatformConfigError> {
382
- return Layer.effect(NodeDurableRuntimeConfig)(configFromOptions(options));
395
+ ): Layer.Layer<NodeDurableAgentRuntimeConfig, NodePlatformConfigError> {
396
+ return Layer.effect(NodeDurableAgentRuntimeConfig)(configFromOptions(options));
383
397
  }
384
398
 
385
399
  /** The full DN runtime stack over one SQLite file. */
@@ -389,20 +403,83 @@ export class NodeDurableRuntime {
389
403
  AuthorizationError = never,
390
404
  AuthorizationRequirements = never,
391
405
  >(
392
- options: NodeDurableRuntimeOptions<
406
+ options: NodeDurableAgentRuntimeOptions<
393
407
  ContextError,
394
408
  ContextRequirements,
395
409
  AuthorizationError,
396
410
  AuthorizationRequirements
397
411
  >,
398
412
  ): Layer.Layer<
399
- NodeDurableRuntimeServices,
400
- NodeDurableRuntimeInitializationError | ContextError | AuthorizationError,
413
+ NodeDurableAgentRuntimeServices,
414
+ NodeDurableAgentRuntimeInitializationError | ContextError | AuthorizationError,
401
415
  Exclude<ContextRequirements | AuthorizationRequirements, Crypto.Crypto>
402
416
  > {
403
- return Layer.unwrap(
417
+ return NodeDurableAgentRuntime.assemble(DurableAgentRuntime.layerWithServices, options);
418
+ }
419
+
420
+ /** Own typed executable registrations for every worker using this Node runtime. */
421
+ static layerRegistered<
422
+ const Entries extends ReadonlyArray<AgentRegistration>,
423
+ ContextError = never,
424
+ ContextRequirements = never,
425
+ AuthorizationError = never,
426
+ AuthorizationRequirements = never,
427
+ >(
428
+ registrations: Entries,
429
+ options: NodeDurableAgentRuntimeOptions<
430
+ ContextError,
431
+ ContextRequirements,
432
+ AuthorizationError,
433
+ AuthorizationRequirements
434
+ >,
435
+ ) {
436
+ return NodeDurableAgentRuntime.assemble(
437
+ DurableAgentRuntime.layerRegistered(registrations),
438
+ options,
439
+ );
440
+ }
441
+
442
+ /** Construct from registrations already resolved within the enclosing application Scope. */
443
+ static layerWithBindings<
444
+ ContextError = never,
445
+ ContextRequirements = never,
446
+ AuthorizationError = never,
447
+ AuthorizationRequirements = never,
448
+ >(
449
+ bindings: ReadonlyArray<ResolvedBinding>,
450
+ options: NodeDurableAgentRuntimeOptions<
451
+ ContextError,
452
+ ContextRequirements,
453
+ AuthorizationError,
454
+ AuthorizationRequirements
455
+ >,
456
+ ) {
457
+ return NodeDurableAgentRuntime.assemble(
458
+ DurableAgentRuntime.layerWithBindings(bindings),
459
+ options,
460
+ );
461
+ }
462
+
463
+ private static assemble<
464
+ RuntimeError,
465
+ RuntimeRequirements,
466
+ ContextError,
467
+ ContextRequirements,
468
+ AuthorizationError,
469
+ AuthorizationRequirements,
470
+ >(
471
+ runtimeLayer: Layer.Layer<DurableAgentRuntime, RuntimeError, RuntimeRequirements>,
472
+ options: NodeDurableAgentRuntimeOptions<
473
+ ContextError,
474
+ ContextRequirements,
475
+ AuthorizationError,
476
+ AuthorizationRequirements
477
+ >,
478
+ ) {
479
+ const assembled = Layer.unwrap(
404
480
  Effect.map(configFromOptions(options), (config) => {
405
- const nodeConfigLayer = Layer.succeed(NodeDurableRuntimeConfig)(config);
481
+ const nodeConfigLayer = Layer.succeed(NodeDurableAgentRuntimeConfig)(config);
482
+
406
483
  const infrastructure = Layer.mergeAll(
407
484
  sqliteStorageConfigLayer,
408
485
  storageFailpointLayer({
@@ -412,15 +489,19 @@ export class NodeDurableRuntime {
412
489
  SqliteClient.layer({ filename: config.filename }),
413
490
  NodeCrypto.layer,
414
491
  );
492
+
415
493
  const runtimeFailpointLayer =
416
494
  options.runtimeFailpoint === undefined
417
495
  ? DurableRuntimeFailpoint.layer
418
496
  : Layer.succeed(DurableRuntimeFailpoint)({ hit: options.runtimeFailpoint });
497
+
419
498
  const reconcilerLayer = options.toolReconciler ?? ToolReconciler.uncertain;
499
+
420
500
  const observerLayer =
421
501
  options.toolFailureObserver === undefined
422
502
  ? Layer.succeed(CurrentToolFailureObserver)(undefined)
423
503
  : toolFailureObserverLayer(options.toolFailureObserver);
504
+
424
505
  const ports = Layer.mergeAll(
425
506
  threadStoreLayer,
426
507
  scheduleStoreLayer,
@@ -428,7 +509,8 @@ export class NodeDurableRuntime {
428
509
  Layer.provideMerge(ownershipDrainLayer.pipe(Layer.provide(submissionLedgerLayer))),
429
510
  ),
430
511
  );
431
- return DurableAgentRuntime.layerWithServices.pipe(
512
+
513
+ return runtimeLayer.pipe(
432
514
  Layer.provideMerge(
433
515
  Layer.mergeAll(ports, durableRuntimeConfigLayer(options.estimateCostMicrousd)),
434
516
  ),
@@ -451,5 +533,13 @@ export class NodeDurableRuntime {
451
533
  );
452
534
  }),
453
535
  );
536
+
537
+ const runtime: Layer.Layer<
538
+ NodeDurableAgentRuntimeServices,
539
+ Layer.Error<typeof assembled>,
540
+ Layer.Services<typeof assembled>
541
+ > = assembled;
542
+
543
+ return runtime;
454
544
  }
455
545
  }
package/src/scheduling.ts CHANGED
@@ -22,7 +22,9 @@ export const nodeScheduleWakeLayer: Layer.Layer<ScheduleWake> = Layer.effect(
22
22
  Effect.gen(function* () {
23
23
  const hints = yield* PubSub.sliding<void>(1);
24
24
  const subscription = yield* PubSub.subscribe(hints);
25
+
25
26
  yield* Effect.addFinalizer(() => PubSub.shutdown(hints));
27
+
26
28
  return ScheduleWake.of({
27
29
  notify: PubSub.publish(hints, undefined).pipe(Effect.asVoid),
28
30
  await: PubSub.take(subscription),
@@ -58,20 +60,25 @@ const nodeSchedulingDriverLayer = (
58
60
  Effect.map((pass) => pass.failed === 0),
59
61
  Effect.catchCause(reportPassFailure),
60
62
  );
63
+
61
64
  const deadlineResult = passSucceeded
62
65
  ? yield* store.nextDeadline().pipe(Effect.result)
63
66
  : Result.fail(
64
67
  ScheduleStorageError.make({ operation: "driver pass", reason: "unavailable" }),
65
68
  );
69
+
66
70
  if (Result.isFailure(deadlineResult) && passSucceeded) {
67
71
  yield* Effect.logWarning("Node scheduling deadline query failed");
68
72
  }
69
73
  const nowMillis = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
74
+
70
75
  const deadlineDelay =
71
76
  Result.isSuccess(deadlineResult) && deadlineResult.success !== null
72
77
  ? Math.max(0, deadlineResult.success - nowMillis)
73
78
  : limits.recoveryPollMillis;
79
+
74
80
  const delay = Math.min(deadlineDelay, limits.recoveryPollMillis);
81
+
75
82
  yield* Effect.raceFirst(wake.await, Effect.sleep(Duration.millis(delay)));
76
83
  }
77
84
  });
@@ -97,10 +104,12 @@ export class NodeScheduling {
97
104
  NodeDurableHost | ScheduleStore | ScheduleAuthorizer
98
105
  > {
99
106
  const limits = options.limits ?? defaultSchedulingLimits;
107
+
100
108
  const schedulingWithDriver = nodeSchedulingDriverLayer(limits).pipe(
101
109
  Layer.provide(ScheduleDriver.layer(limits)),
102
110
  Layer.merge(Scheduling.layer(limits)),
103
111
  );
112
+
104
113
  return schedulingWithDriver.pipe(
105
114
  Layer.provide(
106
115
  Layer.mergeAll(nodeScheduledInputAdmissionLayer, nodeScheduleWakeLayer, NodeCrypto.layer),
@@ -44,6 +44,7 @@ export const nodePreparedInputAdmissionLayer: Layer.Layer<
44
44
  PreparedInputAdmission,
45
45
  Effect.gen(function* () {
46
46
  const host = yield* NodeDurableHost;
47
+
47
48
  return PreparedInputAdmission.of({
48
49
  submit: (envelope) =>
49
50
  host
@@ -140,6 +141,7 @@ const nodeSubscriptionDriverLayer = (
140
141
  }
141
142
 
142
143
  const deadline = yield* store.nextDeadline.pipe(Effect.exit);
144
+
143
145
  if (Exit.isFailure(deadline)) {
144
146
  yield* reportPassFailure(deadline.cause);
145
147
  yield* Effect.sleep(Duration.millis(limits.retryMillis));
@@ -147,10 +149,12 @@ const nodeSubscriptionDriverLayer = (
147
149
  }
148
150
 
149
151
  const nowMillis = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
152
+
150
153
  const delay =
151
154
  deadline.value === null
152
155
  ? limits.retryMillis
153
156
  : Math.max(1, Math.min(deadline.value - nowMillis, limits.retryMillis));
157
+
154
158
  yield* Effect.sleep(Duration.millis(delay));
155
159
  }
156
160
  });
@@ -180,13 +184,16 @@ export class NodeSubscriptions {
180
184
  | SubscriptionInputBindings
181
185
  > {
182
186
  const limits = options.limits ?? defaultSubscriptionLimits;
187
+
183
188
  const publicServices = Layer.merge(
184
189
  Subscriptions.layer(limits),
185
190
  SubscriptionIntake.layer(limits),
186
191
  );
192
+
187
193
  const driver = nodeSubscriptionDriverLayer(limits).pipe(
188
194
  Layer.provide(SubscriptionDriver.layer(limits)),
189
195
  );
196
+
190
197
  return Layer.merge(publicServices, driver).pipe(
191
198
  Layer.provide(nodePreparedInputAdmissionLayer),
192
199
  Layer.provide(NodeCrypto.layer),