@yolk-sdk/harness 0.1.0-canary.100

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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +206 -0
  3. package/dist/coordinator.d.mts +72 -0
  4. package/dist/coordinator.d.mts.map +1 -0
  5. package/dist/coordinator.mjs +154 -0
  6. package/dist/coordinator.mjs.map +1 -0
  7. package/dist/driver/durable-object.d.mts +20 -0
  8. package/dist/driver/durable-object.d.mts.map +1 -0
  9. package/dist/driver/durable-object.mjs +12 -0
  10. package/dist/driver/durable-object.mjs.map +1 -0
  11. package/dist/driver/memory.d.mts +21 -0
  12. package/dist/driver/memory.d.mts.map +1 -0
  13. package/dist/driver/memory.mjs +15 -0
  14. package/dist/driver/memory.mjs.map +1 -0
  15. package/dist/driver.d.mts +126 -0
  16. package/dist/driver.d.mts.map +1 -0
  17. package/dist/driver.mjs +96 -0
  18. package/dist/driver.mjs.map +1 -0
  19. package/dist/inbox.d.mts +368 -0
  20. package/dist/inbox.d.mts.map +1 -0
  21. package/dist/inbox.mjs +235 -0
  22. package/dist/inbox.mjs.map +1 -0
  23. package/dist/index.d.mts +1 -0
  24. package/dist/index.mjs +1 -0
  25. package/dist/outcome-constructors-internal.d.mts +403 -0
  26. package/dist/outcome-constructors-internal.d.mts.map +1 -0
  27. package/dist/outcome-constructors-internal.mjs +11 -0
  28. package/dist/outcome-constructors-internal.mjs.map +1 -0
  29. package/dist/outcome.d.mts +361 -0
  30. package/dist/outcome.d.mts.map +1 -0
  31. package/dist/outcome.mjs +136 -0
  32. package/dist/outcome.mjs.map +1 -0
  33. package/dist/store.d.mts +39 -0
  34. package/dist/store.d.mts.map +1 -0
  35. package/dist/store.mjs +98 -0
  36. package/dist/store.mjs.map +1 -0
  37. package/package.json +90 -0
  38. package/src/coordinator.ts +355 -0
  39. package/src/driver/durable-object.ts +28 -0
  40. package/src/driver/memory.ts +39 -0
  41. package/src/driver.ts +270 -0
  42. package/src/inbox.ts +528 -0
  43. package/src/index.ts +2 -0
  44. package/src/outcome-constructors-internal.ts +18 -0
  45. package/src/outcome.ts +377 -0
  46. package/src/store.ts +161 -0
package/src/inbox.ts ADDED
@@ -0,0 +1,528 @@
1
+ import { Context, Data, Effect, Layer, Predicate, Ref, Semaphore } from 'effect'
2
+ import * as Schema from 'effect/Schema'
3
+ import type { CapturedRun, Promotable } from './coordinator.ts'
4
+
5
+ /** Nominal identity only. All strings stay admissible so stale/empty/foreign
6
+ * tokens retain the inbox's existing no-op decisions rather than parse failures. */
7
+ export const DrainToken = Schema.String.pipe(Schema.brand('DrainToken'))
8
+
9
+ export type DrainToken = typeof DrainToken.Type
10
+
11
+ /** Nominal identity only; freshness and instance ownership are checked by Inbox. */
12
+ export const ParkGeneration = Schema.String.pipe(Schema.brand('ParkGeneration'))
13
+
14
+ export type ParkGeneration = typeof ParkGeneration.Type
15
+
16
+ export type InboxKind = 'input' | 'hitl'
17
+
18
+ export type InboxItem = {
19
+ readonly id: string
20
+ readonly runId: string
21
+ readonly delivery: Promotable | 'queue'
22
+ readonly kind: InboxKind
23
+ }
24
+
25
+ export type ParkedResponse = {
26
+ readonly itemId: string
27
+ readonly requestId: string
28
+ }
29
+
30
+ export type ParkedState = {
31
+ readonly generation: ParkGeneration
32
+ readonly requestIds: ReadonlyArray<string>
33
+ readonly responses: ReadonlyArray<ParkedResponse>
34
+ readonly ready: boolean
35
+ }
36
+
37
+ export type HitlAdmission = {
38
+ readonly itemId: string
39
+ readonly requestId: string
40
+ readonly generation: ParkGeneration
41
+ }
42
+
43
+ export type HitlDecision =
44
+ | { readonly _tag: 'Accepted' }
45
+ | { readonly _tag: 'Ready' }
46
+ | { readonly _tag: 'Duplicate' }
47
+ | { readonly _tag: 'Stale' }
48
+ | { readonly _tag: 'UnknownRequest' }
49
+ | { readonly _tag: 'NotParked' }
50
+
51
+ export type PauseDecision =
52
+ | { readonly _tag: 'Parked'; readonly generation: ParkGeneration }
53
+ | { readonly _tag: 'Stale' }
54
+
55
+ export type RecoveryAttempt =
56
+ | { readonly _tag: 'Skip' }
57
+ | { readonly _tag: 'Exhausted' }
58
+ | { readonly _tag: 'Resume' }
59
+
60
+ export type RecoveryAdmission =
61
+ | { readonly _tag: 'Skip' }
62
+ | { readonly _tag: 'Exhausted' }
63
+ | { readonly _tag: 'Resumed' }
64
+
65
+ export type DrainBegin =
66
+ | { readonly _tag: 'Skip' }
67
+ | {
68
+ readonly _tag: 'Run'
69
+ readonly drainToken: DrainToken
70
+ readonly readyResponses: ReadonlyArray<ParkedResponse>
71
+ }
72
+
73
+ export const HitlDecision = Data.taggedEnum<HitlDecision>()
74
+
75
+ export const PauseDecision = Data.taggedEnum<PauseDecision>()
76
+
77
+ export const RecoveryAttempt = Data.taggedEnum<RecoveryAttempt>()
78
+
79
+ export const RecoveryAdmission = Data.taggedEnum<RecoveryAdmission>()
80
+
81
+ export const DrainBegin = Data.taggedEnum<DrainBegin>()
82
+
83
+ export type InboxApi = {
84
+ readonly enqueue: (item: InboxItem) => Effect.Effect<void>
85
+ readonly takePromotable: (
86
+ runId: string,
87
+ scope: Promotable,
88
+ drainToken: DrainToken
89
+ ) => Effect.Effect<InboxItem | undefined>
90
+ readonly pending: (runId: string) => Effect.Effect<ReadonlyArray<InboxItem>>
91
+ readonly parked: (runId: string) => Effect.Effect<ParkedState | undefined>
92
+ readonly park: (
93
+ runId: string,
94
+ requestIds: ReadonlyArray<string>,
95
+ drainToken: DrainToken
96
+ ) => Effect.Effect<PauseDecision>
97
+ readonly acceptHitl: (
98
+ runId: string,
99
+ admission: HitlAdmission,
100
+ onReady: Effect.Effect<void>
101
+ ) => Effect.Effect<HitlDecision>
102
+ readonly clearPark: (runId: string, generation: ParkGeneration) => Effect.Effect<boolean>
103
+ readonly beginDrain: (runId: string, scope: Promotable) => Effect.Effect<DrainBegin>
104
+ readonly endDrain: (
105
+ runId: string,
106
+ drainToken: DrainToken,
107
+ acknowledged: boolean
108
+ ) => Effect.Effect<void>
109
+ readonly invalidate: (
110
+ runId: string,
111
+ interrupt: Effect.Effect<boolean>,
112
+ releaseIdleClaim: Effect.Effect<void>
113
+ ) => Effect.Effect<{ readonly hadPark: boolean; readonly interrupted: boolean }>
114
+ readonly enqueueAndWake: (item: InboxItem, wake: Effect.Effect<void>) => Effect.Effect<void>
115
+ readonly wakeIfUnblocked: (
116
+ runId: string,
117
+ scope: Promotable,
118
+ wake: Effect.Effect<void>
119
+ ) => Effect.Effect<boolean>
120
+ readonly startIfUnblocked: <E, R>(
121
+ runId: string,
122
+ start: Effect.Effect<CapturedRun<E>, E, R>
123
+ ) => Effect.Effect<CapturedRun<E> | undefined, E, R>
124
+ readonly admitRecovery: <E, R>(
125
+ runId: string,
126
+ attempt: Effect.Effect<RecoveryAttempt, E, R>,
127
+ wake: Effect.Effect<void>
128
+ ) => Effect.Effect<RecoveryAdmission, E, R>
129
+ }
130
+
131
+ const isPromotableAt = (item: InboxItem, scope: Promotable) => {
132
+ if (scope === 'steer') return item.delivery === 'steer'
133
+
134
+ return item.delivery === 'steer' || item.delivery === 'input' || item.delivery === 'queue'
135
+ }
136
+
137
+ type Park = {
138
+ readonly generation: ParkGeneration
139
+ readonly requestIds: ReadonlyArray<string>
140
+ readonly responses: ReadonlyArray<ParkedResponse>
141
+ readonly readyWoken: boolean
142
+ }
143
+
144
+ type RunControl = {
145
+ readonly pending: Promotable | undefined
146
+ readonly liveToken: DrainToken | undefined
147
+ readonly park: Park | undefined
148
+ readonly leasedGeneration: ParkGeneration | undefined
149
+ }
150
+
151
+ const emptyControl: RunControl = {
152
+ pending: undefined,
153
+ liveToken: undefined,
154
+ park: undefined,
155
+ leasedGeneration: undefined
156
+ }
157
+
158
+ const widerPending = (current: Promotable | undefined, next: Promotable): Promotable =>
159
+ current === 'input' || next === 'input' ? 'input' : 'steer'
160
+
161
+ const remainingPending = (pending: Promotable, scope: Promotable): Promotable | undefined =>
162
+ scope === 'steer' && pending === 'input' ? 'input' : undefined
163
+
164
+ const itemScope = (item: InboxItem): Promotable => (item.delivery === 'steer' ? 'steer' : 'input')
165
+
166
+ const parkComplete = (park: Park) =>
167
+ park.requestIds.every(requestId =>
168
+ park.responses.some(response => response.requestId === requestId)
169
+ )
170
+
171
+ const parkBlocked = (park: Park | undefined) => park !== undefined && !parkComplete(park)
172
+
173
+ const parkedState = (park: Park): ParkedState => ({
174
+ generation: park.generation,
175
+ requestIds: park.requestIds,
176
+ responses: park.responses,
177
+ ready: parkComplete(park)
178
+ })
179
+
180
+ const isPrunable = (control: RunControl) =>
181
+ control.pending === undefined &&
182
+ control.liveToken === undefined &&
183
+ control.park === undefined &&
184
+ control.leasedGeneration === undefined
185
+
186
+ export class Inbox extends Context.Service<Inbox, InboxApi>()('@yolk-sdk/harness/Inbox') {
187
+ /** Fresh queue, admission gate and HITL generations per layer acquisition. */
188
+ static layer = (): Layer.Layer<Inbox> =>
189
+ Layer.effect(
190
+ this,
191
+ Effect.gen(function* () {
192
+ const items = yield* Ref.make<ReadonlyArray<InboxItem>>([])
193
+ const controls = yield* Ref.make<ReadonlyMap<string, RunControl>>(new Map())
194
+ const gate = yield* Semaphore.make(1)
195
+ let generationSeq = 0
196
+ let drainSeq = 0
197
+
198
+ const withGate = <A, E, R>(body: Effect.Effect<A, E, R>) => gate.withPermits(1)(body)
199
+
200
+ const getControl = (runId: string) =>
201
+ Ref.get(controls).pipe(Effect.map(current => current.get(runId) ?? emptyControl))
202
+
203
+ const writeControl = (runId: string, next: RunControl) =>
204
+ Ref.update(controls, current => {
205
+ const copy = new Map(current)
206
+
207
+ if (isPrunable(next)) copy.delete(runId)
208
+ else copy.set(runId, next)
209
+
210
+ return copy
211
+ })
212
+
213
+ const dropQueued = (runId: string) =>
214
+ Ref.update(items, current => current.filter(item => item.runId !== runId))
215
+
216
+ const acceptHitlPure = (control: RunControl, admission: HitlAdmission): HitlDecision => {
217
+ const park = control.park
218
+
219
+ if (park === undefined) return HitlDecision.NotParked()
220
+
221
+ if (park.generation !== admission.generation) return HitlDecision.Stale()
222
+
223
+ if (!park.requestIds.includes(admission.requestId)) return HitlDecision.UnknownRequest()
224
+
225
+ if (
226
+ park.responses.some(
227
+ response =>
228
+ response.itemId === admission.itemId || response.requestId === admission.requestId
229
+ )
230
+ ) {
231
+ return HitlDecision.Duplicate()
232
+ }
233
+
234
+ if (park.readyWoken) return HitlDecision.Duplicate()
235
+
236
+ const responses = [
237
+ ...park.responses,
238
+ { itemId: admission.itemId, requestId: admission.requestId }
239
+ ]
240
+
241
+ return parkComplete({ ...park, responses })
242
+ ? HitlDecision.Ready()
243
+ : HitlDecision.Accepted()
244
+ }
245
+
246
+ return Inbox.of({
247
+ enqueue: item => Ref.update(items, current => [...current, item]),
248
+ takePromotable: (runId, scope, drainToken) =>
249
+ withGate(
250
+ Effect.uninterruptible(
251
+ Effect.gen(function* () {
252
+ const control = yield* getControl(runId)
253
+
254
+ if (control.liveToken === undefined || control.liveToken !== drainToken) {
255
+ return undefined
256
+ }
257
+
258
+ return yield* Ref.modify(items, current => {
259
+ const index = current.findIndex(
260
+ item => item.runId === runId && isPromotableAt(item, scope)
261
+ )
262
+
263
+ if (index < 0) return [undefined, current] as const
264
+ const taken = current[index]
265
+
266
+ return [taken, current.filter((_, itemIndex) => itemIndex !== index)] as const
267
+ })
268
+ })
269
+ )
270
+ ),
271
+ pending: runId =>
272
+ Ref.get(items).pipe(
273
+ Effect.map(current => current.filter(item => item.runId === runId))
274
+ ),
275
+ parked: runId =>
276
+ getControl(runId).pipe(
277
+ Effect.map(control =>
278
+ control.park === undefined ? undefined : parkedState(control.park)
279
+ )
280
+ ),
281
+ park: (runId, requestIds, drainToken) =>
282
+ withGate(
283
+ Effect.uninterruptible(
284
+ Effect.gen(function* () {
285
+ const control = yield* getControl(runId)
286
+
287
+ if (control.liveToken !== drainToken) return PauseDecision.Stale()
288
+ generationSeq += 1
289
+ const generation = ParkGeneration.make(String(generationSeq))
290
+ yield* writeControl(runId, {
291
+ pending: control.pending,
292
+ liveToken: control.liveToken,
293
+ leasedGeneration: control.leasedGeneration,
294
+ park: {
295
+ generation,
296
+ requestIds: [...requestIds],
297
+ responses: [],
298
+ readyWoken: false
299
+ }
300
+ })
301
+
302
+ return PauseDecision.Parked({ generation })
303
+ })
304
+ )
305
+ ),
306
+ acceptHitl: (runId, admission, onReady) =>
307
+ withGate(
308
+ Effect.uninterruptible(
309
+ Effect.gen(function* () {
310
+ const control = yield* getControl(runId)
311
+ const decision = acceptHitlPure(control, admission)
312
+
313
+ if (
314
+ !Predicate.isTagged(decision, 'Accepted') &&
315
+ !Predicate.isTagged(decision, 'Ready')
316
+ ) {
317
+ return decision
318
+ }
319
+
320
+ const park = control.park
321
+
322
+ if (park === undefined) return HitlDecision.NotParked()
323
+
324
+ const nextPark: Park = {
325
+ ...park,
326
+ responses: [
327
+ ...park.responses,
328
+ { itemId: admission.itemId, requestId: admission.requestId }
329
+ ],
330
+ readyWoken: Predicate.isTagged(decision, 'Ready')
331
+ }
332
+
333
+ yield* writeControl(runId, {
334
+ pending: Predicate.isTagged(decision, 'Ready')
335
+ ? widerPending(control.pending, 'input')
336
+ : control.pending,
337
+ liveToken: control.liveToken,
338
+ leasedGeneration: control.leasedGeneration,
339
+ park: nextPark
340
+ })
341
+
342
+ if (Predicate.isTagged(decision, 'Ready')) yield* onReady
343
+
344
+ return decision
345
+ })
346
+ )
347
+ ),
348
+ clearPark: (runId, generation) =>
349
+ withGate(
350
+ Effect.uninterruptible(
351
+ Effect.gen(function* () {
352
+ const control = yield* getControl(runId)
353
+
354
+ if (control.park === undefined || control.park.generation !== generation) {
355
+ return false
356
+ }
357
+
358
+ yield* writeControl(runId, {
359
+ pending: control.pending,
360
+ liveToken: control.liveToken,
361
+ leasedGeneration: control.leasedGeneration,
362
+ park: undefined
363
+ })
364
+
365
+ return true
366
+ })
367
+ )
368
+ ),
369
+ beginDrain: (runId, scope) =>
370
+ withGate(
371
+ Effect.uninterruptible(
372
+ Effect.gen(function* () {
373
+ const control = yield* getControl(runId)
374
+ const pending = control.pending
375
+
376
+ if (parkBlocked(control.park) || pending === undefined) {
377
+ return DrainBegin.Skip()
378
+ }
379
+
380
+ drainSeq += 1
381
+ const drainToken = DrainToken.make(`d${drainSeq}`)
382
+ const park = control.park
383
+ const ready = park !== undefined && parkComplete(park) ? park : undefined
384
+ yield* writeControl(runId, {
385
+ pending: remainingPending(pending, scope),
386
+ liveToken: drainToken,
387
+ park,
388
+ leasedGeneration: ready?.generation
389
+ })
390
+
391
+ return DrainBegin.Run({
392
+ drainToken,
393
+ readyResponses: ready === undefined ? [] : ready.responses
394
+ })
395
+ })
396
+ )
397
+ ),
398
+ endDrain: (runId, drainToken, acknowledged) =>
399
+ withGate(
400
+ Effect.uninterruptible(
401
+ Effect.gen(function* () {
402
+ const control = yield* getControl(runId)
403
+
404
+ if (control.liveToken !== drainToken) return
405
+ const leased = control.leasedGeneration
406
+ const park = control.park
407
+
408
+ const clearPark =
409
+ acknowledged &&
410
+ leased !== undefined &&
411
+ park !== undefined &&
412
+ park.generation === leased
413
+
414
+ yield* writeControl(runId, {
415
+ pending: control.pending,
416
+ liveToken: undefined,
417
+ leasedGeneration: undefined,
418
+ park: clearPark ? undefined : park
419
+ })
420
+ })
421
+ )
422
+ ),
423
+ invalidate: (runId, interrupt, releaseIdleClaim) =>
424
+ withGate(
425
+ Effect.uninterruptible(
426
+ Effect.gen(function* () {
427
+ const control = yield* getControl(runId)
428
+ const hadPark = control.park !== undefined
429
+ yield* dropQueued(runId)
430
+ yield* writeControl(runId, emptyControl)
431
+ const interrupted = yield* interrupt
432
+
433
+ if (!interrupted) yield* releaseIdleClaim
434
+
435
+ return { hadPark, interrupted }
436
+ })
437
+ )
438
+ ),
439
+ enqueueAndWake: (item, wake) =>
440
+ withGate(
441
+ Effect.uninterruptible(
442
+ Effect.gen(function* () {
443
+ yield* Ref.update(items, current => [...current, item])
444
+ const control = yield* getControl(item.runId)
445
+
446
+ if (parkBlocked(control.park)) return
447
+ yield* writeControl(item.runId, {
448
+ pending: widerPending(control.pending, itemScope(item)),
449
+ liveToken: control.liveToken,
450
+ leasedGeneration: control.leasedGeneration,
451
+ park: control.park
452
+ })
453
+ yield* wake
454
+ })
455
+ )
456
+ ),
457
+ wakeIfUnblocked: (runId, scope, wake) =>
458
+ withGate(
459
+ Effect.uninterruptible(
460
+ Effect.gen(function* () {
461
+ const control = yield* getControl(runId)
462
+
463
+ if (parkBlocked(control.park)) return false
464
+ yield* writeControl(runId, {
465
+ pending: widerPending(control.pending, scope),
466
+ liveToken: control.liveToken,
467
+ leasedGeneration: control.leasedGeneration,
468
+ park: control.park
469
+ })
470
+ yield* wake
471
+
472
+ return true
473
+ })
474
+ )
475
+ ),
476
+ startIfUnblocked: (runId, start) =>
477
+ withGate(
478
+ Effect.uninterruptible(
479
+ Effect.gen(function* () {
480
+ const control = yield* getControl(runId)
481
+
482
+ if (parkBlocked(control.park)) return undefined
483
+ const ticket = yield* start
484
+
485
+ if (Predicate.isTagged(ticket, 'Started')) {
486
+ yield* writeControl(runId, {
487
+ pending: widerPending(control.pending, 'input'),
488
+ liveToken: control.liveToken,
489
+ leasedGeneration: control.leasedGeneration,
490
+ park: control.park
491
+ })
492
+ }
493
+
494
+ return ticket
495
+ })
496
+ )
497
+ ),
498
+ admitRecovery: (runId, attempt, wake) =>
499
+ Effect.uninterruptibleMask(restore =>
500
+ restore(gate.take(1)).pipe(
501
+ Effect.flatMap(() =>
502
+ Effect.gen(function* () {
503
+ const control = yield* getControl(runId)
504
+
505
+ if (parkBlocked(control.park)) return RecoveryAdmission.Skip()
506
+ const decision = yield* attempt
507
+
508
+ if (!RecoveryAttempt.$is('Resume')(decision)) return decision
509
+ yield* writeControl(runId, {
510
+ pending: widerPending(control.pending, 'input'),
511
+ liveToken: control.liveToken,
512
+ leasedGeneration: control.leasedGeneration,
513
+ park: control.park
514
+ })
515
+ yield* wake
516
+
517
+ return RecoveryAdmission.Resumed()
518
+ }).pipe(Effect.ensuring(gate.release(1)))
519
+ )
520
+ )
521
+ )
522
+ })
523
+ })
524
+ )
525
+ }
526
+
527
+ /** Backward-compatible delegation to the owning inbox layer. */
528
+ export const makeInMemoryInboxLayer = (): Layer.Layer<Inbox> => Inbox.layer()
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Root intentionally empty. Import feature APIs from explicit subpaths.
2
+ export {}
@@ -0,0 +1,18 @@
1
+ import { Data } from 'effect'
2
+ import type { StopReceipt as StopReceiptType } from './coordinator.ts'
3
+ import type { StopDecision as StopDecisionType } from './driver.ts'
4
+ import type {
5
+ HitlMatch as HitlMatchType,
6
+ OverflowCompactionResult as OverflowCompactionResultType,
7
+ StepOutcome as StepOutcomeType
8
+ } from './outcome.ts'
9
+
10
+ export const OverflowCompactionResult = Data.taggedEnum<OverflowCompactionResultType>()
11
+
12
+ export const StepOutcome = Data.taggedEnum<StepOutcomeType>()
13
+
14
+ export const HitlMatch = Data.taggedEnum<HitlMatchType>()
15
+
16
+ export const StopReceipt = Data.taggedEnum<StopReceiptType>()
17
+
18
+ export const StopDecision = Data.taggedEnum<StopDecisionType>()