@effect-agent/platform-cloudflare 0.1.0-beta.14 → 0.1.0-beta.16
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 +192 -120
- package/dist/index.mjs +351 -105
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/alarm.ts +330 -113
- package/src/bindings.ts +4 -0
- package/src/client.ts +150 -2
- package/src/conversation-object.ts +87 -67
- package/src/index.ts +4 -2
- package/src/layers.ts +39 -7
- package/src/progress-wait.ts +103 -0
- package/src/wake-scheduler.ts +5 -2
package/src/layers.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { ConversationId } from "@effect-agent/core";
|
|
2
2
|
import {
|
|
3
3
|
AgentBindingResolver,
|
|
4
|
-
ConversationStore,
|
|
5
4
|
DurableAgentRuntime,
|
|
6
5
|
DurableRuntimeConfig,
|
|
7
6
|
DurableRuntimeFailpoint,
|
|
8
7
|
ProducerId,
|
|
9
|
-
|
|
8
|
+
operationAuthorizerLayer,
|
|
10
9
|
ToolReconciler,
|
|
11
|
-
|
|
10
|
+
type ConversationStore,
|
|
12
11
|
type DurableRuntimeFailpointHandler,
|
|
12
|
+
type OperationAuthorizerService,
|
|
13
13
|
type ResolvedBinding,
|
|
14
|
+
type SubmissionLedger,
|
|
15
|
+
type WakeScheduler,
|
|
14
16
|
} from "@effect-agent/session";
|
|
15
17
|
import {
|
|
16
18
|
conversationStoreLayer,
|
|
@@ -28,11 +30,16 @@ import { BrowserCrypto } from "@effect/platform-browser";
|
|
|
28
30
|
import { SqliteClient } from "@effect/sql-sqlite-do";
|
|
29
31
|
import { Context, Duration, Effect, Layer, Schema } from "effect";
|
|
30
32
|
|
|
31
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
ConversationMaintenance,
|
|
35
|
+
ConversationMaintenanceFailpoint,
|
|
36
|
+
DurableAlarmService,
|
|
37
|
+
type ConversationMaintenanceFailpointHandler,
|
|
38
|
+
} from "./alarm.ts";
|
|
32
39
|
import {
|
|
33
40
|
ConversationObjectIdentity,
|
|
34
|
-
ConversationObjectNamespace,
|
|
35
41
|
DurableObjectContext,
|
|
42
|
+
type ConversationObjectNamespace,
|
|
36
43
|
} from "./bindings.ts";
|
|
37
44
|
import {
|
|
38
45
|
CLOUDFLARE_RUNTIME_DEFAULTS,
|
|
@@ -40,6 +47,7 @@ import {
|
|
|
40
47
|
CloudflareDurableRuntimeConfigValue,
|
|
41
48
|
CloudflarePlatformConfigError,
|
|
42
49
|
} from "./config.ts";
|
|
50
|
+
import { ProgressWaitRegistry } from "./progress-wait.ts";
|
|
43
51
|
import { conversationPortTransportLayer } from "./transport.ts";
|
|
44
52
|
import { cloudflareWakeSchedulerLayer } from "./wake-scheduler.ts";
|
|
45
53
|
|
|
@@ -89,6 +97,12 @@ export interface CloudflareDurableRuntimeOptions {
|
|
|
89
97
|
readonly runtimeFailpoint?:
|
|
90
98
|
| ((ctx: DurableObjectState) => DurableRuntimeFailpointHandler)
|
|
91
99
|
| undefined;
|
|
100
|
+
/** Conversation-maintenance generation/alarm fault injection; default none. */
|
|
101
|
+
readonly maintenanceFailpoint?:
|
|
102
|
+
| ((ctx: DurableObjectState) => ConversationMaintenanceFailpointHandler)
|
|
103
|
+
| undefined;
|
|
104
|
+
/** Host-supplied fail-closed authorization policy; defaults to service possession. */
|
|
105
|
+
readonly operationAuthorizer?: OperationAuthorizerService | undefined;
|
|
92
106
|
/**
|
|
93
107
|
* Reconciliation policy consulted for open ordinary Tool Calls before an Unknown Outcome
|
|
94
108
|
* is recorded (durability §10, DUR-009). Defaults to the fail-closed
|
|
@@ -143,7 +157,8 @@ export type CloudflareDurableRuntimeServices =
|
|
|
143
157
|
| ConversationObjectIdentity
|
|
144
158
|
| DurableAlarmService
|
|
145
159
|
| ConversationMaintenance
|
|
146
|
-
| ConversationObjectPorts
|
|
160
|
+
| ConversationObjectPorts
|
|
161
|
+
| ProgressWaitRegistry;
|
|
147
162
|
|
|
148
163
|
/**
|
|
149
164
|
* Owner-side endpoint body for the Conversation Object's `portCall` (plan §1.3): decode,
|
|
@@ -340,7 +355,17 @@ export class CloudflareDurableRuntime {
|
|
|
340
355
|
options.runtimeFailpoint === undefined
|
|
341
356
|
? DurableRuntimeFailpoint.layer
|
|
342
357
|
: Layer.succeed(DurableRuntimeFailpoint)({ hit: options.runtimeFailpoint(ctx) });
|
|
358
|
+
const maintenanceFailpointLayer =
|
|
359
|
+
options.maintenanceFailpoint === undefined
|
|
360
|
+
? ConversationMaintenanceFailpoint.layer
|
|
361
|
+
: Layer.succeed(ConversationMaintenanceFailpoint)({
|
|
362
|
+
hit: options.maintenanceFailpoint(ctx),
|
|
363
|
+
});
|
|
343
364
|
const reconcilerLayer = options.toolReconciler ?? ToolReconciler.uncertain;
|
|
365
|
+
const authorizerLayer =
|
|
366
|
+
options.operationAuthorizer === undefined
|
|
367
|
+
? Layer.empty
|
|
368
|
+
: operationAuthorizerLayer(options.operationAuthorizer);
|
|
344
369
|
const bindingResolverLayer = Layer.effect(AgentBindingResolver)(
|
|
345
370
|
Effect.map(
|
|
346
371
|
resolveBindings(options.bindings, { ctx, env, conversationId, producerId }),
|
|
@@ -352,6 +377,8 @@ export class CloudflareDurableRuntime {
|
|
|
352
377
|
identityLayer,
|
|
353
378
|
cloudflareConfigLayer,
|
|
354
379
|
DurableAlarmService.layer,
|
|
380
|
+
maintenanceFailpointLayer,
|
|
381
|
+
ProgressWaitRegistry.layer,
|
|
355
382
|
);
|
|
356
383
|
|
|
357
384
|
const runtimeStack = DurableAgentRuntime.layer.pipe(
|
|
@@ -360,7 +387,12 @@ export class CloudflareDurableRuntime {
|
|
|
360
387
|
Layer.provideMerge(runtimeConfigLayer),
|
|
361
388
|
Layer.provideMerge(bindingResolverLayer),
|
|
362
389
|
Layer.provide(
|
|
363
|
-
Layer.mergeAll(
|
|
390
|
+
Layer.mergeAll(
|
|
391
|
+
runtimeFailpointLayer,
|
|
392
|
+
reconcilerLayer,
|
|
393
|
+
authorizerLayer,
|
|
394
|
+
BrowserCrypto.layer,
|
|
395
|
+
),
|
|
364
396
|
),
|
|
365
397
|
Layer.provideMerge(base),
|
|
366
398
|
);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Context, Deferred, Effect, Layer, Ref, type Scope } from "effect";
|
|
2
|
+
|
|
3
|
+
/** Cancellation tombstones are bounded hints, never durable authority. */
|
|
4
|
+
const MAX_CANCELLATION_TOMBSTONES = 1_024;
|
|
5
|
+
|
|
6
|
+
type ActiveRegistration = ReadonlySet<Deferred.Deferred<void>>;
|
|
7
|
+
type Registration = ActiveRegistration | "cancelled";
|
|
8
|
+
type Registrations = ReadonlyMap<string, Registration>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Per-incarnation cancellation registry for long-lived progress RPCs. The public runtime owns
|
|
12
|
+
* the actual wake registration; this host-only registry lets an interrupted Worker Effect ask
|
|
13
|
+
* the Object to interrupt its scoped wait before the Worker execution context itself ends.
|
|
14
|
+
*/
|
|
15
|
+
export class ProgressWaitRegistry extends Context.Service<
|
|
16
|
+
ProgressWaitRegistry,
|
|
17
|
+
{
|
|
18
|
+
/** Register a Scope-owned cancellation signal, observing any early cancel tombstone. */
|
|
19
|
+
readonly subscribe: (
|
|
20
|
+
waiterId: string,
|
|
21
|
+
) => Effect.Effect<Effect.Effect<void>, never, Scope.Scope>;
|
|
22
|
+
/** Cancel a registered waiter, or remember a bounded early cancellation. */
|
|
23
|
+
readonly cancel: (waiterId: string) => Effect.Effect<void>;
|
|
24
|
+
}
|
|
25
|
+
>()("@effect-agent/platform-cloudflare/ProgressWaitRegistry") {
|
|
26
|
+
static readonly layer: Layer.Layer<ProgressWaitRegistry> = Layer.effect(
|
|
27
|
+
ProgressWaitRegistry,
|
|
28
|
+
Effect.gen(function* () {
|
|
29
|
+
const registrations = yield* Ref.make<Registrations>(new Map());
|
|
30
|
+
|
|
31
|
+
const remove = (waiterId: string, deferred: Deferred.Deferred<void>) =>
|
|
32
|
+
Ref.update(registrations, (current) => {
|
|
33
|
+
const existing = current.get(waiterId);
|
|
34
|
+
if (existing === undefined || existing === "cancelled" || !existing.has(deferred)) {
|
|
35
|
+
return current;
|
|
36
|
+
}
|
|
37
|
+
const next = new Map(current);
|
|
38
|
+
const active = new Set(existing);
|
|
39
|
+
active.delete(deferred);
|
|
40
|
+
if (active.size === 0) {
|
|
41
|
+
next.delete(waiterId);
|
|
42
|
+
} else {
|
|
43
|
+
next.set(waiterId, active);
|
|
44
|
+
}
|
|
45
|
+
return next;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const subscribe = Effect.fn("ProgressWaitRegistry.subscribe")(
|
|
49
|
+
(waiterId: string): Effect.Effect<Effect.Effect<void>, never, Scope.Scope> =>
|
|
50
|
+
Effect.gen(function* () {
|
|
51
|
+
const deferred = yield* Deferred.make<void>();
|
|
52
|
+
yield* Effect.addFinalizer(() => remove(waiterId, deferred));
|
|
53
|
+
const cancelled = yield* Ref.modify(registrations, (current) => {
|
|
54
|
+
const existing = current.get(waiterId);
|
|
55
|
+
const next = new Map(current);
|
|
56
|
+
if (existing === "cancelled") {
|
|
57
|
+
return [true, current] as const;
|
|
58
|
+
}
|
|
59
|
+
const active = new Set(existing ?? []);
|
|
60
|
+
active.add(deferred);
|
|
61
|
+
next.set(waiterId, active);
|
|
62
|
+
return [false, next] as const;
|
|
63
|
+
});
|
|
64
|
+
return { cancelled, deferred };
|
|
65
|
+
}).pipe(
|
|
66
|
+
Effect.map(({ cancelled, deferred }) =>
|
|
67
|
+
cancelled ? Effect.void : Deferred.await(deferred),
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const cancel = Effect.fn("ProgressWaitRegistry.cancel")(function* (waiterId: string) {
|
|
73
|
+
const waiters = yield* Ref.modify(registrations, (current) => {
|
|
74
|
+
const existing = current.get(waiterId);
|
|
75
|
+
const next = new Map(current);
|
|
76
|
+
if (existing === undefined) {
|
|
77
|
+
next.set(waiterId, "cancelled");
|
|
78
|
+
let tombstones = 0;
|
|
79
|
+
for (const registration of next.values()) {
|
|
80
|
+
if (registration === "cancelled") tombstones += 1;
|
|
81
|
+
}
|
|
82
|
+
if (tombstones > MAX_CANCELLATION_TOMBSTONES) {
|
|
83
|
+
for (const [id, registration] of next) {
|
|
84
|
+
if (registration !== "cancelled") continue;
|
|
85
|
+
next.delete(id);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return [[], next] as const;
|
|
90
|
+
}
|
|
91
|
+
if (existing === "cancelled") return [[], current] as const;
|
|
92
|
+
next.delete(waiterId);
|
|
93
|
+
return [[...existing], next] as const;
|
|
94
|
+
});
|
|
95
|
+
yield* Effect.forEach(waiters, (waiter) => Deferred.succeed(waiter, undefined), {
|
|
96
|
+
discard: true,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return ProgressWaitRegistry.of({ subscribe, cancel });
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
}
|
package/src/wake-scheduler.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConversationId } from "@effect-agent/core";
|
|
2
|
-
import { WakeScheduler } from "@effect-agent/session";
|
|
2
|
+
import { makeWakeSubscriptionHub, WakeScheduler } from "@effect-agent/session";
|
|
3
3
|
import { Effect, Layer, PubSub, Schema, Stream } from "effect";
|
|
4
4
|
|
|
5
5
|
import { DurableAlarmService } from "./alarm.ts";
|
|
@@ -42,10 +42,12 @@ export const cloudflareWakeSchedulerLayer: Layer.Layer<
|
|
|
42
42
|
const identity = yield* ConversationObjectIdentity;
|
|
43
43
|
const { namespace } = yield* ConversationObjectNamespace;
|
|
44
44
|
const hints = yield* PubSub.sliding<ConversationId>(WAKE_BUFFER_CAPACITY);
|
|
45
|
+
const progress = yield* makeWakeSubscriptionHub;
|
|
45
46
|
yield* Effect.addFinalizer(() => PubSub.shutdown(hints));
|
|
46
47
|
|
|
47
48
|
const notifyLocal = (conversationId: ConversationId) =>
|
|
48
|
-
|
|
49
|
+
progress.notify(conversationId).pipe(
|
|
50
|
+
Effect.andThen(PubSub.publish(hints, conversationId)),
|
|
49
51
|
Effect.andThen(alarm.scheduleNow),
|
|
50
52
|
Effect.catch((error) =>
|
|
51
53
|
// `notify` never fails by contract; a failed alarm write degrades to "hint lost"
|
|
@@ -79,6 +81,7 @@ export const cloudflareWakeSchedulerLayer: Layer.Layer<
|
|
|
79
81
|
conversationId === identity.conversationId
|
|
80
82
|
? notifyLocal(conversationId)
|
|
81
83
|
: notifyRemote(conversationId),
|
|
84
|
+
subscribe: progress.subscribe,
|
|
82
85
|
wakes: Stream.fromPubSub(hints),
|
|
83
86
|
});
|
|
84
87
|
}),
|