tardie 0.13.0 → 0.14.0
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/package.json +2 -2
- package/src/cloudflare/host.ts +30 -2
- package/src/cloudflare/worker.ts +88 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tardie",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Clavia, Inc.",
|
|
6
6
|
"description": "A durable agent harness. State is a pure function of the log.",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"access": "public"
|
|
17
17
|
},
|
|
18
18
|
"tardigrade": {
|
|
19
|
-
"sourceTree": "
|
|
19
|
+
"sourceTree": "97448ddae8564fb78c339ce7cc9423dfe7e18b73"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"src",
|
package/src/cloudflare/host.ts
CHANGED
|
@@ -35,6 +35,10 @@ export type CloudflareHostOptions<R> = {
|
|
|
35
35
|
readonly pick?: (dirty: ReadonlySet<string>) => string
|
|
36
36
|
readonly keyOf?: (event: Event) => string | undefined
|
|
37
37
|
} & LayersFor<R>
|
|
38
|
+
export interface CloudflareHostRouting {
|
|
39
|
+
readonly localThread: string | undefined
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
|
|
39
43
|
export interface CloudflareHost {
|
|
40
44
|
readonly read: (lane: string) => Promise<ReadonlyArray<Event>>
|
|
@@ -59,7 +63,22 @@ const laneOf = (address: string): string => {
|
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
// createCloudflareHost binds one actor graph to Effect SQL over its Durable Object storage.
|
|
62
|
-
export
|
|
66
|
+
export function createCloudflareHost<R = never>(
|
|
67
|
+
options: CloudflareHostOptions<R>
|
|
68
|
+
): Promise<CloudflareHost>
|
|
69
|
+
export function createCloudflareHost<R = never>(
|
|
70
|
+
routing: CloudflareHostRouting,
|
|
71
|
+
options: CloudflareHostOptions<R>
|
|
72
|
+
): Promise<CloudflareHost>
|
|
73
|
+
export async function createCloudflareHost<R = never>(
|
|
74
|
+
routingOrOptions: CloudflareHostRouting | CloudflareHostOptions<R>,
|
|
75
|
+
providedOptions?: CloudflareHostOptions<R>
|
|
76
|
+
): Promise<CloudflareHost> {
|
|
77
|
+
const options = "storage" in routingOrOptions ? routingOrOptions : providedOptions
|
|
78
|
+
if (options === undefined) throw new Error("Cloudflare host options are required")
|
|
79
|
+
const routing = "storage" in routingOrOptions
|
|
80
|
+
? { localThread: undefined }
|
|
81
|
+
: routingOrOptions
|
|
63
82
|
const database = ManagedRuntime.make(SqliteClient.layer({ storage: options.storage }))
|
|
64
83
|
const sql = await database.runPromise(SqliteClient.SqliteClient)
|
|
65
84
|
const workspaceRuntime = ManagedRuntime.make(layerWorkspace(sql))
|
|
@@ -133,7 +152,16 @@ export const createCloudflareHost = async <R = never>(options: CloudflareHostOpt
|
|
|
133
152
|
send: (_destination, envelope) => commitEffect(envelope.link.target, envelope.event, envelope.lineage, envelope.link, envelope.call)
|
|
134
153
|
}
|
|
135
154
|
const routes = [
|
|
136
|
-
directoryRoute(
|
|
155
|
+
directoryRoute(
|
|
156
|
+
localTransport,
|
|
157
|
+
mappedDirectory((id: ActorId) =>
|
|
158
|
+
id.actor === options.principal && (routing.localThread === undefined || id.thread === routing.localThread)
|
|
159
|
+
? id
|
|
160
|
+
: undefined
|
|
161
|
+
),
|
|
162
|
+
isActorEnvelope,
|
|
163
|
+
(envelope) => envelope.link.target
|
|
164
|
+
),
|
|
137
165
|
directoryRoute(providerTransport, mappedDirectory<ProviderEndpoint, ProviderEndpoint>((endpoint) => endpoint), isProviderEnvelope, (envelope) => envelope.link.target),
|
|
138
166
|
...(options.routes ?? [])
|
|
139
167
|
]
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -70,10 +70,15 @@ export interface Env {
|
|
|
70
70
|
|
|
71
71
|
const LANE_PREFIX = "ag."
|
|
72
72
|
const laneOf = (thread: string): string => `${LANE_PREFIX}${thread}`
|
|
73
|
+
const actorThreadOf = (thread: string): string => thread.startsWith(LANE_PREFIX) ? thread : laneOf(thread)
|
|
73
74
|
const threadOf = (lane: string): string | undefined => lane.startsWith(LANE_PREFIX) ? lane.slice(LANE_PREFIX.length) : undefined
|
|
74
75
|
|
|
75
76
|
const flattenThreads = (nodes: ReadonlyArray<ThreadNode>): ReadonlyArray<ThreadSummary> =>
|
|
76
77
|
nodes.flatMap(({ children, ...summary }) => [summary, ...flattenThreads(children)])
|
|
78
|
+
export type CloudflarePlacement = "actor" | "thread"
|
|
79
|
+
|
|
80
|
+
const objectNameOf = (actor: string, thread: string | undefined, placement: CloudflarePlacement): string =>
|
|
81
|
+
placement === "thread" && thread !== undefined ? JSON.stringify([actor, thread]) : actor
|
|
77
82
|
|
|
78
83
|
const DEFAULT_ACTOR_NAME = "default"
|
|
79
84
|
|
|
@@ -86,6 +91,7 @@ interface MountedActor {
|
|
|
86
91
|
readonly modelAdapters: ModelAdapterRegistry
|
|
87
92
|
readonly inferenceObserverFor?: (context: CloudflareWorkerLayerContext<Env>) => InferenceObserver
|
|
88
93
|
readonly layersFor?: (context: CloudflareWorkerLayerContext<Env>) => CloudflareLaneEnv<never>
|
|
94
|
+
readonly placement: CloudflarePlacement
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
let mountedActor: MountedActor | undefined
|
|
@@ -348,6 +354,7 @@ export class ActorHost extends DurableObject<Env> {
|
|
|
348
354
|
private catalogState: Promise<ModelCatalogState> | undefined
|
|
349
355
|
private driving: Promise<void> | undefined
|
|
350
356
|
private principal: string | undefined
|
|
357
|
+
private threadId: string | undefined
|
|
351
358
|
private readonly alarmPolicy: AlarmPolicy
|
|
352
359
|
private readonly sandboxCalls = new Map<
|
|
353
360
|
string,
|
|
@@ -362,19 +369,35 @@ export class ActorHost extends DurableObject<Env> {
|
|
|
362
369
|
: { recoveryDelayMillis: nonNegativeInteger(env.TARDIGRADE_ALARM_DELAY_MILLIS, 0, "TARDIGRADE_ALARM_DELAY_MILLIS") })
|
|
363
370
|
}
|
|
364
371
|
|
|
365
|
-
async init(name: string): Promise<void> {
|
|
372
|
+
async init(name: string, thread?: string): Promise<void> {
|
|
366
373
|
if (!deployed(name)) throw new Error(`actor ${JSON.stringify(name)} is not deployed`)
|
|
367
374
|
this.ctx.storage.sql.exec("INSERT OR IGNORE INTO actor_meta (key, value) VALUES ('principal', ?)", name)
|
|
368
|
-
|
|
369
|
-
|
|
375
|
+
if (thread !== undefined) {
|
|
376
|
+
this.ctx.storage.sql.exec("INSERT OR IGNORE INTO actor_meta (key, value) VALUES ('thread', ?)", thread)
|
|
377
|
+
}
|
|
378
|
+
const principal = this.meta("principal")
|
|
379
|
+
const storedThread = this.meta("thread", false)
|
|
380
|
+
if (principal !== name) throw new Error("actor definition does not match the durable host identity")
|
|
381
|
+
if (storedThread !== thread) throw new Error("actor thread does not match the durable host identity")
|
|
370
382
|
this.principal ??= principal
|
|
383
|
+
this.threadId ??= storedThread
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private meta(key: string, required = true): string | undefined {
|
|
387
|
+
const row = this.ctx.storage.sql.exec<{ value: string }>(
|
|
388
|
+
"SELECT value FROM actor_meta WHERE key = ?",
|
|
389
|
+
key
|
|
390
|
+
).toArray()[0]
|
|
391
|
+
if (row === undefined && required) throw new Error("actor host has not been initialized")
|
|
392
|
+
return row?.value
|
|
371
393
|
}
|
|
372
394
|
|
|
373
395
|
private name(): string {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
396
|
+
return this.principal ??= this.meta("principal")!
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private thread(): string | undefined {
|
|
400
|
+
return this.threadId ??= this.meta("thread", false)
|
|
378
401
|
}
|
|
379
402
|
|
|
380
403
|
async catalog(): Promise<ModelCatalogState> {
|
|
@@ -415,6 +438,11 @@ export class ActorHost extends DurableObject<Env> {
|
|
|
415
438
|
const principal = this.name()
|
|
416
439
|
const selectedAssembly = assemblyOf(principal, this.env, models, catalog)
|
|
417
440
|
if (selectedAssembly === undefined) throw new Error(`actor ${JSON.stringify(principal)} is not deployed`)
|
|
441
|
+
const placement = mountedActor?.placement ?? "actor"
|
|
442
|
+
const currentThread = this.thread()
|
|
443
|
+
if (placement === "thread" && currentThread === undefined) {
|
|
444
|
+
throw new Error("thread-scoped actor host requires a thread identity")
|
|
445
|
+
}
|
|
418
446
|
const sandboxCpuMs = optionalNonNegativeInteger(this.env.TARDIGRADE_SANDBOX_CPU_MILLIS, "TARDIGRADE_SANDBOX_CPU_MILLIS")
|
|
419
447
|
const sandboxSubRequests = optionalNonNegativeInteger(
|
|
420
448
|
this.env.TARDIGRADE_SANDBOX_SUBREQUESTS,
|
|
@@ -457,8 +485,9 @@ export class ActorHost extends DurableObject<Env> {
|
|
|
457
485
|
: envelope.event
|
|
458
486
|
return Effect.promise(async () => {
|
|
459
487
|
if (!deployed(destination.actor)) throw new Error(`actor ${JSON.stringify(destination.actor)} is not deployed`)
|
|
460
|
-
const
|
|
461
|
-
|
|
488
|
+
const targetThread = placement === "thread" ? destination.thread : undefined
|
|
489
|
+
const stub = this.env.ACTORS.getByName(objectNameOf(destination.actor, targetThread, placement))
|
|
490
|
+
await stub.init(destination.actor, targetThread)
|
|
462
491
|
await stub.deliver({ ...envelope, event })
|
|
463
492
|
})
|
|
464
493
|
})
|
|
@@ -466,11 +495,16 @@ export class ActorHost extends DurableObject<Env> {
|
|
|
466
495
|
}
|
|
467
496
|
const remoteRoute = directoryRoute(
|
|
468
497
|
remoteTransport,
|
|
469
|
-
mappedDirectory((id: ActorId) =>
|
|
498
|
+
mappedDirectory((id: ActorId) => {
|
|
499
|
+
if (placement === "actor") return id.actor === principal ? undefined : id
|
|
500
|
+
return id.actor === principal && id.thread === currentThread ? undefined : id
|
|
501
|
+
}),
|
|
470
502
|
isActorEnvelope,
|
|
471
503
|
(envelope) => envelope.link.target
|
|
472
504
|
)
|
|
473
|
-
return createCloudflareHost(
|
|
505
|
+
return createCloudflareHost(
|
|
506
|
+
{ localThread: placement === "thread" ? currentThread : undefined },
|
|
507
|
+
{
|
|
474
508
|
storage: this.ctx.storage,
|
|
475
509
|
principal,
|
|
476
510
|
actorFor: (lane) => threadOf(lane) === undefined ? undefined : selectedAssembly,
|
|
@@ -489,7 +523,8 @@ export class ActorHost extends DurableObject<Env> {
|
|
|
489
523
|
)
|
|
490
524
|
}),
|
|
491
525
|
keyOf: selectedAssembly.keyOf
|
|
492
|
-
|
|
526
|
+
}
|
|
527
|
+
)
|
|
493
528
|
}
|
|
494
529
|
|
|
495
530
|
private host(): Promise<CloudflareHost> {
|
|
@@ -559,19 +594,33 @@ export class ActorHost extends DurableObject<Env> {
|
|
|
559
594
|
}
|
|
560
595
|
|
|
561
596
|
async append(thread: string, event: Event): Promise<void> {
|
|
597
|
+
const actorThread = actorThreadOf(thread)
|
|
598
|
+
const ownedThread = this.thread()
|
|
599
|
+
if (ownedThread !== undefined && ownedThread !== actorThread) {
|
|
600
|
+
throw new Error("request thread does not match actor host identity")
|
|
601
|
+
}
|
|
562
602
|
const stamped = event.at === undefined ? { ...event, at: Date.now() } : event
|
|
563
603
|
const host = await this.host()
|
|
564
|
-
await this.accept(host, () => host.stageRoot(host.self(
|
|
604
|
+
await this.accept(host, () => host.stageRoot(host.self(actorThread), stamped))
|
|
565
605
|
}
|
|
566
606
|
|
|
567
607
|
async deliver(envelope: ActorEnvelope): Promise<void> {
|
|
568
|
-
if (envelope.link.target.actor !== this.name()) throw new Error("delivery target does not match actor
|
|
608
|
+
if (envelope.link.target.actor !== this.name()) throw new Error("delivery target does not match actor definition")
|
|
609
|
+
const ownedThread = this.thread()
|
|
610
|
+
if (ownedThread !== undefined && envelope.link.target.thread !== ownedThread) {
|
|
611
|
+
throw new Error("delivery target does not match actor thread")
|
|
612
|
+
}
|
|
569
613
|
const host = await this.host()
|
|
570
614
|
await this.accept(host, () => host.stage(envelope))
|
|
571
615
|
}
|
|
572
616
|
|
|
573
617
|
async events(thread: string): Promise<ReadonlyArray<Event>> {
|
|
574
|
-
|
|
618
|
+
const actorThread = actorThreadOf(thread)
|
|
619
|
+
const ownedThread = this.thread()
|
|
620
|
+
if (ownedThread !== undefined && ownedThread !== actorThread) {
|
|
621
|
+
throw new Error("request thread does not match actor host identity")
|
|
622
|
+
}
|
|
623
|
+
return (await this.host()).read(actorThread)
|
|
575
624
|
}
|
|
576
625
|
|
|
577
626
|
async threads(): Promise<ReadonlyArray<ThreadSummary>> {
|
|
@@ -602,11 +651,14 @@ export class ActorHost extends DurableObject<Env> {
|
|
|
602
651
|
|
|
603
652
|
const actorStub = async (
|
|
604
653
|
env: Env,
|
|
605
|
-
name: string
|
|
654
|
+
name: string,
|
|
655
|
+
thread?: string
|
|
606
656
|
): Promise<DurableObjectStub<ActorHost> | undefined> => {
|
|
607
657
|
if (!deployed(name)) return undefined
|
|
608
|
-
const
|
|
609
|
-
|
|
658
|
+
const placement = mountedActor?.placement ?? "actor"
|
|
659
|
+
const targetThread = placement === "thread" && thread !== undefined ? actorThreadOf(thread) : undefined
|
|
660
|
+
const stub = env.ACTORS.getByName(objectNameOf(name, targetThread, placement))
|
|
661
|
+
await stub.init(name, targetThread)
|
|
610
662
|
return stub
|
|
611
663
|
}
|
|
612
664
|
|
|
@@ -677,6 +729,9 @@ const protectedRoute = <E, R>(
|
|
|
677
729
|
|
|
678
730
|
const routes = [
|
|
679
731
|
HttpRouter.route("GET", "/healthz", Effect.gen(function* () {
|
|
732
|
+
if ((mountedActor?.placement ?? "actor") === "thread") {
|
|
733
|
+
return json({ status: "ready", actor: deployedActor })
|
|
734
|
+
}
|
|
680
735
|
const env = yield* WorkerEnv
|
|
681
736
|
return json(yield* Effect.promise(async () => (await actorStub(env, deployedActor))!.status()))
|
|
682
737
|
})),
|
|
@@ -756,7 +811,7 @@ const routes = [
|
|
|
756
811
|
const at = yield* Clock.currentTimeMillis
|
|
757
812
|
const decoded = methodEventOf(method, { id: call, input, at })
|
|
758
813
|
if ("error" in decoded) return json({ error: decoded.error }, 400)
|
|
759
|
-
const stub = yield* Effect.promise(() => actorStub(env, actor))
|
|
814
|
+
const stub = yield* Effect.promise(() => actorStub(env, actor, thread))
|
|
760
815
|
if (stub === undefined) return json({ error: "actor is not deployed" }, 503)
|
|
761
816
|
yield* Effect.promise(() => stub.append(thread, decoded.event))
|
|
762
817
|
return json({ thread, method: methodName, call }, 202)
|
|
@@ -771,7 +826,7 @@ const routes = [
|
|
|
771
826
|
const call = decodeURIComponent(params.call ?? "")
|
|
772
827
|
const method = methodsOf(actor)?.[methodName]
|
|
773
828
|
if (method === undefined) return json({ error: "unknown method" }, 404)
|
|
774
|
-
const stub = yield* Effect.promise(() => actorStub(env, actor))
|
|
829
|
+
const stub = yield* Effect.promise(() => actorStub(env, actor, thread))
|
|
775
830
|
if (stub === undefined) return json({ error: "actor is not deployed" }, 503)
|
|
776
831
|
const events = yield* Effect.promise(() => stub.events(thread)).pipe(
|
|
777
832
|
Effect.map((value) => value as ReadonlyArray<Event>)
|
|
@@ -782,6 +837,9 @@ const routes = [
|
|
|
782
837
|
)),
|
|
783
838
|
HttpRouter.route("GET", "/v1/threads", protectedRoute((_request, env) =>
|
|
784
839
|
Effect.gen(function* () {
|
|
840
|
+
if ((mountedActor?.placement ?? "actor") === "thread") {
|
|
841
|
+
return json({ error: "thread-scoped workers do not support actor-wide thread listing" }, 400)
|
|
842
|
+
}
|
|
785
843
|
const stub = yield* Effect.promise(() => actorStub(env, deployedActor))
|
|
786
844
|
if (stub === undefined) return json({ error: "actor is not deployed" }, 503)
|
|
787
845
|
return json(yield* Effect.promise(() => stub.threads()))
|
|
@@ -792,7 +850,7 @@ const routes = [
|
|
|
792
850
|
const params = yield* HttpRouter.params
|
|
793
851
|
const actor = deployedActor
|
|
794
852
|
const thread = decodeURIComponent(params.thread ?? "")
|
|
795
|
-
const stub = yield* Effect.promise(() => actorStub(env, actor))
|
|
853
|
+
const stub = yield* Effect.promise(() => actorStub(env, actor, thread))
|
|
796
854
|
if (stub === undefined) return json({ error: "actor is not deployed" }, 503)
|
|
797
855
|
const event = (yield* request.json.pipe(Effect.orElseSucceed(() => undefined))) as Event | undefined
|
|
798
856
|
if (typeof event !== "object" || event === null || typeof event.type !== "string" || event.type === "") {
|
|
@@ -807,7 +865,7 @@ const routes = [
|
|
|
807
865
|
const params = yield* HttpRouter.params
|
|
808
866
|
const actor = deployedActor
|
|
809
867
|
const thread = decodeURIComponent(params.thread ?? "")
|
|
810
|
-
const stub = yield* Effect.promise(() => actorStub(env, actor))
|
|
868
|
+
const stub = yield* Effect.promise(() => actorStub(env, actor, thread))
|
|
811
869
|
if (stub === undefined) return json({ error: "actor is not deployed" }, 503)
|
|
812
870
|
const url = new URL(request.url, "http://worker")
|
|
813
871
|
const after = Number(url.searchParams.get("after") ?? 0)
|
|
@@ -856,10 +914,14 @@ export interface CloudflareWorkerLayerContext<WorkerEnv extends Env = Env> {
|
|
|
856
914
|
type CloudflareWorkerLayersFor<R, WorkerEnv extends Env> = (
|
|
857
915
|
context: CloudflareWorkerLayerContext<WorkerEnv>
|
|
858
916
|
) => CloudflareLaneEnv<CloudflareApplicationRequirements<R>>
|
|
917
|
+
interface CloudflareWorkerPlacementOptions {
|
|
918
|
+
readonly placement?: CloudflarePlacement
|
|
919
|
+
}
|
|
920
|
+
|
|
859
921
|
|
|
860
922
|
// CloudflareWorkerOptions supplies every actor requirement the Worker does not bind itself.
|
|
861
923
|
export type CloudflareWorkerOptions<R, WorkerEnv extends Env = Env> =
|
|
862
|
-
[CloudflareApplicationRequirements<R>] extends [never]
|
|
924
|
+
([CloudflareApplicationRequirements<R>] extends [never]
|
|
863
925
|
? {
|
|
864
926
|
readonly layersFor?: CloudflareWorkerLayersFor<R, WorkerEnv>
|
|
865
927
|
readonly modelAdapters?: ModelAdapterRegistry
|
|
@@ -869,7 +931,8 @@ export type CloudflareWorkerOptions<R, WorkerEnv extends Env = Env> =
|
|
|
869
931
|
readonly layersFor: CloudflareWorkerLayersFor<R, WorkerEnv>
|
|
870
932
|
readonly modelAdapters?: ModelAdapterRegistry
|
|
871
933
|
readonly inferenceObserverFor?: (context: CloudflareWorkerLayerContext<WorkerEnv>) => InferenceObserver
|
|
872
|
-
}
|
|
934
|
+
}) &
|
|
935
|
+
CloudflareWorkerPlacementOptions
|
|
873
936
|
|
|
874
937
|
type CloudflareWorkerArguments<R, WorkerEnv extends Env> =
|
|
875
938
|
[CloudflareApplicationRequirements<R>] extends [never]
|
|
@@ -890,6 +953,7 @@ export const cloudflareWorker = <
|
|
|
890
953
|
actor: definition as unknown as DefaultAssembly,
|
|
891
954
|
methods: definition.methods,
|
|
892
955
|
modelAdapters: options?.modelAdapters ?? modelAdapters(),
|
|
956
|
+
placement: options?.placement ?? "actor",
|
|
893
957
|
...(options?.inferenceObserverFor === undefined ? {} : {
|
|
894
958
|
inferenceObserverFor: options.inferenceObserverFor as unknown as (context: CloudflareWorkerLayerContext<Env>) => InferenceObserver
|
|
895
959
|
}),
|