@llm4ts/flow 2.8.0 → 2.9.1
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/dist/Approval.d.ts +1 -1
- package/dist/Approval.d.ts.map +1 -1
- package/dist/Artifacts.d.ts +2 -2
- package/dist/Artifacts.d.ts.map +1 -1
- package/dist/BenchReport.d.ts +2 -2
- package/dist/BenchReport.d.ts.map +1 -1
- package/dist/CostLedger.d.ts +2 -2
- package/dist/CostLedger.d.ts.map +1 -1
- package/dist/Equiv.d.ts +3 -3
- package/dist/Equiv.d.ts.map +1 -1
- package/dist/Flow.d.ts +1 -1
- package/dist/Flow.d.ts.map +1 -1
- package/dist/FlowContext.d.ts +15 -1
- package/dist/FlowContext.d.ts.map +1 -1
- package/dist/FlowContext.js.map +1 -1
- package/dist/FlowError.d.ts +24 -1
- package/dist/FlowError.d.ts.map +1 -1
- package/dist/FlowError.js +29 -0
- package/dist/FlowError.js.map +1 -1
- package/dist/FlowEvents.d.ts +45 -0
- package/dist/FlowEvents.d.ts.map +1 -1
- package/dist/FlowEvents.js +87 -7
- package/dist/FlowEvents.js.map +1 -1
- package/dist/Judgment.d.ts +2 -2
- package/dist/Judgment.d.ts.map +1 -1
- package/dist/Persistence.d.ts +2 -2
- package/dist/Persistence.d.ts.map +1 -1
- package/dist/PlanExecution.d.ts +1 -1
- package/dist/PlanExecution.d.ts.map +1 -1
- package/dist/ProgramJudge.d.ts +1 -1
- package/dist/ProgramJudge.d.ts.map +1 -1
- package/dist/Replay.d.ts +2 -2
- package/dist/Replay.d.ts.map +1 -1
- package/dist/Review.d.ts +3 -3
- package/dist/Review.d.ts.map +1 -1
- package/dist/ReviewCache.d.ts +1 -1
- package/dist/ReviewCache.d.ts.map +1 -1
- package/dist/Roster.d.ts +164 -0
- package/dist/Roster.d.ts.map +1 -0
- package/dist/Roster.js +583 -0
- package/dist/Roster.js.map +1 -0
- package/dist/RosterSeats.d.ts +71 -0
- package/dist/RosterSeats.d.ts.map +1 -0
- package/dist/RosterSeats.js +167 -0
- package/dist/RosterSeats.js.map +1 -0
- package/dist/Stories.d.ts +9 -5
- package/dist/Stories.d.ts.map +1 -1
- package/dist/Stories.js +95 -24
- package/dist/Stories.js.map +1 -1
- package/package.json +4 -2
- package/src/FlowContext.ts +19 -1
- package/src/FlowError.ts +32 -0
- package/src/FlowEvents.ts +108 -7
- package/src/Roster.ts +849 -0
- package/src/RosterSeats.ts +360 -0
- package/src/Stories.ts +129 -28
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// Seats over the executor roster (ADR 0019). A flow keeps calling an
|
|
2
|
+
// `LlmServiceShape`; behind it, a reasoning seat leases an executor for each
|
|
3
|
+
// call, and a coder seat holds one executor for its context and hands over
|
|
4
|
+
// to the next when that one is taken out of the round. The runner supplies
|
|
5
|
+
// `seatFor`, which turns an executor into a connector rooted in a directory.
|
|
6
|
+
import * as Effect from "effect/Effect"
|
|
7
|
+
import * as Ref from "effect/Ref"
|
|
8
|
+
import * as Scope from "effect/Scope"
|
|
9
|
+
import * as Semaphore from "effect/Semaphore"
|
|
10
|
+
import * as Stream from "effect/Stream"
|
|
11
|
+
import { ConfigError, type LlmError } from "@llm4ts/core/Errors"
|
|
12
|
+
import type { LlmServiceShape } from "@llm4ts/core/LlmService"
|
|
13
|
+
import { Message, type ConnectorCapabilities, type LlmChunk } from "@llm4ts/core/Models"
|
|
14
|
+
import { describeFlowError, type FlowError } from "./FlowError.ts"
|
|
15
|
+
import { Info, type FlowEventsShape } from "./FlowEvents.ts"
|
|
16
|
+
import {
|
|
17
|
+
describeExclusion,
|
|
18
|
+
hasRole,
|
|
19
|
+
type ExecutorSpec,
|
|
20
|
+
type Lease,
|
|
21
|
+
type Role,
|
|
22
|
+
type RosterShape
|
|
23
|
+
} from "./Roster.ts"
|
|
24
|
+
|
|
25
|
+
/** An executor's connector, as the runner resolved it for a role and a directory. */
|
|
26
|
+
export type RosterSeat = LlmServiceShape & { readonly capabilities: ConnectorCapabilities }
|
|
27
|
+
|
|
28
|
+
export interface SeatSource {
|
|
29
|
+
readonly seatFor: (
|
|
30
|
+
executor: ExecutorSpec,
|
|
31
|
+
role: Role,
|
|
32
|
+
workDir: string
|
|
33
|
+
) => Effect.Effect<RosterSeat, FlowError>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What a flow context knows about the roster behind its seats. */
|
|
37
|
+
export interface RosterView {
|
|
38
|
+
/** A seat for `role`, leased per call (independent of this context's coder where possible). */
|
|
39
|
+
readonly forRole: (role: Role) => LlmServiceShape
|
|
40
|
+
/** Free slots for `role` right now. */
|
|
41
|
+
readonly available: (role: Role) => Effect.Effect<number>
|
|
42
|
+
/** Configured slots for `role`. */
|
|
43
|
+
readonly slots: (role: Role) => number
|
|
44
|
+
/** The executor holding this context's coder, once leased. */
|
|
45
|
+
readonly executor: Effect.Effect<string | undefined>
|
|
46
|
+
/** Every executor that held this context's coder, in order (handovers). */
|
|
47
|
+
readonly history: Effect.Effect<ReadonlyArray<string>>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A roster failure seen through an `LlmServiceShape`, whose failures are `LlmError`s. */
|
|
51
|
+
const asLlmError = (error: FlowError): LlmError =>
|
|
52
|
+
ConfigError.make({ message: describeFlowError(error) })
|
|
53
|
+
|
|
54
|
+
const say = (events: FlowEventsShape, message: string): Effect.Effect<void> =>
|
|
55
|
+
events.publish(Info.make({ message: `roster: ${message}` }))
|
|
56
|
+
|
|
57
|
+
/** Shared plumbing: every service method as "run this on some seat". */
|
|
58
|
+
interface SeatRunner {
|
|
59
|
+
readonly run: <A, R>(
|
|
60
|
+
use: (seat: RosterSeat, note: string | undefined) => Effect.Effect<A, LlmError, R>
|
|
61
|
+
) => Effect.Effect<A, LlmError, R>
|
|
62
|
+
readonly stream: (
|
|
63
|
+
use: (seat: RosterSeat, note: string | undefined) => Stream.Stream<LlmChunk, LlmError>
|
|
64
|
+
) => Stream.Stream<LlmChunk, LlmError>
|
|
65
|
+
readonly isAvailable: Effect.Effect<boolean>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const withNote = (note: string | undefined, prompt: string): string =>
|
|
69
|
+
note === undefined ? prompt : `${note}\n\n${prompt}`
|
|
70
|
+
|
|
71
|
+
const withNoteOnLast = (
|
|
72
|
+
note: string | undefined,
|
|
73
|
+
messages: ReadonlyArray<Message>
|
|
74
|
+
): ReadonlyArray<Message> => {
|
|
75
|
+
const last = messages.at(-1)
|
|
76
|
+
if (note === undefined || last === undefined) {
|
|
77
|
+
return messages
|
|
78
|
+
}
|
|
79
|
+
return [
|
|
80
|
+
...messages.slice(0, -1),
|
|
81
|
+
Message.make({ ...last, content: withNote(note, last.content) })
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const serviceOver = (runner: SeatRunner): LlmServiceShape => ({
|
|
86
|
+
executeStream: (prompt) =>
|
|
87
|
+
runner.stream((seat, note) => seat.executeStream(withNote(note, prompt))),
|
|
88
|
+
executeStreamWithHistory: (messages) =>
|
|
89
|
+
runner.stream((seat, note) => seat.executeStreamWithHistory(withNoteOnLast(note, messages))),
|
|
90
|
+
executeWithTools: (prompt, tools) =>
|
|
91
|
+
runner.run((seat, note) => seat.executeWithTools(withNote(note, prompt), tools)),
|
|
92
|
+
executeStructured: (prompt, schema, jsonSchema) =>
|
|
93
|
+
runner.run((seat, note) => seat.executeStructured(withNote(note, prompt), schema, jsonSchema)),
|
|
94
|
+
executeStructuredWithUsage: (prompt, schema, jsonSchema) =>
|
|
95
|
+
runner.run((seat, note) =>
|
|
96
|
+
seat.executeStructuredWithUsage(withNote(note, prompt), schema, jsonSchema)
|
|
97
|
+
),
|
|
98
|
+
scoreLabels: (prompt, labels) => runner.run((seat) => seat.scoreLabels(prompt, labels)),
|
|
99
|
+
isAvailable: runner.isAvailable
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// ---- Per-call seats ----------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
export interface RosterSeatOptions {
|
|
105
|
+
readonly events: FlowEventsShape
|
|
106
|
+
/** Executors this seat must not use: the context's coder, for independence. */
|
|
107
|
+
readonly avoid?: Effect.Effect<ReadonlyArray<string>>
|
|
108
|
+
/** Used without a slot when nobody outside `avoid` can ever serve the role. */
|
|
109
|
+
readonly borrow?: Effect.Effect<ExecutorSpec | undefined>
|
|
110
|
+
/** Who is asking, for the events. */
|
|
111
|
+
readonly label?: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A seat for `role` that leases an executor for each call and releases it
|
|
116
|
+
* when the call (or its stream) ends. A failure is reported to the roster,
|
|
117
|
+
* which excludes the executor when the failure says it is out of quota or
|
|
118
|
+
* down; the call itself still fails — a per-call seat has nothing to hand
|
|
119
|
+
* over, and the retry layer below has already had its turn.
|
|
120
|
+
*/
|
|
121
|
+
export const rosterSeat = (
|
|
122
|
+
roster: RosterShape,
|
|
123
|
+
source: SeatSource,
|
|
124
|
+
role: Role,
|
|
125
|
+
workDir: string,
|
|
126
|
+
options: RosterSeatOptions
|
|
127
|
+
): LlmServiceShape => {
|
|
128
|
+
const acquire = (
|
|
129
|
+
alsoAvoid: ReadonlyArray<string>
|
|
130
|
+
): Effect.Effect<ExecutorSpec, LlmError, Scope.Scope> =>
|
|
131
|
+
Effect.gen(function* () {
|
|
132
|
+
const avoid = [...(options.avoid === undefined ? [] : yield* options.avoid), ...alsoAvoid]
|
|
133
|
+
if (avoid.length > 0 && !(yield* roster.canEverServe(role, avoid))) {
|
|
134
|
+
const borrowed = options.borrow === undefined ? undefined : yield* options.borrow
|
|
135
|
+
if (borrowed !== undefined && hasRole(borrowed, role)) {
|
|
136
|
+
yield* say(
|
|
137
|
+
options.events,
|
|
138
|
+
`${borrowed.id} takes ${role}${options.label === undefined ? "" : ` for ${options.label}`} on its own coder's slot — not independent (no other executor can take ${role})`
|
|
139
|
+
)
|
|
140
|
+
return borrowed
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const lease = yield* roster
|
|
144
|
+
.lease(role, { avoid, ...(options.label === undefined ? {} : { label: options.label }) })
|
|
145
|
+
.pipe(Effect.mapError(asLlmError))
|
|
146
|
+
return lease.executor
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const seatOf = (executor: ExecutorSpec): Effect.Effect<RosterSeat, LlmError> =>
|
|
150
|
+
source.seatFor(executor, role, workDir).pipe(Effect.mapError(asLlmError))
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* After a failure that took `executor` out of the round, whether another
|
|
154
|
+
* executor can still take the call: the call moves once, never twice — a
|
|
155
|
+
* second failure is the call's own, not the executor's.
|
|
156
|
+
*/
|
|
157
|
+
const retryElsewhere = (
|
|
158
|
+
executor: ExecutorSpec,
|
|
159
|
+
error: LlmError,
|
|
160
|
+
tried: ReadonlyArray<string>
|
|
161
|
+
): Effect.Effect<boolean> =>
|
|
162
|
+
Effect.gen(function* () {
|
|
163
|
+
const exclusion = yield* roster.report(executor.id, error)
|
|
164
|
+
if (exclusion === undefined || tried.length > 0) {
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
const avoid = options.avoid === undefined ? [] : yield* options.avoid
|
|
168
|
+
const other = yield* roster.canEverServe(role, [...avoid, executor.id])
|
|
169
|
+
if (other) {
|
|
170
|
+
yield* say(
|
|
171
|
+
options.events,
|
|
172
|
+
`${role}${options.label === undefined ? "" : ` for ${options.label}`} moves off ${executor.id} (${describeExclusion(exclusion)})`
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
return other
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const runOn = <A, R>(
|
|
179
|
+
use: (seat: RosterSeat, note: string | undefined) => Effect.Effect<A, LlmError, R>,
|
|
180
|
+
tried: ReadonlyArray<string>
|
|
181
|
+
): Effect.Effect<A, LlmError, R> =>
|
|
182
|
+
// The slot is held for the call and released before any retry.
|
|
183
|
+
Effect.scoped(
|
|
184
|
+
Effect.gen(function* () {
|
|
185
|
+
const executor = yield* acquire(tried)
|
|
186
|
+
const seat = yield* seatOf(executor)
|
|
187
|
+
const outcome = yield* Effect.result(use(seat, undefined))
|
|
188
|
+
return { executor, outcome }
|
|
189
|
+
})
|
|
190
|
+
).pipe(
|
|
191
|
+
Effect.flatMap(({ executor, outcome }) =>
|
|
192
|
+
outcome._tag === "Success"
|
|
193
|
+
? Effect.succeed(outcome.success)
|
|
194
|
+
: Effect.flatMap(retryElsewhere(executor, outcome.failure, tried), (again) =>
|
|
195
|
+
again ? runOn(use, [...tried, executor.id]) : Effect.fail(outcome.failure)
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
const streamOn = (
|
|
201
|
+
use: (seat: RosterSeat, note: string | undefined) => Stream.Stream<LlmChunk, LlmError>,
|
|
202
|
+
tried: ReadonlyArray<string>
|
|
203
|
+
): Stream.Stream<LlmChunk, LlmError> =>
|
|
204
|
+
Stream.unwrap(
|
|
205
|
+
Effect.gen(function* () {
|
|
206
|
+
const executor = yield* acquire(tried)
|
|
207
|
+
const seat = yield* seatOf(executor)
|
|
208
|
+
return use(seat, undefined).pipe(
|
|
209
|
+
Stream.catchIf(
|
|
210
|
+
() => true,
|
|
211
|
+
(error) =>
|
|
212
|
+
Stream.unwrap(
|
|
213
|
+
Effect.map(retryElsewhere(executor, error, tried), (again) =>
|
|
214
|
+
again ? streamOn(use, [...tried, executor.id]) : Stream.fail(error)
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
})
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return serviceOver({
|
|
223
|
+
run: (use) => runOn(use, []),
|
|
224
|
+
stream: (use) => streamOn(use, []),
|
|
225
|
+
isAvailable: roster.canEverServe(role)
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---- The held coder ------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
export interface HeldCoderOptions {
|
|
232
|
+
readonly events: FlowEventsShape
|
|
233
|
+
/** Lease now (a story waiting for capacity), or on the first call (a flow's root coder). */
|
|
234
|
+
readonly eager: boolean
|
|
235
|
+
/** The executor this context held before, taken first while free. */
|
|
236
|
+
readonly prefer?: string
|
|
237
|
+
/** Handovers before a failure surfaces. Default 2. */
|
|
238
|
+
readonly maxHandovers?: number
|
|
239
|
+
readonly label?: string
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface HeldCoder {
|
|
243
|
+
readonly service: LlmServiceShape
|
|
244
|
+
readonly executor: Effect.Effect<string | undefined>
|
|
245
|
+
readonly lease: Effect.Effect<ExecutorSpec | undefined>
|
|
246
|
+
readonly history: Effect.Effect<ReadonlyArray<string>>
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const takeoverNote = (from: string, reason: string): string =>
|
|
250
|
+
[
|
|
251
|
+
`(You are taking over this work from another coding agent ('${from}'), which had to stop:`,
|
|
252
|
+
`${reason}. Its changes so far are in the working tree: inspect them (git status, git diff)`,
|
|
253
|
+
"before continuing, and carry on from there.)"
|
|
254
|
+
].join(" ")
|
|
255
|
+
|
|
256
|
+
interface Held {
|
|
257
|
+
readonly lease: Lease
|
|
258
|
+
readonly seat: RosterSeat
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The coder of one context (a story, or a flow's root): one executor held
|
|
263
|
+
* for the context's lifetime. When a call fails in a way that takes that
|
|
264
|
+
* executor out of the round (a usage limit, a dead serving engine), the
|
|
265
|
+
* lease is released, the next coder leased, and the call repeated there
|
|
266
|
+
* with a takeover note — the chat history the flow replays, the task
|
|
267
|
+
* checkpoint and the working tree carry the work across.
|
|
268
|
+
*/
|
|
269
|
+
export const makeHeldCoder = Effect.fn("@llm4ts/flow/RosterSeats.heldCoder")(function* (
|
|
270
|
+
roster: RosterShape,
|
|
271
|
+
source: SeatSource,
|
|
272
|
+
workDir: string,
|
|
273
|
+
options: HeldCoderOptions
|
|
274
|
+
): Effect.fn.Return<HeldCoder, FlowError, Scope.Scope> {
|
|
275
|
+
const scope = yield* Effect.scope
|
|
276
|
+
const current = yield* Ref.make<Held | undefined>(undefined)
|
|
277
|
+
const history = yield* Ref.make<ReadonlyArray<string>>([])
|
|
278
|
+
const handovers = yield* Ref.make(0)
|
|
279
|
+
const prefer = yield* Ref.make(options.prefer)
|
|
280
|
+
const gate = yield* Semaphore.make(1)
|
|
281
|
+
const maxHandovers = options.maxHandovers ?? 2
|
|
282
|
+
|
|
283
|
+
const acquire: Effect.Effect<Held, FlowError> = gate.withPermit(
|
|
284
|
+
Effect.gen(function* () {
|
|
285
|
+
const held = yield* Ref.get(current)
|
|
286
|
+
if (held !== undefined) {
|
|
287
|
+
return held
|
|
288
|
+
}
|
|
289
|
+
const preferred = yield* Ref.get(prefer)
|
|
290
|
+
const lease = yield* roster
|
|
291
|
+
.lease("coder", {
|
|
292
|
+
...(preferred === undefined ? {} : { prefer: preferred }),
|
|
293
|
+
...(options.label === undefined ? {} : { label: options.label })
|
|
294
|
+
})
|
|
295
|
+
.pipe(Scope.provide(scope))
|
|
296
|
+
const seat = yield* source.seatFor(lease.executor, "coder", workDir)
|
|
297
|
+
const next = { lease, seat }
|
|
298
|
+
yield* Ref.set(current, next)
|
|
299
|
+
yield* Ref.update(history, (ids) =>
|
|
300
|
+
ids.at(-1) === lease.executor.id ? ids : [...ids, lease.executor.id]
|
|
301
|
+
)
|
|
302
|
+
return next
|
|
303
|
+
})
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if (options.eager) {
|
|
307
|
+
yield* acquire
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Decides whether a failure hands over; returns the takeover note, or fails with the error. */
|
|
311
|
+
const handover = (held: Held, error: LlmError): Effect.Effect<string, LlmError> =>
|
|
312
|
+
Effect.gen(function* () {
|
|
313
|
+
const exclusion = yield* roster.report(held.lease.executor.id, error)
|
|
314
|
+
if (exclusion === undefined || (yield* Ref.get(handovers)) >= maxHandovers) {
|
|
315
|
+
return yield* Effect.fail(error)
|
|
316
|
+
}
|
|
317
|
+
yield* Ref.update(handovers, (count) => count + 1)
|
|
318
|
+
yield* Ref.set(prefer, undefined)
|
|
319
|
+
yield* held.lease.release
|
|
320
|
+
yield* Ref.set(current, undefined)
|
|
321
|
+
yield* say(
|
|
322
|
+
options.events,
|
|
323
|
+
`${options.label ?? workDir}: handing the coder over from ${held.lease.executor.id} (${describeExclusion(exclusion)})`
|
|
324
|
+
)
|
|
325
|
+
return takeoverNote(held.lease.executor.id, exclusion.reason)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
const run = <A, R>(
|
|
329
|
+
use: (seat: RosterSeat, note: string | undefined) => Effect.Effect<A, LlmError, R>,
|
|
330
|
+
note?: string
|
|
331
|
+
): Effect.Effect<A, LlmError, R> =>
|
|
332
|
+
Effect.gen(function* () {
|
|
333
|
+
const held = yield* acquire.pipe(Effect.mapError(asLlmError))
|
|
334
|
+
return yield* use(held.seat, note).pipe(
|
|
335
|
+
Effect.catch((error) => Effect.flatMap(handover(held, error), (next) => run(use, next)))
|
|
336
|
+
)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
const stream = (
|
|
340
|
+
use: (seat: RosterSeat, note: string | undefined) => Stream.Stream<LlmChunk, LlmError>,
|
|
341
|
+
note?: string
|
|
342
|
+
): Stream.Stream<LlmChunk, LlmError> =>
|
|
343
|
+
Stream.unwrap(
|
|
344
|
+
Effect.map(acquire.pipe(Effect.mapError(asLlmError)), (held) =>
|
|
345
|
+
use(held.seat, note).pipe(
|
|
346
|
+
Stream.catchIf(
|
|
347
|
+
() => true,
|
|
348
|
+
(error) => Stream.unwrap(Effect.map(handover(held, error), (next) => stream(use, next)))
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
service: serviceOver({ run, stream, isAvailable: roster.canEverServe("coder") }),
|
|
356
|
+
executor: Effect.map(Ref.get(current), (held) => held?.lease.executor.id),
|
|
357
|
+
lease: Effect.map(Ref.get(current), (held) => held?.lease.executor),
|
|
358
|
+
history: Ref.get(history)
|
|
359
|
+
}
|
|
360
|
+
})
|