@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/dist/index.d.mts +49 -35
- package/dist/index.mjs +84 -33
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -8
- package/src/host.ts +103 -84
- package/src/index.ts +1 -0
- package/src/layers.ts +82 -45
- package/src/scheduling.ts +3 -57
- package/src/subscriptions.ts +195 -0
- package/src/wake-scheduler.ts +15 -14
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
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
|
|
153
|
+
readonly toolAuthorization?:
|
|
154
|
+
| Layer.Layer<
|
|
155
|
+
RunToolAuthorization,
|
|
156
|
+
AuthorizationError,
|
|
157
|
+
AuthorizationRequirements | Crypto.Crypto
|
|
158
|
+
>
|
|
159
|
+
| undefined;
|
|
147
160
|
}
|
|
148
161
|
|
|
149
|
-
/**
|
|
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
|
-
|
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
366
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|
package/src/wake-scheduler.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { makeWakeSubscriptionHub, SubmissionLedger, WakeScheduler } from "@effect-agent/
|
|
3
|
-
import {
|
|
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
|
|
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<
|
|
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
|
|
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<
|
|
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<
|
|
43
|
+
const lanes = new Set<ThreadId>();
|
|
43
44
|
for (const snapshot of snapshots) {
|
|
44
|
-
lanes.add(snapshot.
|
|
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<
|
|
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<
|
|
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: (
|
|
66
|
+
notify: (threadId) =>
|
|
66
67
|
progress
|
|
67
|
-
.notify(
|
|
68
|
-
.pipe(Effect.andThen(PubSub.publish(hints,
|
|
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
|
});
|