@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/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/coordinator.d.mts +62 -0
- package/dist/coordinator.d.mts.map +1 -0
- package/dist/coordinator.mjs +129 -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 +67 -0
- package/dist/driver.d.mts.map +1 -0
- package/dist/driver.mjs +95 -0
- package/dist/driver.mjs.map +1 -0
- package/dist/inbox.d.mts +91 -0
- package/dist/inbox.d.mts.map +1 -0
- package/dist/inbox.mjs +224 -0
- package/dist/inbox.mjs.map +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/outcome.d.mts +61 -0
- package/dist/outcome.d.mts.map +1 -0
- package/dist/outcome.mjs +159 -0
- package/dist/outcome.mjs.map +1 -0
- package/dist/store.d.mts +25 -0
- package/dist/store.d.mts.map +1 -0
- package/dist/store.mjs +86 -0
- package/dist/store.mjs.map +1 -0
- package/package.json +90 -0
- package/src/coordinator.ts +254 -0
- package/src/driver/durable-object.ts +34 -0
- package/src/driver/memory.ts +38 -0
- package/src/driver.ts +250 -0
- package/src/inbox.ts +454 -0
- package/src/index.ts +2 -0
- package/src/outcome.ts +316 -0
- package/src/store.ts +137 -0
package/src/outcome.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { Effect, Stream } from 'effect'
|
|
2
|
+
import {
|
|
3
|
+
addAgentUsage,
|
|
4
|
+
zeroAgentUsage,
|
|
5
|
+
type AgentEvent,
|
|
6
|
+
type AgentMessage,
|
|
7
|
+
type AgentUsage,
|
|
8
|
+
type HitlRequest,
|
|
9
|
+
type HitlResponse
|
|
10
|
+
} from '@yolk-sdk/agent/protocol'
|
|
11
|
+
import {
|
|
12
|
+
collectModelTurnAttempt,
|
|
13
|
+
LLMError,
|
|
14
|
+
runModelTurn,
|
|
15
|
+
runToolBatch,
|
|
16
|
+
type AgentLoopError,
|
|
17
|
+
type ContextTransformer,
|
|
18
|
+
type LLMProvider,
|
|
19
|
+
type LoopConfig,
|
|
20
|
+
type ModelTurnConfig,
|
|
21
|
+
type ModelTurnResult,
|
|
22
|
+
type ToolBatchConfig,
|
|
23
|
+
type ToolExecutor
|
|
24
|
+
} from '@yolk-sdk/agent/loop'
|
|
25
|
+
import {
|
|
26
|
+
applyOverflowCompaction,
|
|
27
|
+
isOverflowCompactionAttemptCount
|
|
28
|
+
} from '@yolk-sdk/agent/compaction'
|
|
29
|
+
|
|
30
|
+
export type OverflowCompactionResult =
|
|
31
|
+
| {
|
|
32
|
+
readonly _tag: 'Compacted'
|
|
33
|
+
readonly messages: ReadonlyArray<AgentMessage>
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
readonly _tag: 'Skipped'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type CompletedTurn = {
|
|
40
|
+
readonly _tag: 'Completed'
|
|
41
|
+
readonly needsContinuation: boolean
|
|
42
|
+
} & ModelTurnResult
|
|
43
|
+
|
|
44
|
+
export type ModelTurnOutcome =
|
|
45
|
+
| CompletedTurn
|
|
46
|
+
| { readonly _tag: 'Retry'; readonly error: AgentLoopError }
|
|
47
|
+
| ({ readonly _tag: 'Continue'; readonly error: AgentLoopError } & ModelTurnResult)
|
|
48
|
+
| { readonly _tag: 'RecoverFull'; readonly error: AgentLoopError }
|
|
49
|
+
| {
|
|
50
|
+
readonly _tag: 'Compacted'
|
|
51
|
+
readonly messages: ReadonlyArray<AgentMessage>
|
|
52
|
+
readonly overflowCompactionAttempt: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ToolBatchOutcome =
|
|
56
|
+
| CompletedTurn
|
|
57
|
+
| {
|
|
58
|
+
readonly _tag: 'AwaitingInput'
|
|
59
|
+
readonly requests: ReadonlyArray<HitlRequest>
|
|
60
|
+
readonly usage: AgentUsage
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type StepOutcome = ModelTurnOutcome | ToolBatchOutcome
|
|
64
|
+
|
|
65
|
+
const completedOutcome = (result: ModelTurnResult): CompletedTurn => ({
|
|
66
|
+
_tag: 'Completed',
|
|
67
|
+
needsContinuation: result.stopReason === 'tool_use',
|
|
68
|
+
assistantMessage: result.assistantMessage,
|
|
69
|
+
toolCalls: result.toolCalls,
|
|
70
|
+
usage: result.usage,
|
|
71
|
+
stopReason: result.stopReason
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const modelTurnResult = (result: ModelTurnResult): ModelTurnResult => ({
|
|
75
|
+
assistantMessage: result.assistantMessage,
|
|
76
|
+
toolCalls: result.toolCalls,
|
|
77
|
+
usage: result.usage,
|
|
78
|
+
stopReason: result.stopReason
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const invalidOverflowCompactionAttemptError = () =>
|
|
82
|
+
new LLMError({
|
|
83
|
+
cause: 'validation_error',
|
|
84
|
+
message: 'overflowCompactionAttempt must be a finite integer >= 0',
|
|
85
|
+
retryable: false
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const isAgentLoopError = (error: unknown): error is AgentLoopError => {
|
|
89
|
+
if (typeof error !== 'object' || error === null || !('_tag' in error)) {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
const tag = error._tag
|
|
93
|
+
return (
|
|
94
|
+
tag === 'LLMError' ||
|
|
95
|
+
tag === 'FauxExhaustedError' ||
|
|
96
|
+
tag === 'ToolError' ||
|
|
97
|
+
tag === 'ContextTransformError' ||
|
|
98
|
+
tag === 'AbortError'
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isOverflow = (error: AgentLoopError) =>
|
|
103
|
+
(error._tag === 'LLMError' || error._tag === 'ContextTransformError') &&
|
|
104
|
+
error.cause === 'context_overflow'
|
|
105
|
+
|
|
106
|
+
const isMissingDone = (error: AgentLoopError) =>
|
|
107
|
+
error._tag === 'LLMError' && error.responseIssue === 'missing_done'
|
|
108
|
+
|
|
109
|
+
const isRetryableLlm = (
|
|
110
|
+
error: AgentLoopError
|
|
111
|
+
): error is Extract<AgentLoopError, { _tag: 'LLMError' }> =>
|
|
112
|
+
error._tag === 'LLMError' && error.retryable
|
|
113
|
+
|
|
114
|
+
const classifyModelTurnFailure = <E2, R2>(input: {
|
|
115
|
+
readonly error: AgentLoopError
|
|
116
|
+
readonly collected: ModelTurnResult
|
|
117
|
+
readonly outputStarted: boolean
|
|
118
|
+
readonly messages: ReadonlyArray<AgentMessage>
|
|
119
|
+
readonly overflowCompactionAttempt: number
|
|
120
|
+
readonly compact?: (
|
|
121
|
+
messages: ReadonlyArray<AgentMessage>
|
|
122
|
+
) => Effect.Effect<OverflowCompactionResult, E2, R2>
|
|
123
|
+
}): Effect.Effect<ModelTurnOutcome, AgentLoopError | E2, R2> => {
|
|
124
|
+
if (isOverflow(input.error)) {
|
|
125
|
+
if (input.compact === undefined) {
|
|
126
|
+
return Effect.fail(input.error)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return applyOverflowCompaction({
|
|
130
|
+
compact: input.compact,
|
|
131
|
+
messages: input.messages,
|
|
132
|
+
attempt: input.overflowCompactionAttempt,
|
|
133
|
+
outputStarted: input.outputStarted
|
|
134
|
+
}).pipe(
|
|
135
|
+
Effect.flatMap(result =>
|
|
136
|
+
result._tag === 'Compacted'
|
|
137
|
+
? Effect.succeed({
|
|
138
|
+
_tag: 'Compacted' as const,
|
|
139
|
+
messages: result.messages,
|
|
140
|
+
overflowCompactionAttempt: input.overflowCompactionAttempt + 1
|
|
141
|
+
})
|
|
142
|
+
: Effect.fail(input.error)
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (isMissingDone(input.error) && !input.outputStarted) {
|
|
148
|
+
return Effect.succeed({ _tag: 'RecoverFull', error: input.error })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (isMissingDone(input.error) && input.outputStarted) {
|
|
152
|
+
return Effect.succeed({
|
|
153
|
+
_tag: 'Continue',
|
|
154
|
+
error: input.error,
|
|
155
|
+
...modelTurnResult(input.collected)
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (isRetryableLlm(input.error) && !input.outputStarted) {
|
|
160
|
+
if (input.error.cause === 'invalid_response') {
|
|
161
|
+
return Effect.succeed({ _tag: 'RecoverFull', error: input.error })
|
|
162
|
+
}
|
|
163
|
+
return Effect.succeed({ _tag: 'Retry', error: input.error })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (isRetryableLlm(input.error) && input.outputStarted) {
|
|
167
|
+
return Effect.succeed({
|
|
168
|
+
_tag: 'Continue',
|
|
169
|
+
error: input.error,
|
|
170
|
+
...modelTurnResult(input.collected)
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return Effect.fail(input.error)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const attemptModelTurn = <E2 = never, R2 = never>(
|
|
178
|
+
config: ModelTurnConfig,
|
|
179
|
+
options?: {
|
|
180
|
+
readonly onEvent?: (event: AgentEvent) => Effect.Effect<void, E2, R2>
|
|
181
|
+
readonly initialUsage?: AgentUsage
|
|
182
|
+
readonly overflowCompactionAttempt?: number
|
|
183
|
+
readonly compact?: (
|
|
184
|
+
messages: ReadonlyArray<AgentMessage>
|
|
185
|
+
) => Effect.Effect<OverflowCompactionResult, E2, R2>
|
|
186
|
+
}
|
|
187
|
+
): Effect.Effect<
|
|
188
|
+
ModelTurnOutcome,
|
|
189
|
+
AgentLoopError | E2,
|
|
190
|
+
ContextTransformer | LLMProvider | LoopConfig | R2
|
|
191
|
+
> =>
|
|
192
|
+
Effect.gen(function* () {
|
|
193
|
+
const overflowCompactionAttempt = options?.overflowCompactionAttempt ?? 0
|
|
194
|
+
if (!isOverflowCompactionAttemptCount(overflowCompactionAttempt)) {
|
|
195
|
+
return yield* Effect.fail(invalidOverflowCompactionAttemptError())
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const outcome = yield* collectModelTurnAttempt(runModelTurn(config), {
|
|
199
|
+
onEvent: options?.onEvent,
|
|
200
|
+
initialUsage: options?.initialUsage
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
switch (outcome._tag) {
|
|
204
|
+
case 'Collected':
|
|
205
|
+
return completedOutcome(outcome.collection)
|
|
206
|
+
case 'SinkFailed':
|
|
207
|
+
return yield* Effect.fail(outcome.error)
|
|
208
|
+
case 'StreamFailed':
|
|
209
|
+
if (!isAgentLoopError(outcome.error)) {
|
|
210
|
+
return yield* Effect.fail(outcome.error)
|
|
211
|
+
}
|
|
212
|
+
return yield* classifyModelTurnFailure({
|
|
213
|
+
error: outcome.error,
|
|
214
|
+
collected: {
|
|
215
|
+
assistantMessage:
|
|
216
|
+
outcome.collection.assistantMessage ?? outcome.collection.partialAssistantMessage,
|
|
217
|
+
toolCalls: outcome.collection.toolCalls,
|
|
218
|
+
usage: outcome.collection.usage,
|
|
219
|
+
stopReason: outcome.collection.stopReason
|
|
220
|
+
},
|
|
221
|
+
outputStarted: outcome.collection.outputStarted,
|
|
222
|
+
messages: config.messages,
|
|
223
|
+
overflowCompactionAttempt,
|
|
224
|
+
compact: options?.compact
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
export const attemptToolBatch = <E2 = never, R2 = never>(
|
|
230
|
+
config: ToolBatchConfig,
|
|
231
|
+
options?: {
|
|
232
|
+
readonly onEvent?: (event: AgentEvent) => Effect.Effect<void, E2, R2>
|
|
233
|
+
}
|
|
234
|
+
): Effect.Effect<ToolBatchOutcome, AgentLoopError | E2, LoopConfig | ToolExecutor | R2> => {
|
|
235
|
+
const onEvent = options?.onEvent
|
|
236
|
+
|
|
237
|
+
return runToolBatch(config).pipe(
|
|
238
|
+
Stream.runFoldEffect(
|
|
239
|
+
(): {
|
|
240
|
+
requests: ReadonlyArray<HitlRequest>
|
|
241
|
+
usage: AgentUsage
|
|
242
|
+
toolCalls: ModelTurnResult['toolCalls']
|
|
243
|
+
} => ({
|
|
244
|
+
requests: [],
|
|
245
|
+
usage: config.usage ?? zeroAgentUsage,
|
|
246
|
+
toolCalls: []
|
|
247
|
+
}),
|
|
248
|
+
(acc, event) => {
|
|
249
|
+
const next =
|
|
250
|
+
event._tag === 'AgentAwaitingInput'
|
|
251
|
+
? { ...acc, requests: event.requests, usage: event.usage }
|
|
252
|
+
: event._tag === 'UsageUpdate'
|
|
253
|
+
? { ...acc, usage: addAgentUsage(acc.usage, event.usage) }
|
|
254
|
+
: event._tag === 'ToolExecutionCompleted' || event._tag === 'ToolExecutionAccepted'
|
|
255
|
+
? { ...acc, toolCalls: [...acc.toolCalls, event.call] }
|
|
256
|
+
: acc
|
|
257
|
+
return onEvent === undefined ? Effect.succeed(next) : onEvent(event).pipe(Effect.as(next))
|
|
258
|
+
}
|
|
259
|
+
),
|
|
260
|
+
Effect.map((result): ToolBatchOutcome => {
|
|
261
|
+
if (result.requests.length === 0) {
|
|
262
|
+
const needsContinuation = result.toolCalls.length > 0
|
|
263
|
+
return {
|
|
264
|
+
_tag: 'Completed',
|
|
265
|
+
needsContinuation,
|
|
266
|
+
assistantMessage: undefined,
|
|
267
|
+
toolCalls: result.toolCalls,
|
|
268
|
+
usage: result.usage,
|
|
269
|
+
stopReason: needsContinuation ? 'tool_use' : 'stop'
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { _tag: 'AwaitingInput', requests: result.requests, usage: result.usage }
|
|
273
|
+
})
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export type HitlMatch =
|
|
278
|
+
| { readonly _tag: 'Match'; readonly requestId: string }
|
|
279
|
+
| { readonly _tag: 'Mismatch' }
|
|
280
|
+
|
|
281
|
+
const hitlResponseMatchesRequest = (response: HitlResponse, request: HitlRequest) => {
|
|
282
|
+
switch (response._tag) {
|
|
283
|
+
case 'ToolApprovalResponse':
|
|
284
|
+
return (
|
|
285
|
+
request._tag === 'ToolApprovalRequest' &&
|
|
286
|
+
response.requestId === request.requestId &&
|
|
287
|
+
response.toolCallId === request.toolCallId
|
|
288
|
+
)
|
|
289
|
+
case 'QuestionResponse':
|
|
290
|
+
return (
|
|
291
|
+
request._tag === 'QuestionRequest' &&
|
|
292
|
+
response.requestId === request.requestId &&
|
|
293
|
+
response.toolCallId === request.toolCallId
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export const matchHitlResponse = (
|
|
299
|
+
pending: ReadonlyArray<HitlRequest>,
|
|
300
|
+
response: HitlResponse
|
|
301
|
+
): HitlMatch => {
|
|
302
|
+
const matched = pending.find(request => hitlResponseMatchesRequest(response, request))
|
|
303
|
+
return matched === undefined
|
|
304
|
+
? { _tag: 'Mismatch' }
|
|
305
|
+
: { _tag: 'Match', requestId: matched.requestId }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export const resumeHitlIfMatched = <A, E, R>(input: {
|
|
309
|
+
readonly pending: ReadonlyArray<HitlRequest>
|
|
310
|
+
readonly response: HitlResponse
|
|
311
|
+
readonly resume: (requestId: string) => Effect.Effect<A, E, R>
|
|
312
|
+
}): Effect.Effect<A | { readonly _tag: 'Mismatch' }, E, R> => {
|
|
313
|
+
const matched = matchHitlResponse(input.pending, input.response)
|
|
314
|
+
if (matched._tag === 'Mismatch') return Effect.succeed(matched)
|
|
315
|
+
return input.resume(matched.requestId)
|
|
316
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Ref, Semaphore } from 'effect'
|
|
2
|
+
|
|
3
|
+
export type RunStoreShape = {
|
|
4
|
+
readonly claim: (runId: string) => Effect.Effect<void>
|
|
5
|
+
readonly release: (runId: string) => Effect.Effect<void>
|
|
6
|
+
readonly isClaimed: (runId: string) => Effect.Effect<boolean>
|
|
7
|
+
readonly claimed: Effect.Effect<ReadonlySet<string>>
|
|
8
|
+
readonly incrementResumeCount: (runId: string) => Effect.Effect<number>
|
|
9
|
+
readonly resumeCount: (runId: string) => Effect.Effect<number>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class RunStore extends Context.Service<RunStore, RunStoreShape>()(
|
|
13
|
+
'@yolk-sdk/harness/RunStore'
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
export type DurableRunStoreSnapshot = {
|
|
17
|
+
readonly claimed: ReadonlyArray<string>
|
|
18
|
+
readonly resumes: ReadonlyArray<readonly [string, number]>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type MemorySnapshot = {
|
|
22
|
+
readonly claimed: ReadonlySet<string>
|
|
23
|
+
readonly resumes: ReadonlyMap<string, number>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const emptySnapshot: DurableRunStoreSnapshot = {
|
|
27
|
+
claimed: [],
|
|
28
|
+
resumes: []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const snapshotFromMemory = (memory: MemorySnapshot): DurableRunStoreSnapshot => ({
|
|
32
|
+
claimed: [...memory.claimed],
|
|
33
|
+
resumes: [...memory.resumes.entries()]
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const memoryFromSnapshot = (snapshot: DurableRunStoreSnapshot): MemorySnapshot => ({
|
|
37
|
+
claimed: new Set(snapshot.claimed),
|
|
38
|
+
resumes: new Map(snapshot.resumes)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const makeSnapshotRunStore = (options: {
|
|
42
|
+
readonly load: Effect.Effect<DurableRunStoreSnapshot | undefined>
|
|
43
|
+
readonly save: (snapshot: DurableRunStoreSnapshot) => Effect.Effect<void>
|
|
44
|
+
}): Effect.Effect<RunStore['Service']> =>
|
|
45
|
+
Effect.gen(function* () {
|
|
46
|
+
const loaded = yield* options.load
|
|
47
|
+
const memory = yield* Ref.make(memoryFromSnapshot(loaded ?? emptySnapshot))
|
|
48
|
+
const lock = yield* Semaphore.make(1)
|
|
49
|
+
const mutate = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
50
|
+
Effect.uninterruptibleMask(restore =>
|
|
51
|
+
restore(lock.take(1)).pipe(
|
|
52
|
+
Effect.flatMap(() => effect.pipe(Effect.ensuring(lock.release(1))))
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
const commit = (next: MemorySnapshot) =>
|
|
56
|
+
options.save(snapshotFromMemory(next)).pipe(Effect.flatMap(() => Ref.set(memory, next)))
|
|
57
|
+
|
|
58
|
+
return RunStore.of({
|
|
59
|
+
claim: runId =>
|
|
60
|
+
mutate(
|
|
61
|
+
Effect.gen(function* () {
|
|
62
|
+
const current = yield* Ref.get(memory)
|
|
63
|
+
yield* commit({
|
|
64
|
+
claimed: new Set(current.claimed).add(runId),
|
|
65
|
+
resumes: current.resumes
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
),
|
|
69
|
+
release: runId =>
|
|
70
|
+
mutate(
|
|
71
|
+
Effect.gen(function* () {
|
|
72
|
+
const current = yield* Ref.get(memory)
|
|
73
|
+
const claimed = new Set(current.claimed)
|
|
74
|
+
claimed.delete(runId)
|
|
75
|
+
const resumes = new Map(current.resumes)
|
|
76
|
+
resumes.delete(runId)
|
|
77
|
+
yield* commit({ claimed, resumes })
|
|
78
|
+
})
|
|
79
|
+
),
|
|
80
|
+
isClaimed: runId => Ref.get(memory).pipe(Effect.map(current => current.claimed.has(runId))),
|
|
81
|
+
claimed: Ref.get(memory).pipe(Effect.map(current => new Set(current.claimed))),
|
|
82
|
+
incrementResumeCount: runId =>
|
|
83
|
+
mutate(
|
|
84
|
+
Effect.gen(function* () {
|
|
85
|
+
const current = yield* Ref.get(memory)
|
|
86
|
+
const nextCount = (current.resumes.get(runId) ?? 0) + 1
|
|
87
|
+
const resumes = new Map(current.resumes)
|
|
88
|
+
resumes.set(runId, nextCount)
|
|
89
|
+
yield* commit({ claimed: current.claimed, resumes })
|
|
90
|
+
return nextCount
|
|
91
|
+
})
|
|
92
|
+
),
|
|
93
|
+
resumeCount: runId =>
|
|
94
|
+
Ref.get(memory).pipe(Effect.map(current => current.resumes.get(runId) ?? 0))
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
export const makeSnapshotRunStoreLayer = (options: {
|
|
99
|
+
readonly load: Effect.Effect<DurableRunStoreSnapshot | undefined>
|
|
100
|
+
readonly save: (snapshot: DurableRunStoreSnapshot) => Effect.Effect<void>
|
|
101
|
+
}): Layer.Layer<RunStore> => Layer.effect(RunStore, makeSnapshotRunStore(options))
|
|
102
|
+
|
|
103
|
+
export const makeInMemoryRunStoreLayer = (): Layer.Layer<RunStore> =>
|
|
104
|
+
Layer.effect(
|
|
105
|
+
RunStore,
|
|
106
|
+
Effect.gen(function* () {
|
|
107
|
+
const claimed = yield* Ref.make(new Set<string>())
|
|
108
|
+
const resumes = yield* Ref.make(new Map<string, number>())
|
|
109
|
+
|
|
110
|
+
return RunStore.of({
|
|
111
|
+
claim: runId => Ref.update(claimed, current => new Set(current).add(runId)),
|
|
112
|
+
release: runId =>
|
|
113
|
+
Effect.zip(
|
|
114
|
+
Ref.update(claimed, current => {
|
|
115
|
+
const next = new Set(current)
|
|
116
|
+
next.delete(runId)
|
|
117
|
+
return next
|
|
118
|
+
}),
|
|
119
|
+
Ref.update(resumes, current => {
|
|
120
|
+
const next = new Map(current)
|
|
121
|
+
next.delete(runId)
|
|
122
|
+
return next
|
|
123
|
+
})
|
|
124
|
+
).pipe(Effect.asVoid),
|
|
125
|
+
isClaimed: runId => Ref.get(claimed).pipe(Effect.map(current => current.has(runId))),
|
|
126
|
+
claimed: Ref.get(claimed).pipe(Effect.map(current => new Set(current))),
|
|
127
|
+
incrementResumeCount: runId =>
|
|
128
|
+
Ref.modify(resumes, current => {
|
|
129
|
+
const nextCount = (current.get(runId) ?? 0) + 1
|
|
130
|
+
const next = new Map(current)
|
|
131
|
+
next.set(runId, nextCount)
|
|
132
|
+
return [nextCount, next] as const
|
|
133
|
+
}),
|
|
134
|
+
resumeCount: runId => Ref.get(resumes).pipe(Effect.map(current => current.get(runId) ?? 0))
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
)
|