@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/driver.ts ADDED
@@ -0,0 +1,270 @@
1
+ import { Context, Data, Effect, Exit, Layer, Predicate, Semaphore } from 'effect'
2
+ import { StopDecision as stopDecision } from './outcome-constructors-internal.ts'
3
+ import { RunCoordinator, type InterruptReason, type Promotable } from './coordinator.ts'
4
+ import {
5
+ DrainBegin,
6
+ Inbox,
7
+ RecoveryAttempt,
8
+ type DrainToken,
9
+ type HitlAdmission,
10
+ type HitlDecision,
11
+ type InboxItem,
12
+ type ParkedResponse,
13
+ type PauseDecision
14
+ } from './inbox.ts'
15
+ import { RunStore } from './store.ts'
16
+
17
+ export type { InterruptReason } from './coordinator.ts'
18
+
19
+ export const defaultMaxResumeAttempts = 10
20
+
21
+ export class InvalidMaxResumeAttempts extends Data.TaggedError('InvalidMaxResumeAttempts')<{
22
+ readonly maxResumeAttempts: number
23
+ }> {}
24
+
25
+ const isValidMaxResumeAttempts = (value: number) => Number.isSafeInteger(value) && value >= 0
26
+
27
+ export type ResumeSuspendedResult = {
28
+ readonly resumed: ReadonlyArray<string>
29
+ readonly exhausted: ReadonlyArray<string>
30
+ }
31
+
32
+ export type DrainContext = {
33
+ readonly drainToken: DrainToken
34
+ readonly readyResponses: ReadonlyArray<ParkedResponse>
35
+ }
36
+
37
+ export type Drain = (
38
+ runId: string,
39
+ force: boolean,
40
+ scope: Promotable,
41
+ context: DrainContext
42
+ ) => Effect.Effect<void, never, Inbox | RunStore>
43
+
44
+ export type DriverLayerOptions = {
45
+ readonly drain?: Drain
46
+ readonly maxResumeAttempts?: number
47
+ }
48
+
49
+ export type StopDecision =
50
+ | { readonly _tag: 'Interrupted' }
51
+ | { readonly _tag: 'ParkCleared' }
52
+ | { readonly _tag: 'Idle' }
53
+
54
+ export const StopDecision = stopDecision
55
+
56
+ export type DriverApi = {
57
+ readonly active: Effect.Effect<ReadonlySet<string>>
58
+ readonly isActive: (runId: string) => Effect.Effect<boolean>
59
+ readonly run: (runId: string) => Effect.Effect<void>
60
+ readonly wake: (runId: string, scope?: Promotable) => Effect.Effect<void>
61
+ readonly interrupt: (
62
+ runId: string,
63
+ options?: { readonly reason?: InterruptReason; readonly awaitSettlement?: boolean }
64
+ ) => Effect.Effect<boolean>
65
+ readonly awaitIdle: (runId: string) => Effect.Effect<void>
66
+ readonly resumeSuspended: Effect.Effect<ResumeSuspendedResult>
67
+ readonly admit: (item: InboxItem) => Effect.Effect<void>
68
+ readonly pause: (
69
+ runId: string,
70
+ requestIds: ReadonlyArray<string>,
71
+ drainToken: DrainToken
72
+ ) => Effect.Effect<PauseDecision>
73
+ readonly resumeHitl: (runId: string, admission: HitlAdmission) => Effect.Effect<HitlDecision>
74
+ readonly stop: (runId: string) => Effect.Effect<StopDecision>
75
+ }
76
+
77
+ export class Driver extends Context.Service<Driver, DriverApi>()('@yolk-sdk/harness/Driver') {
78
+ static layer(): Layer.Layer<Driver, never, RunStore | Inbox | RunCoordinator>
79
+ static layer(options?: {
80
+ readonly maxResumeAttempts?: undefined
81
+ }): Layer.Layer<Driver, never, RunStore | Inbox | RunCoordinator>
82
+ static layer(
83
+ options?: Pick<DriverLayerOptions, 'maxResumeAttempts'>
84
+ ): Layer.Layer<Driver, InvalidMaxResumeAttempts, RunStore | Inbox | RunCoordinator>
85
+ static layer(options?: Pick<DriverLayerOptions, 'maxResumeAttempts'>) {
86
+ return Layer.effect(
87
+ Driver,
88
+ Effect.gen(function* () {
89
+ const maxResumeAttempts = options?.maxResumeAttempts ?? defaultMaxResumeAttempts
90
+
91
+ if (!isValidMaxResumeAttempts(maxResumeAttempts)) {
92
+ return yield* Effect.fail(new InvalidMaxResumeAttempts({ maxResumeAttempts }))
93
+ }
94
+
95
+ const store = yield* RunStore
96
+ const inbox = yield* Inbox
97
+ const sweep = yield* Semaphore.make(1)
98
+ const coordinator = yield* RunCoordinator
99
+
100
+ const resumeSuspended = sweep.withPermits(1)(
101
+ Effect.gen(function* () {
102
+ const claimed = yield* store.claimed
103
+ const resumed: Array<string> = []
104
+ const exhausted: Array<string> = []
105
+
106
+ for (const runId of claimed) {
107
+ const admission = yield* inbox.admitRecovery(
108
+ runId,
109
+ Effect.gen(function* () {
110
+ if (yield* coordinator.isActive(runId)) return RecoveryAttempt.Skip()
111
+
112
+ if (!(yield* store.isClaimed(runId))) return RecoveryAttempt.Skip()
113
+ const count = yield* store.resumeCount(runId)
114
+
115
+ if (count >= maxResumeAttempts) {
116
+ yield* store.release(runId)
117
+
118
+ return RecoveryAttempt.Exhausted()
119
+ }
120
+
121
+ yield* store.incrementResumeCount(runId)
122
+
123
+ return RecoveryAttempt.Resume()
124
+ }),
125
+ coordinator.wake(runId, 'input')
126
+ )
127
+
128
+ if (Predicate.isTagged(admission, 'Resumed')) resumed.push(runId)
129
+ else if (Predicate.isTagged(admission, 'Exhausted')) exhausted.push(runId)
130
+ }
131
+
132
+ return { resumed, exhausted }
133
+ })
134
+ )
135
+
136
+ return Driver.of({
137
+ active: coordinator.active,
138
+ isActive: coordinator.isActive,
139
+ run: runId => {
140
+ const continueRun = (): Effect.Effect<void> =>
141
+ inbox.startIfUnblocked(runId, coordinator.captureRun(runId)).pipe(
142
+ Effect.flatMap(ticket => {
143
+ if (ticket === undefined) return Effect.void
144
+
145
+ if (Predicate.isTagged(ticket, 'Stopping')) {
146
+ return ticket.awaitSettlement.pipe(Effect.andThen(Effect.suspend(continueRun)))
147
+ }
148
+
149
+ return ticket.join
150
+ })
151
+ )
152
+
153
+ return continueRun()
154
+ },
155
+ wake: (runId, scope = 'input') =>
156
+ inbox.wakeIfUnblocked(runId, scope, coordinator.wake(runId, scope)).pipe(Effect.asVoid),
157
+ interrupt: (runId, interruptOptions) =>
158
+ coordinator.interrupt(runId, interruptOptions?.reason ?? 'user', interruptOptions),
159
+ awaitIdle: coordinator.awaitIdle,
160
+ resumeSuspended,
161
+ admit: item =>
162
+ inbox.enqueueAndWake(
163
+ item,
164
+ coordinator.wake(item.runId, item.delivery === 'steer' ? 'steer' : 'input')
165
+ ),
166
+ pause: (runId, requestIds, drainToken) => inbox.park(runId, requestIds, drainToken),
167
+ resumeHitl: (runId, admission) =>
168
+ inbox.acceptHitl(runId, admission, coordinator.wake(runId, 'input')),
169
+ stop: runId =>
170
+ inbox
171
+ .invalidate(
172
+ runId,
173
+ coordinator
174
+ .terminalStop(runId, 'user')
175
+ .pipe(
176
+ Effect.map(
177
+ receipt =>
178
+ Predicate.isTagged(receipt, 'Interrupted') ||
179
+ Predicate.isTagged(receipt, 'LiveStopping')
180
+ )
181
+ ),
182
+ store
183
+ .isClaimed(runId)
184
+ .pipe(Effect.flatMap(claimed => (claimed ? store.release(runId) : Effect.void)))
185
+ )
186
+ .pipe(
187
+ Effect.map(result => {
188
+ if (result.interrupted) return StopDecision.Interrupted()
189
+
190
+ if (result.hadPark) return StopDecision.ParkCleared()
191
+
192
+ return StopDecision.Idle()
193
+ })
194
+ )
195
+ })
196
+ })
197
+ )
198
+ }
199
+
200
+ static coordinatedLayer(): Layer.Layer<Driver, never, RunStore | Inbox>
201
+ static coordinatedLayer(options?: {
202
+ readonly drain?: Drain
203
+ readonly maxResumeAttempts?: undefined
204
+ }): Layer.Layer<Driver, never, RunStore | Inbox>
205
+ static coordinatedLayer(
206
+ options?: DriverLayerOptions
207
+ ): Layer.Layer<Driver, InvalidMaxResumeAttempts, RunStore | Inbox>
208
+ static coordinatedLayer(options?: DriverLayerOptions) {
209
+ return Layer.unwrap(
210
+ Effect.gen(function* () {
211
+ const store = yield* RunStore
212
+ const inbox = yield* Inbox
213
+ // Keep compatibility option reads lazy and in drain/max order.
214
+ const hostDrain = options?.drain ?? ((_runId, _force, _scope, _context) => Effect.void)
215
+ const maxResumeAttempts = options?.maxResumeAttempts ?? defaultMaxResumeAttempts
216
+ const storeLayer = Layer.succeed(RunStore, store)
217
+ const inboxLayer = Layer.succeed(Inbox, inbox)
218
+
219
+ const coordinator = RunCoordinator.layer({
220
+ drain: (runId, force, scope) =>
221
+ inbox.beginDrain(runId, scope).pipe(
222
+ Effect.flatMap(begun => {
223
+ if (DrainBegin.$is('Skip')(begun)) return Effect.void
224
+
225
+ return Effect.suspend(() =>
226
+ hostDrain(runId, force, scope, {
227
+ drainToken: begun.drainToken,
228
+ readyResponses: begun.readyResponses
229
+ })
230
+ ).pipe(
231
+ Effect.provideService(Inbox, inbox),
232
+ Effect.provideService(RunStore, store),
233
+ Effect.onExit(exit =>
234
+ inbox.endDrain(runId, begun.drainToken, Exit.isSuccess(exit))
235
+ )
236
+ )
237
+ })
238
+ )
239
+ }).pipe(Layer.provide(storeLayer))
240
+
241
+ return Driver.layer({ maxResumeAttempts }).pipe(
242
+ Layer.provide(coordinator),
243
+ Layer.provide(storeLayer),
244
+ Layer.provide(inboxLayer)
245
+ )
246
+ })
247
+ )
248
+ }
249
+ }
250
+
251
+ export const makeHarness = <DE, DR, SE, SR, IE, IR>(input: {
252
+ readonly driver: Layer.Layer<Driver, DE, DR>
253
+ readonly store: Layer.Layer<RunStore, SE, SR>
254
+ readonly inbox: Layer.Layer<Inbox, IE, IR>
255
+ }): Layer.Layer<Driver | RunStore | Inbox, DE | SE | IE, DR | SR | IR> =>
256
+ Layer.mergeAll(input.driver, input.store, input.inbox)
257
+
258
+ export const admit = (item: InboxItem) => Effect.flatMap(Driver, driver => driver.admit(item))
259
+
260
+ export function makeDriverLayer(): Layer.Layer<Driver, never, RunStore | Inbox>
261
+ export function makeDriverLayer(options?: {
262
+ readonly drain?: Drain
263
+ readonly maxResumeAttempts?: undefined
264
+ }): Layer.Layer<Driver, never, RunStore | Inbox>
265
+ export function makeDriverLayer(
266
+ options?: DriverLayerOptions
267
+ ): Layer.Layer<Driver, InvalidMaxResumeAttempts, RunStore | Inbox>
268
+ export function makeDriverLayer(options?: DriverLayerOptions) {
269
+ return options === undefined ? Driver.coordinatedLayer() : Driver.coordinatedLayer(options)
270
+ }