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