@effect-agent/platform-node 0.1.0-beta.37 → 0.1.0-beta.39

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/layers.ts CHANGED
@@ -1,15 +1,27 @@
1
1
  import type { SubmissionId } from "@effect-agent/core";
2
2
  import {
3
3
  CurrentToolFailureObserver,
4
+ RunContextPreparationPassthrough,
5
+ RunToolAuthorization,
4
6
  toolFailureObserverLayer,
7
+ type RunContextPreparation,
5
8
  type RunCostEstimator,
6
9
  type RunToolFailureObserver,
7
10
  } from "@effect-agent/engine";
8
- import type { ScheduleStore } from "@effect-agent/session";
9
11
  import {
10
- type ConversationStore,
12
+ threadStoreLayer,
13
+ scheduleStoreLayer,
14
+ SqliteStorageConfig,
15
+ SqliteStorageConfigValue,
16
+ storageFailpointLayer,
17
+ submissionLedgerLayer,
18
+ type SqliteStorageFailpointHandler,
19
+ type SqliteStorageInitializationError,
20
+ } from "@effect-agent/storage-sqlite";
21
+ import {
22
+ type ScheduleStore,
23
+ type ThreadStore,
11
24
  type WakeScheduler,
12
- AgentBindingResolver,
13
25
  DEFAULT_OWNERSHIP_LEASE_DURATION,
14
26
  DeploymentId,
15
27
  DurableAgentRuntime,
@@ -21,21 +33,10 @@ import {
21
33
  ToolReconciler,
22
34
  type DurableRuntimeFailpointHandler,
23
35
  type OwnershipToken,
24
- type ResolvedBinding,
25
- } from "@effect-agent/session";
26
- import {
27
- conversationStoreLayer,
28
- scheduleStoreLayer,
29
- SqliteStorageConfig,
30
- SqliteStorageConfigValue,
31
- storageFailpointLayer,
32
- submissionLedgerLayer,
33
- type SqliteStorageFailpointHandler,
34
- type SqliteStorageInitializationError,
35
- } from "@effect-agent/storage-sqlite";
36
+ } from "@effect-agent/thread";
36
37
  import { NodeCrypto } from "@effect/platform-node";
37
38
  import { SqliteClient } from "@effect/sql-sqlite-node";
38
- import { Context, Duration, Effect, Layer, Ref, Schema } from "effect";
39
+ import { Context, type Crypto, Duration, Effect, Layer, Ref, Schema } from "effect";
39
40
 
40
41
  import { NodeWakeSchedulerConfig, nodeWakeSchedulerLayer } from "./wake-scheduler.ts";
41
42
 
@@ -63,7 +64,7 @@ export class NodePlatformConfigError extends Schema.TaggedError<NodePlatformConf
63
64
  export class NodeDurableRuntimeConfigValue extends Schema.Class<NodeDurableRuntimeConfigValue>(
64
65
  "@effect-agent/platform-node/NodeDurableRuntimeConfigValue",
65
66
  )({
66
- /** SQLite database file backing BOTH the Conversation Log and the Submission Ledger. */
67
+ /** SQLite database file backing BOTH the Thread Log and the Submission Ledger. */
67
68
  filename: Schema.NonEmptyString,
68
69
  deploymentId: DeploymentId,
69
70
  producerId: ProducerId,
@@ -98,7 +99,12 @@ export class NodeDurableRuntimeConfig extends Context.Service<
98
99
  * to the documented production values; everything is schema-decoded into
99
100
  * `NodeDurableRuntimeConfigValue` before any resource opens (deployment §5 gate 1).
100
101
  */
101
- export interface NodeDurableRuntimeOptions {
102
+ export interface NodeDurableRuntimeOptions<
103
+ ContextError = never,
104
+ ContextRequirements = never,
105
+ AuthorizationError = never,
106
+ AuthorizationRequirements = never,
107
+ > {
102
108
  readonly filename: string;
103
109
  readonly deploymentId: string;
104
110
  readonly producerId: string;
@@ -135,18 +141,25 @@ export interface NodeDurableRuntimeOptions {
135
141
  * DUR-017 resolution path.
136
142
  */
137
143
  readonly toolReconciler?: Layer.Layer<ToolReconciler> | undefined;
144
+ /** Host prompt preparation/compaction, acquired once with the runtime; default pass-through. */
145
+ readonly runContext?:
146
+ | Layer.Layer<RunContextPreparation, ContextError, ContextRequirements | Crypto.Crypto>
147
+ | undefined;
138
148
  /**
139
- * Registered worker Bindings resolved at durable claim time:
140
- * build each with `DurableWorkerBinding.make(binding, digests)` so
141
- * `NodeDurableHost.runResolvedWorkers` / `DurableAgentRuntime.runResolvedWorker` can serve
142
- * parent and child lanes from one pool with exact-digest resolution (SUB-023). Defaults to
143
- * the empty registration: every resolved claim then fails closed (`BindingUnavailable` for a
144
- * root, the framework `ChildCompatibilityFailure` Settlement for a parent-linked child).
149
+ * Independent action-time Tool authority, acquired once with the runtime; default allow-all.
150
+ * Construction errors and application dependencies remain in the assembled Layer's E and R.
151
+ * The platform supplies Crypto to both extension Layers.
145
152
  */
146
- readonly bindings?: ReadonlyArray<ResolvedBinding> | undefined;
153
+ readonly toolAuthorization?:
154
+ | Layer.Layer<
155
+ RunToolAuthorization,
156
+ AuthorizationError,
157
+ AuthorizationRequirements | Crypto.Crypto
158
+ >
159
+ | undefined;
147
160
  }
148
161
 
149
- /** Every construction failure of the assembled Node durable runtime stack. */
162
+ /** Built-in construction failures. `layer` also preserves supplied service Layers' errors. */
150
163
  export type NodeDurableRuntimeInitializationError =
151
164
  | NodePlatformConfigError
152
165
  | SqliteStorageInitializationError;
@@ -155,17 +168,16 @@ export type NodeDurableRuntimeInitializationError =
155
168
  export type NodeDurableRuntimeServices =
156
169
  | DurableAgentRuntime
157
170
  | SubmissionLedger
158
- | ConversationStore
171
+ | ThreadStore
159
172
  | ScheduleStore
160
173
  | WakeScheduler
161
174
  | DurableRuntimeConfig
162
- | NodeDurableRuntimeConfig
163
- | AgentBindingResolver;
175
+ | NodeDurableRuntimeConfig;
164
176
 
165
177
  const decodeConfigValue = Schema.decodeUnknownEffect(NodeDurableRuntimeConfigValue);
166
178
 
167
179
  const configFromOptions = (
168
- options: NodeDurableRuntimeOptions,
180
+ options: Omit<NodeDurableRuntimeOptions, "runContext" | "toolAuthorization">,
169
181
  ): Effect.Effect<NodeDurableRuntimeConfigValue, NodePlatformConfigError> =>
170
182
  decodeConfigValue({
171
183
  filename: options.filename,
@@ -204,7 +216,7 @@ const sqliteStorageConfigLayer: Layer.Layer<SqliteStorageConfig, never, NodeDura
204
216
  }),
205
217
  );
206
218
 
207
- /** Session coordinator configuration derived from the single validated Node configuration. */
219
+ /** Thread coordinator configuration derived from the single validated Node configuration. */
208
220
  const durableRuntimeConfigLayer = (
209
221
  estimateCostMicrousd: RunCostEstimator | undefined,
210
222
  ): Layer.Layer<DurableRuntimeConfig, never, NodeDurableRuntimeConfig> =>
@@ -344,7 +356,7 @@ export const ownershipDrainLayer: Layer.Layer<SubmissionLedger, never, Submissio
344
356
  /**
345
357
  * The DN Layer assembly (deployment §12: a Layer-assembly library, not an app entrypoint).
346
358
  * `layer(options)` decodes the configuration, opens ONE SQLite database serving both the
347
- * Conversation Log and the Submission Ledger (so claims fence the same producer epochs), wires
359
+ * Thread Log and the Submission Ledger (so claims fence the same producer epochs), wires
348
360
  * the Node wake scheduler with its ledger-scan fallback, wraps the ledger with the shutdown
349
361
  * ownership drain, defaults the Tool reconciliation policy to the fail-closed
350
362
  * `ToolReconciler.uncertain` (override via `options.toolReconciler`), and provides a ready
@@ -354,16 +366,40 @@ export const ownershipDrainLayer: Layer.Layer<SubmissionLedger, never, Submissio
354
366
  */
355
367
  export class NodeDurableRuntime {
356
368
  /** Validated configuration Layer; fails typed when the supplied options are out of bounds. */
357
- static configLayer(
358
- options: NodeDurableRuntimeOptions,
369
+ static configLayer<
370
+ ContextError = never,
371
+ ContextRequirements = never,
372
+ AuthorizationError = never,
373
+ AuthorizationRequirements = never,
374
+ >(
375
+ options: NodeDurableRuntimeOptions<
376
+ ContextError,
377
+ ContextRequirements,
378
+ AuthorizationError,
379
+ AuthorizationRequirements
380
+ >,
359
381
  ): Layer.Layer<NodeDurableRuntimeConfig, NodePlatformConfigError> {
360
382
  return Layer.effect(NodeDurableRuntimeConfig)(configFromOptions(options));
361
383
  }
362
384
 
363
385
  /** The full DN runtime stack over one SQLite file. */
364
- static layer(
365
- options: NodeDurableRuntimeOptions,
366
- ): Layer.Layer<NodeDurableRuntimeServices, NodeDurableRuntimeInitializationError> {
386
+ static layer<
387
+ ContextError = never,
388
+ ContextRequirements = never,
389
+ AuthorizationError = never,
390
+ AuthorizationRequirements = never,
391
+ >(
392
+ options: NodeDurableRuntimeOptions<
393
+ ContextError,
394
+ ContextRequirements,
395
+ AuthorizationError,
396
+ AuthorizationRequirements
397
+ >,
398
+ ): Layer.Layer<
399
+ NodeDurableRuntimeServices,
400
+ NodeDurableRuntimeInitializationError | ContextError | AuthorizationError,
401
+ Exclude<ContextRequirements | AuthorizationRequirements, Crypto.Crypto>
402
+ > {
367
403
  return Layer.unwrap(
368
404
  Effect.map(configFromOptions(options), (config) => {
369
405
  const nodeConfigLayer = Layer.succeed(NodeDurableRuntimeConfig)(config);
@@ -385,21 +421,16 @@ export class NodeDurableRuntime {
385
421
  options.toolFailureObserver === undefined
386
422
  ? Layer.succeed(CurrentToolFailureObserver)(undefined)
387
423
  : toolFailureObserverLayer(options.toolFailureObserver);
388
- const bindingResolverLayer = AgentBindingResolver.layer(options.bindings ?? []);
389
424
  const ports = Layer.mergeAll(
390
- conversationStoreLayer,
425
+ threadStoreLayer,
391
426
  scheduleStoreLayer,
392
427
  nodeWakeSchedulerLayer.pipe(
393
428
  Layer.provideMerge(ownershipDrainLayer.pipe(Layer.provide(submissionLedgerLayer))),
394
429
  ),
395
430
  );
396
- return DurableAgentRuntime.layer.pipe(
431
+ return DurableAgentRuntime.layerWithServices.pipe(
397
432
  Layer.provideMerge(
398
- Layer.mergeAll(
399
- ports,
400
- durableRuntimeConfigLayer(options.estimateCostMicrousd),
401
- bindingResolverLayer,
402
- ),
433
+ Layer.mergeAll(ports, durableRuntimeConfigLayer(options.estimateCostMicrousd)),
403
434
  ),
404
435
  Layer.provide(
405
436
  Layer.mergeAll(
@@ -411,6 +442,12 @@ export class NodeDurableRuntime {
411
442
  ),
412
443
  Layer.provideMerge(infrastructure),
413
444
  Layer.provideMerge(nodeConfigLayer),
445
+ Layer.provide(
446
+ Layer.mergeAll(
447
+ options.runContext ?? RunContextPreparationPassthrough,
448
+ options.toolAuthorization ?? RunToolAuthorization.allowAll,
449
+ ).pipe(Layer.provide(NodeCrypto.layer)),
450
+ ),
414
451
  );
415
452
  }),
416
453
  );
package/src/scheduling.ts CHANGED
@@ -1,74 +1,20 @@
1
- import type { AgentId } from "@effect-agent/core";
2
1
  import {
3
- type DurableSubmitAgent,
4
2
  type ScheduleProcessFailure,
5
3
  type ScheduleAuthorizer,
6
4
  type SchedulingLimits,
7
5
  ScheduleStorageError,
8
6
  ScheduleStore,
9
7
  type ScheduleValidationError,
10
- ScheduledInputAdmission,
11
- ScheduledInputRetryable,
12
8
  ScheduleWake,
13
9
  Scheduling,
14
10
  ScheduleDriver,
15
11
  defaultSchedulingLimits,
16
- PersistedJson,
17
- } from "@effect-agent/session";
12
+ } from "@effect-agent/thread";
18
13
  import { NodeCrypto } from "@effect/platform-node";
19
14
  import { Cause, Duration, Effect, Layer, Option, PubSub, Result } from "effect";
20
15
 
21
- import { NodeDurableHost } from "./host.ts";
22
-
23
- const passthroughSubmitAgent = (agentId: AgentId): DurableSubmitAgent<typeof PersistedJson> => ({
24
- definition: { id: agentId, input: PersistedJson },
25
- });
26
-
27
- const ambiguous = (): ScheduledInputRetryable =>
28
- ScheduledInputRetryable.make({ reason: "ambiguous" });
29
-
30
- const corrupt = (operation: string): ScheduleStorageError =>
31
- ScheduleStorageError.make({ operation, reason: "corrupt" });
32
-
33
- /**
34
- * Scheduled admission through the existing host gate. Once the gate admits the call, every
35
- * runtime failure stays ambiguous because the Submission may already have committed.
36
- */
37
- export const nodeScheduledInputAdmissionLayer: Layer.Layer<
38
- ScheduledInputAdmission,
39
- never,
40
- NodeDurableHost
41
- > = Layer.effect(
42
- ScheduledInputAdmission,
43
- Effect.gen(function* () {
44
- const host = yield* NodeDurableHost;
45
- return ScheduledInputAdmission.of({
46
- submit: (envelope) =>
47
- host
48
- .submit(passthroughSubmitAgent(envelope.agentId), envelope.input, {
49
- conversationId: envelope.conversationId,
50
- principal: envelope.deliveryPrincipal,
51
- idempotencyKey: envelope.admissionKey,
52
- definitions: envelope.definitions,
53
- })
54
- .pipe(
55
- Effect.catchTags({
56
- AdmissionClosed: () =>
57
- Effect.fail(ScheduledInputRetryable.make({ reason: "host-closed" })),
58
- AgentInputError: () => Effect.fail(corrupt("scheduled admission input")),
59
- AdmissionConflict: () => Effect.fail(corrupt("scheduled admission conflict")),
60
- DigestError: () => Effect.fail(ambiguous()),
61
- LedgerError: () => Effect.fail(ambiguous()),
62
- ConversationStoreError: () => Effect.fail(ambiguous()),
63
- ConversationNotMaterialized: () => Effect.fail(ambiguous()),
64
- AppendConflict: () => Effect.fail(ambiguous()),
65
- FenceRejected: () => Effect.fail(ambiguous()),
66
- DurableRuntimeFailpointError: () => Effect.fail(ambiguous()),
67
- }),
68
- ),
69
- });
70
- }),
71
- );
16
+ import type { NodeDurableHost } from "./host.ts";
17
+ import { nodeScheduledInputAdmissionLayer } from "./subscriptions.ts";
72
18
 
73
19
  /** One bounded hint slot for the single supported Node scheduler. Indexed polling repairs loss. */
74
20
  export const nodeScheduleWakeLayer: Layer.Layer<ScheduleWake> = Layer.effect(
@@ -0,0 +1,195 @@
1
+ import type { AgentId } from "@effect-agent/core";
2
+ import {
3
+ type DurableSubmitAgent,
4
+ type EventSources,
5
+ type SubscriptionInputBindings,
6
+ PersistedJson,
7
+ type PreparedInput,
8
+ PreparedInputAdmission,
9
+ type ScheduledEnvelope,
10
+ ScheduledInputAdmission,
11
+ ScheduledInputRetryable,
12
+ type SubscriptionAuthorizer,
13
+ SubscriptionDriver,
14
+ type SubscriptionError,
15
+ SubscriptionIntake,
16
+ type SubscriptionLimits,
17
+ type SubscriptionStoreFailure,
18
+ SubscriptionStore,
19
+ Subscriptions,
20
+ defaultSubscriptionLimits,
21
+ ScheduleStorageError,
22
+ } from "@effect-agent/thread";
23
+ import { NodeCrypto } from "@effect/platform-node";
24
+ import { Cause, Duration, Effect, Exit, Layer, Option } from "effect";
25
+
26
+ import { NodeDurableHost } from "./host.ts";
27
+
28
+ const passthroughSubmitAgent = (agentId: AgentId): DurableSubmitAgent<typeof PersistedJson> => ({
29
+ definition: { id: agentId, input: PersistedJson },
30
+ });
31
+
32
+ const ambiguous = (): ScheduledInputRetryable =>
33
+ ScheduledInputRetryable.make({ reason: "ambiguous" });
34
+
35
+ const corrupt = (operation: string): ScheduleStorageError =>
36
+ ScheduleStorageError.make({ operation, reason: "corrupt" });
37
+
38
+ /** Ordinary prepared admission through the Scope-owned Node host gate. */
39
+ export const nodePreparedInputAdmissionLayer: Layer.Layer<
40
+ PreparedInputAdmission,
41
+ never,
42
+ NodeDurableHost
43
+ > = Layer.effect(
44
+ PreparedInputAdmission,
45
+ Effect.gen(function* () {
46
+ const host = yield* NodeDurableHost;
47
+ return PreparedInputAdmission.of({
48
+ submit: (envelope) =>
49
+ host
50
+ .submit(passthroughSubmitAgent(envelope.agentId), envelope.input, {
51
+ threadId: envelope.threadId,
52
+ principal: envelope.deliveryPrincipal,
53
+ idempotencyKey: envelope.admissionKey,
54
+ definitions: envelope.definitions,
55
+ })
56
+ .pipe(
57
+ Effect.catchTags({
58
+ AdmissionClosed: () =>
59
+ Effect.fail(ScheduledInputRetryable.make({ reason: "host-closed" })),
60
+ AgentInputError: () => Effect.fail(corrupt("prepared admission input")),
61
+ AdmissionConflict: () => Effect.fail(corrupt("prepared admission conflict")),
62
+ DigestError: () => Effect.fail(ambiguous()),
63
+ LedgerError: () => Effect.fail(ambiguous()),
64
+ ThreadStoreError: () => Effect.fail(ambiguous()),
65
+ ThreadNotMaterialized: () => Effect.fail(ambiguous()),
66
+ AppendConflict: () => Effect.fail(ambiguous()),
67
+ FenceRejected: () => Effect.fail(ambiguous()),
68
+ DurableRuntimeFailpointError: () => Effect.fail(ambiguous()),
69
+ }),
70
+ ),
71
+ });
72
+ }),
73
+ );
74
+
75
+ const preparedFromSchedule = (envelope: ScheduledEnvelope): PreparedInput => ({
76
+ schemaVersion: 1,
77
+ threadId: envelope.threadId,
78
+ deliveryPrincipal: envelope.deliveryPrincipal,
79
+ agentId: envelope.agentId,
80
+ definitions: envelope.definitions,
81
+ input: envelope.input,
82
+ inputDigest: envelope.inputDigest,
83
+ admissionKey: envelope.admissionKey,
84
+ authorization: envelope.authorization,
85
+ });
86
+
87
+ /** Compatibility adapter retaining the public scheduling admission port. */
88
+ const nodeScheduledInputAdmissionFromPreparedLayer: Layer.Layer<
89
+ ScheduledInputAdmission,
90
+ never,
91
+ PreparedInputAdmission
92
+ > = Layer.effect(
93
+ ScheduledInputAdmission,
94
+ Effect.map(PreparedInputAdmission, (admission) =>
95
+ ScheduledInputAdmission.of({
96
+ submit: (envelope) => admission.submit(preparedFromSchedule(envelope)),
97
+ }),
98
+ ),
99
+ );
100
+
101
+ export const nodeScheduledInputAdmissionLayer: Layer.Layer<
102
+ ScheduledInputAdmission,
103
+ never,
104
+ NodeDurableHost
105
+ > = nodeScheduledInputAdmissionFromPreparedLayer.pipe(
106
+ Layer.provide(nodePreparedInputAdmissionLayer),
107
+ );
108
+
109
+ const reportPassFailure = (cause: Cause.Cause<SubscriptionStoreFailure>): Effect.Effect<boolean> =>
110
+ Cause.hasInterruptsOnly(cause)
111
+ ? Effect.interrupt
112
+ : Effect.logWarning("Node subscription pass failed").pipe(
113
+ Effect.annotateLogs({
114
+ failureTag: Option.match(Cause.findErrorOption(cause), {
115
+ onNone: () => "Defect",
116
+ onSome: (error) => error._tag,
117
+ }),
118
+ }),
119
+ Effect.as(false),
120
+ );
121
+
122
+ const nodeSubscriptionDriverLayer = (
123
+ limits: SubscriptionLimits,
124
+ ): Layer.Layer<never, never, SubscriptionDriver | SubscriptionStore> =>
125
+ Layer.effectDiscard(
126
+ Effect.gen(function* () {
127
+ const driver = yield* SubscriptionDriver;
128
+ const store = yield* SubscriptionStore;
129
+
130
+ const run = Effect.gen(function* () {
131
+ while (true) {
132
+ const passSucceeded = yield* driver.runDue.pipe(
133
+ Effect.map((pass) => pass.failed === 0),
134
+ Effect.catchCause(reportPassFailure),
135
+ );
136
+
137
+ if (!passSucceeded) {
138
+ yield* Effect.sleep(Duration.millis(limits.retryMillis));
139
+ continue;
140
+ }
141
+
142
+ const deadline = yield* store.nextDeadline.pipe(Effect.exit);
143
+ if (Exit.isFailure(deadline)) {
144
+ yield* reportPassFailure(deadline.cause);
145
+ yield* Effect.sleep(Duration.millis(limits.retryMillis));
146
+ continue;
147
+ }
148
+
149
+ const nowMillis = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
150
+ const delay =
151
+ deadline.value === null
152
+ ? limits.retryMillis
153
+ : Math.max(1, Math.min(deadline.value - nowMillis, limits.retryMillis));
154
+ yield* Effect.sleep(Duration.millis(delay));
155
+ }
156
+ });
157
+
158
+ yield* Effect.forkScoped(run);
159
+ }),
160
+ );
161
+
162
+ export interface NodeSubscriptionsOptions {
163
+ readonly limits?: SubscriptionLimits | undefined;
164
+ }
165
+
166
+ /**
167
+ * One Scope-owned subscription partition in the sole process owning its SQLite database.
168
+ * Indexed polling repairs restart and lost wake state; closing the Scope interrupts the driver.
169
+ */
170
+ export class NodeSubscriptions {
171
+ static layer(
172
+ options: NodeSubscriptionsOptions = {},
173
+ ): Layer.Layer<
174
+ Subscriptions | SubscriptionIntake,
175
+ SubscriptionError,
176
+ | NodeDurableHost
177
+ | SubscriptionStore
178
+ | SubscriptionAuthorizer
179
+ | EventSources
180
+ | SubscriptionInputBindings
181
+ > {
182
+ const limits = options.limits ?? defaultSubscriptionLimits;
183
+ const publicServices = Layer.merge(
184
+ Subscriptions.layer(limits),
185
+ SubscriptionIntake.layer(limits),
186
+ );
187
+ const driver = nodeSubscriptionDriverLayer(limits).pipe(
188
+ Layer.provide(SubscriptionDriver.layer(limits)),
189
+ );
190
+ return Layer.merge(publicServices, driver).pipe(
191
+ Layer.provide(nodePreparedInputAdmissionLayer),
192
+ Layer.provide(NodeCrypto.layer),
193
+ );
194
+ }
195
+ }
@@ -1,6 +1,7 @@
1
- import type { ConversationId } from "@effect-agent/core";
2
- import { makeWakeSubscriptionHub, SubmissionLedger, WakeScheduler } from "@effect-agent/session";
3
- import { Context, Duration, Effect, Layer, PubSub, Stream } from "effect";
1
+ import type { ThreadId } from "@effect-agent/core";
2
+ import { makeWakeSubscriptionHub, SubmissionLedger, WakeScheduler } from "@effect-agent/thread";
3
+ import type { Duration } from "effect";
4
+ import { Context, Effect, Layer, PubSub, Stream } from "effect";
4
5
 
5
6
  /**
6
7
  * Bounded in-process wake buffer. Wake hints are droppable by contract (the ledger-scan fallback
@@ -12,7 +13,7 @@ const WAKE_BUFFER_CAPACITY = 1_024;
12
13
  export class NodeWakeSchedulerConfig extends Context.Service<
13
14
  NodeWakeSchedulerConfig,
14
15
  {
15
- /** Interval between ledger scans that re-emit every nonterminal Conversation lane. */
16
+ /** Interval between ledger scans that re-emit every nonterminal Thread lane. */
16
17
  readonly scanInterval: Duration.Duration;
17
18
  }
18
19
  >()("@effect-agent/platform-node/NodeWakeSchedulerConfig") {
@@ -26,28 +27,28 @@ export class NodeWakeSchedulerConfig extends Context.Service<
26
27
  const makeWakeScheduler = Effect.gen(function* () {
27
28
  const ledger = yield* SubmissionLedger;
28
29
  const config = yield* NodeWakeSchedulerConfig;
29
- const hints = yield* PubSub.sliding<ConversationId>(WAKE_BUFFER_CAPACITY);
30
+ const hints = yield* PubSub.sliding<ThreadId>(WAKE_BUFFER_CAPACITY);
30
31
  const progress = yield* makeWakeSubscriptionHub;
31
32
  yield* Effect.addFinalizer(() => PubSub.shutdown(hints));
32
33
 
33
34
  /**
34
- * One fallback scan: every Conversation lane with nonterminal work, deduplicated. A scan
35
+ * One fallback scan: every Thread lane with nonterminal work, deduplicated. A scan
35
36
  * failure degrades to "no hints this round" — the wake channel has no error contract and the
36
37
  * next round retries — but is logged so a persistently failing ledger stays visible.
37
38
  */
38
- const scanOnce: Effect.Effect<ReadonlyArray<ConversationId>> = Stream.runCollect(
39
+ const scanOnce: Effect.Effect<ReadonlyArray<ThreadId>> = Stream.runCollect(
39
40
  ledger.scanNonterminal,
40
41
  ).pipe(
41
42
  Effect.map((snapshots) => {
42
- const lanes = new Set<ConversationId>();
43
+ const lanes = new Set<ThreadId>();
43
44
  for (const snapshot of snapshots) {
44
- lanes.add(snapshot.conversationId);
45
+ lanes.add(snapshot.threadId);
45
46
  }
46
47
  return [...lanes];
47
48
  }),
48
49
  Effect.catch((error) =>
49
50
  Effect.logWarning("NodeWakeScheduler fallback scan failed", error).pipe(
50
- Effect.as([] as ReadonlyArray<ConversationId>),
51
+ Effect.as([] as ReadonlyArray<ThreadId>),
51
52
  ),
52
53
  ),
53
54
  );
@@ -57,15 +58,15 @@ const makeWakeScheduler = Effect.gen(function* () {
57
58
  * merges its PubSub subscription with a Clock-driven ledger-scan loop. Both live entirely in
58
59
  * the consuming run's Scope; no fiber outlives its subscriber.
59
60
  */
60
- const fallbackScans: Stream.Stream<ConversationId> = Stream.fromIterableEffectRepeat(
61
+ const fallbackScans: Stream.Stream<ThreadId> = Stream.fromIterableEffectRepeat(
61
62
  Effect.sleep(config.scanInterval).pipe(Effect.andThen(scanOnce)),
62
63
  );
63
64
 
64
65
  return WakeScheduler.of({
65
- notify: (conversationId) =>
66
+ notify: (threadId) =>
66
67
  progress
67
- .notify(conversationId)
68
- .pipe(Effect.andThen(PubSub.publish(hints, conversationId)), Effect.asVoid),
68
+ .notify(threadId)
69
+ .pipe(Effect.andThen(PubSub.publish(hints, threadId)), Effect.asVoid),
69
70
  subscribe: progress.subscribe,
70
71
  wakes: Stream.merge(Stream.fromPubSub(hints), fallbackScans),
71
72
  });