@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.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/dist/coordinator.d.mts +72 -0
- package/dist/coordinator.d.mts.map +1 -0
- package/dist/coordinator.mjs +154 -0
- package/dist/coordinator.mjs.map +1 -0
- package/dist/driver/durable-object.d.mts +20 -0
- package/dist/driver/durable-object.d.mts.map +1 -0
- package/dist/driver/durable-object.mjs +12 -0
- package/dist/driver/durable-object.mjs.map +1 -0
- package/dist/driver/memory.d.mts +21 -0
- package/dist/driver/memory.d.mts.map +1 -0
- package/dist/driver/memory.mjs +15 -0
- package/dist/driver/memory.mjs.map +1 -0
- package/dist/driver.d.mts +126 -0
- package/dist/driver.d.mts.map +1 -0
- package/dist/driver.mjs +96 -0
- package/dist/driver.mjs.map +1 -0
- package/dist/inbox.d.mts +368 -0
- package/dist/inbox.d.mts.map +1 -0
- package/dist/inbox.mjs +235 -0
- package/dist/inbox.mjs.map +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/outcome-constructors-internal.d.mts +403 -0
- package/dist/outcome-constructors-internal.d.mts.map +1 -0
- package/dist/outcome-constructors-internal.mjs +11 -0
- package/dist/outcome-constructors-internal.mjs.map +1 -0
- package/dist/outcome.d.mts +361 -0
- package/dist/outcome.d.mts.map +1 -0
- package/dist/outcome.mjs +136 -0
- package/dist/outcome.mjs.map +1 -0
- package/dist/store.d.mts +39 -0
- package/dist/store.d.mts.map +1 -0
- package/dist/store.mjs +98 -0
- package/dist/store.mjs.map +1 -0
- package/package.json +90 -0
- package/src/coordinator.ts +355 -0
- package/src/driver/durable-object.ts +28 -0
- package/src/driver/memory.ts +39 -0
- package/src/driver.ts +270 -0
- package/src/inbox.ts +528 -0
- package/src/index.ts +2 -0
- package/src/outcome-constructors-internal.ts +18 -0
- package/src/outcome.ts +377 -0
- 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
|
+
}
|