@yolk-sdk/harness 0.1.0-canary.77

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