tardie 0.22.2 → 0.22.3-rc.265
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tardie",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.3-rc.265",
|
|
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": "c77212bc7abbcc6391e5c56d162d2f27830f71c8"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"src",
|
|
@@ -256,7 +256,7 @@ const childClaimOf = (
|
|
|
256
256
|
...(recorded.placement === undefined ? {} : { placement: recorded.placement })
|
|
257
257
|
}
|
|
258
258
|
const target = recorded?.address ?? (yield* allocateChildThread({
|
|
259
|
-
parent: source, child: childKeyOf(JSON.stringify([parentRunId, callId])
|
|
259
|
+
parent: source, child: childKeyOf("unnamed"), key: JSON.stringify([parentRunId, callId])
|
|
260
260
|
}))
|
|
261
261
|
// clash rejects a derived address already claimed by another recorded child (agents.test.ts, "a derived address that names another child dies rather than delivering").
|
|
262
262
|
if (recorded === undefined) {
|
package/src/bun/host.ts
CHANGED
|
@@ -295,7 +295,7 @@ export const createBunHost = async <R = never>(options: BunHostOptions<R>): Prom
|
|
|
295
295
|
`.pipe(
|
|
296
296
|
Effect.map((rows) => ({
|
|
297
297
|
cursor: Number(rows.at(-1)?.seq ?? 0),
|
|
298
|
-
threads: actorThreadsOf(rows.map((row) => JSON.parse(row.event) as Event))
|
|
298
|
+
threads: actorThreadsOf(rows.map((row) => JSON.parse(row.event) as Event))
|
|
299
299
|
})),
|
|
300
300
|
Effect.orDie
|
|
301
301
|
))
|
|
@@ -342,7 +342,7 @@ export const createBunHost = async <R = never>(options: BunHostOptions<R>): Prom
|
|
|
342
342
|
}),
|
|
343
343
|
at
|
|
344
344
|
} satisfies ThreadRequested)
|
|
345
|
-
await appendActorEvent({ type: "ThreadRegistered", thread, at } satisfies ThreadRegistered)
|
|
345
|
+
await appendActorEvent({ type: "ThreadRegistered", thread, ...(lineage?.placement === undefined ? {} : { placement: lineage.placement }), at } satisfies ThreadRegistered)
|
|
346
346
|
}
|
|
347
347
|
const threads = (): Promise<ReadonlyArray<string>> => directoryRuntime.runPromise(
|
|
348
348
|
directorySql<{ thread: string }>`SELECT thread FROM thread_directory ORDER BY thread`.pipe(Effect.map((rows) => rows.map((row) => row.thread)), Effect.orDie)
|
package/src/cloudflare/actor.ts
CHANGED
|
@@ -44,17 +44,18 @@ const actorSupervisorOf = (
|
|
|
44
44
|
key: registeredKeyOf(event.thread),
|
|
45
45
|
input: event,
|
|
46
46
|
act: (request) => Effect.gen(function* () {
|
|
47
|
-
yield* Effect.promise(async () => {
|
|
47
|
+
const registration = yield* Effect.promise(async () => {
|
|
48
48
|
const stub = env.THREADS.getByName(threadObjectNameOf(identity.actor, identity.instance, request.thread))
|
|
49
49
|
if (request.parentThread === undefined) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
} else {
|
|
54
|
-
await stub.commitCreation()
|
|
50
|
+
await stub.init(identity.actor, identity.instance, request.thread)
|
|
51
|
+
await stub.initializeRoot()
|
|
52
|
+
return {}
|
|
55
53
|
}
|
|
54
|
+
const created = await stub.commitCreation()
|
|
55
|
+
return created === undefined ? undefined : created.placement === undefined ? {} : { placement: created.placement }
|
|
56
56
|
})
|
|
57
|
-
|
|
57
|
+
if (registration === undefined) return []
|
|
58
|
+
return [{ type: "ThreadRegistered", thread: request.thread, ...registration, at: yield* Clock.currentTimeMillis }]
|
|
58
59
|
})
|
|
59
60
|
})]
|
|
60
61
|
})
|
|
@@ -220,12 +221,15 @@ export class ActorDO extends DurableObject<Env> {
|
|
|
220
221
|
WHERE json_extract(event, '$.type') = 'ThreadRequested' AND json_extract(event, '$.thread') = ${target.thread}`.pipe(
|
|
221
222
|
Effect.map((rows) => rows.length > 0 && (!existingRoot || rows.some((row) => row.parent !== null))), Effect.orDie
|
|
222
223
|
))
|
|
223
|
-
|
|
224
|
+
const target = await Effect.runPromise(allocateThread(request).pipe(Effect.provideService(
|
|
224
225
|
ThreadAllocator, mountedActor?.threadAllocator ?? registeredThreadAllocator({
|
|
225
226
|
get: (key) => Effect.promise(() => this.database.runPromise(store.get(key))),
|
|
226
227
|
claim: (key, target, existingRoot, request) => Effect.promise(() => this.database.runPromise(store.claim(key, target, existingRoot, request)))
|
|
227
228
|
}, mountedActor?.allocation)
|
|
228
229
|
)))
|
|
230
|
+
const at = armAt(await this.ctx.storage.getAlarm(), Date.now(), this.alarmPolicy.recoveryDelayMillis)
|
|
231
|
+
if (at !== null) await this.ctx.storage.setAlarm(at)
|
|
232
|
+
return target
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
async initializeRootThread(thread: string): Promise<void> {
|
|
@@ -261,7 +265,7 @@ export class ActorDO extends DurableObject<Env> {
|
|
|
261
265
|
if (existing !== undefined && (
|
|
262
266
|
existing.parentThread !== lineage.parent.thread ||
|
|
263
267
|
Number(existing.depth) !== lineage.depth ||
|
|
264
|
-
(existing.state
|
|
268
|
+
((existing.state === "registered" || existing.placement !== undefined) && (existing.placement ?? null) !== placement)
|
|
265
269
|
)) {
|
|
266
270
|
throw new Error("a child thread already has different lineage")
|
|
267
271
|
}
|
package/src/cloudflare/thread.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { threadCreatedOf, type ThreadCreated } from "tardie/core/interaction/relations"
|
|
1
2
|
import { cloudflareRpcTransport } from "./transport/rpc"
|
|
2
3
|
import { DurableObject } from "cloudflare:workers"
|
|
3
4
|
import { Effect, Layer, Schema } from "effect"
|
|
@@ -313,12 +314,16 @@ export class ThreadDO extends DurableObject<Env> {
|
|
|
313
314
|
await this.ctx.storage.sync()
|
|
314
315
|
}
|
|
315
316
|
|
|
316
|
-
async commitCreation(): Promise<
|
|
317
|
+
async commitCreation(): Promise<ThreadCreated | undefined> {
|
|
318
|
+
if (!this.initialized()) return undefined
|
|
317
319
|
const host = await this.host()
|
|
320
|
+
const created = threadCreatedOf(await host.read())
|
|
321
|
+
if (created === undefined) return undefined
|
|
318
322
|
await this.arm()
|
|
319
323
|
await this.commitTurn()
|
|
320
324
|
host.publishStaged()
|
|
321
325
|
this.kick(host)
|
|
326
|
+
return created
|
|
322
327
|
}
|
|
323
328
|
|
|
324
329
|
async deliver(envelope: ActorEnvelope): Promise<void> {
|
package/src/core/actor/events.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ChildPlacement } from "../interaction/relations"
|
|
|
3
3
|
|
|
4
4
|
export interface ThreadRequested extends Event {
|
|
5
5
|
readonly type: "ThreadRequested"
|
|
6
|
+
readonly allocationKey?: string
|
|
6
7
|
readonly thread: string
|
|
7
8
|
readonly parentThread?: string
|
|
8
9
|
readonly depth: number
|
|
@@ -10,22 +11,14 @@ export interface ThreadRequested extends Event {
|
|
|
10
11
|
readonly at: number
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export interface ThreadAllocated extends Event {
|
|
14
|
-
readonly type: "ThreadAllocated"
|
|
15
|
-
readonly thread: string
|
|
16
|
-
readonly allocationKey: string
|
|
17
|
-
readonly parentThread?: string
|
|
18
|
-
readonly depth: number
|
|
19
|
-
readonly at: number
|
|
20
|
-
}
|
|
21
|
-
|
|
22
14
|
export interface ThreadRegistered extends Event {
|
|
23
15
|
readonly type: "ThreadRegistered"
|
|
16
|
+
readonly placement?: ChildPlacement
|
|
24
17
|
readonly thread: string
|
|
25
18
|
readonly at: number
|
|
26
19
|
}
|
|
27
20
|
|
|
28
|
-
export type ActorEvent =
|
|
21
|
+
export type ActorEvent = ThreadRequested | ThreadRegistered
|
|
29
22
|
|
|
30
23
|
export interface ActorThreadRecord {
|
|
31
24
|
readonly allocationKey?: string
|
|
@@ -33,37 +26,28 @@ export interface ActorThreadRecord {
|
|
|
33
26
|
readonly parentThread?: string
|
|
34
27
|
readonly depth: number
|
|
35
28
|
readonly placement?: ChildPlacement
|
|
36
|
-
readonly state: "
|
|
29
|
+
readonly state: "requested" | "registered"
|
|
37
30
|
}
|
|
38
31
|
|
|
39
32
|
export const actorEventKeyOf = (event: Event): string | undefined => {
|
|
40
|
-
if (event.type === "ThreadAllocated" && typeof event.thread === "string") return `thread:allocated:${event.thread}`
|
|
41
33
|
if (event.type === "ThreadRequested" && typeof event.thread === "string") return `thread:requested:${event.thread}`
|
|
42
34
|
if (event.type === "ThreadRegistered" && typeof event.thread === "string") return `thread:registered:${event.thread}`
|
|
43
35
|
return undefined
|
|
44
36
|
}
|
|
45
37
|
|
|
46
38
|
export const actorEventsOf = (events: ReadonlyArray<Event>): ReadonlyArray<ActorEvent> =>
|
|
47
|
-
events.
|
|
48
|
-
if (typeof event.thread !== "string") return
|
|
49
|
-
return event.type === "
|
|
39
|
+
events.flatMap((event): ReadonlyArray<ActorEvent> => {
|
|
40
|
+
if (typeof event.thread !== "string") return []
|
|
41
|
+
return event.type === "ThreadRequested" || event.type === "ThreadRegistered" ? [event as ActorEvent] : []
|
|
50
42
|
})
|
|
51
43
|
|
|
52
44
|
export const actorThreadsOf = (events: ReadonlyArray<Event>): ReadonlyArray<ActorThreadRecord> => {
|
|
53
45
|
const entries = new Map<string, ActorThreadRecord>()
|
|
54
46
|
for (const event of actorEventsOf(events)) {
|
|
55
47
|
const current = entries.get(event.thread)
|
|
56
|
-
if (event.type === "ThreadAllocated") {
|
|
57
|
-
entries.set(event.thread, current === undefined ? {
|
|
58
|
-
thread: event.thread, allocationKey: event.allocationKey, depth: event.depth,
|
|
59
|
-
...(event.parentThread === undefined ? {} : { parentThread: event.parentThread }),
|
|
60
|
-
state: "allocated"
|
|
61
|
-
} : { ...current, allocationKey: event.allocationKey })
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
48
|
if (event.type === "ThreadRequested") {
|
|
65
49
|
entries.set(event.thread, {
|
|
66
|
-
...(
|
|
50
|
+
...(event.allocationKey === undefined ? {} : { allocationKey: event.allocationKey }),
|
|
67
51
|
thread: event.thread,
|
|
68
52
|
...(event.parentThread === undefined ? {} : { parentThread: event.parentThread }),
|
|
69
53
|
depth: event.depth,
|
|
@@ -73,7 +57,7 @@ export const actorThreadsOf = (events: ReadonlyArray<Event>): ReadonlyArray<Acto
|
|
|
73
57
|
continue
|
|
74
58
|
}
|
|
75
59
|
if (current === undefined) throw new Error(`thread ${JSON.stringify(event.thread)} has no request`)
|
|
76
|
-
entries.set(event.thread, { ...current, state: "registered" })
|
|
60
|
+
entries.set(event.thread, { ...current, ...(event.placement === undefined ? {} : { placement: event.placement }), state: "registered" })
|
|
77
61
|
}
|
|
78
62
|
return [...entries.values()]
|
|
79
63
|
}
|
|
@@ -12,7 +12,7 @@ export const sqlThreadDirectory = (
|
|
|
12
12
|
occupied: (target: ThreadCoordinate, existingRoot: boolean) => Effect.Effect<boolean>
|
|
13
13
|
): ThreadAllocationStore => {
|
|
14
14
|
const get = (key: string) => sql<{ thread: string }>`SELECT json_extract(event, '$.thread') AS thread
|
|
15
|
-
FROM ${sql(table)} WHERE json_extract(event, '$.type') = '
|
|
15
|
+
FROM ${sql(table)} WHERE json_extract(event, '$.type') = 'ThreadRequested'
|
|
16
16
|
AND json_extract(event, '$.allocationKey') = ${key}`.pipe(
|
|
17
17
|
Effect.map((rows) => rows[0]?.thread), Effect.orDie
|
|
18
18
|
)
|
package/src/host/allocation.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Clock, Effect, Schema } from "effect"
|
|
2
2
|
import { ThreadCoordinate, threadIdOf } from "tardie/core/actor/coordinate"
|
|
3
3
|
import { allocateThread, ThreadAllocator, type ThreadAllocation } from "tardie/core/actor/allocation"
|
|
4
|
-
import { actorThreadsOf, type
|
|
4
|
+
import { actorThreadsOf, type ThreadRequested } from "tardie/core/actor/events"
|
|
5
5
|
import type { Event } from "tardie/core/event"
|
|
6
6
|
|
|
7
7
|
// instanceThreadAllocator rejects assignments outside the owning actor instance.
|
|
@@ -68,16 +68,17 @@ export interface ThreadAllocationStore {
|
|
|
68
68
|
// threadAllocationRecord keeps allocation identity in the actor's thread record (allocation.test.ts).
|
|
69
69
|
export const threadAllocationRecord = (
|
|
70
70
|
events: ReadonlyArray<Event>, request: ThreadAllocation, target: ThreadCoordinate, existingRoot: boolean, at: number
|
|
71
|
-
): { readonly thread: string; readonly event?:
|
|
71
|
+
): { readonly thread: string; readonly event?: ThreadRequested } | undefined => {
|
|
72
72
|
const key = threadAllocationKey(request)
|
|
73
73
|
const records = actorThreadsOf(events)
|
|
74
74
|
const assigned = records.find((record) => record.allocationKey === key)
|
|
75
75
|
if (assigned !== undefined) return { thread: assigned.thread }
|
|
76
76
|
const current = records.find((record) => record.thread === target.thread)
|
|
77
77
|
if (current !== undefined && (current.allocationKey !== undefined || !existingRoot || current.parentThread !== undefined)) return undefined
|
|
78
|
+
if (current !== undefined) return { thread: current.thread }
|
|
78
79
|
const parent = request.kind === "child" ? request.parent.thread : undefined
|
|
79
80
|
return { thread: target.thread, event: {
|
|
80
|
-
type: "
|
|
81
|
+
type: "ThreadRequested", thread: target.thread, allocationKey: key,
|
|
81
82
|
...(parent === undefined ? {} : { parentThread: parent }),
|
|
82
83
|
depth: parent === undefined ? 0 : (records.find((record) => record.thread === parent)?.depth ?? 0) + 1,
|
|
83
84
|
at
|
|
@@ -103,13 +104,19 @@ export const registeredThreadAllocator = (
|
|
|
103
104
|
const key = threadAllocationKey(request)
|
|
104
105
|
const recorded = yield* store.get(key)
|
|
105
106
|
if (recorded !== undefined) return { ...parent, thread: recorded }
|
|
107
|
+
if (request.key === undefined) {
|
|
108
|
+
const name = threadIdOf(request.kind === "root" ? parent.thread : request.child)
|
|
109
|
+
const thread = request.kind === "child" && name === parent.thread ? undefined
|
|
110
|
+
: yield* store.claim(key, { ...parent, thread: name }, request.kind === "root", request)
|
|
111
|
+
if (thread !== undefined) return { ...parent, thread }
|
|
112
|
+
return yield* Effect.die(new Error(`thread name ${JSON.stringify(name)} is already taken in actor instance ${JSON.stringify([parent.actor, parent.instance])}`))
|
|
113
|
+
}
|
|
106
114
|
const attempts = policy.maxAttempts ?? DEFAULT_THREAD_ALLOCATION_ATTEMPTS
|
|
107
115
|
if (!Number.isSafeInteger(attempts) || attempts < 1) throw new Error("allocation attempts must be a positive integer")
|
|
108
|
-
const namedRoot = request.kind === "root" && request.key === undefined
|
|
109
116
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
110
|
-
const candidate = threadIdOf(
|
|
117
|
+
const candidate = threadIdOf((policy.generate ?? (() => threadSlug(policy)))())
|
|
111
118
|
if (request.kind === "child" && candidate === parent.thread) continue
|
|
112
|
-
const thread = yield* store.claim(key, { ...parent, thread: candidate },
|
|
119
|
+
const thread = yield* store.claim(key, { ...parent, thread: candidate }, false, request)
|
|
113
120
|
if (thread !== undefined) return { ...parent, thread }
|
|
114
121
|
}
|
|
115
122
|
return yield* Effect.die(new Error(`thread allocation exhausted ${attempts} collision attempts`))
|