@llm4ts/flow 2.7.2 → 2.9.0
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/Chat.d.ts.map +1 -1
- package/dist/Chat.js +35 -4
- package/dist/Chat.js.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 +40 -1
- package/dist/FlowError.d.ts.map +1 -1
- package/dist/FlowError.js +58 -3
- package/dist/FlowError.js.map +1 -1
- package/dist/GitTool.d.ts +26 -2
- package/dist/GitTool.d.ts.map +1 -1
- package/dist/GitTool.js +54 -3
- package/dist/GitTool.js.map +1 -1
- package/dist/Judgment.d.ts +2 -2
- package/dist/Judgment.d.ts.map +1 -1
- package/dist/Perimeter.d.ts +22 -1
- package/dist/Perimeter.d.ts.map +1 -1
- package/dist/Perimeter.js +57 -1
- package/dist/Perimeter.js.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 +163 -0
- package/dist/Roster.d.ts.map +1 -0
- package/dist/Roster.js +560 -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 +144 -0
- package/dist/RosterSeats.js.map +1 -0
- package/dist/Stories.d.ts +54 -8
- package/dist/Stories.d.ts.map +1 -1
- package/dist/Stories.js +413 -51
- package/dist/Stories.js.map +1 -1
- package/dist/StoryPlan.d.ts +15 -0
- package/dist/StoryPlan.d.ts.map +1 -1
- package/dist/StoryPlan.js +58 -0
- package/dist/StoryPlan.js.map +1 -1
- package/dist/TransientRetry.d.ts +9 -0
- package/dist/TransientRetry.d.ts.map +1 -1
- package/dist/TransientRetry.js +43 -8
- package/dist/TransientRetry.js.map +1 -1
- package/package.json +4 -2
- package/src/Chat.ts +45 -6
- package/src/FlowContext.ts +19 -1
- package/src/FlowError.ts +65 -3
- package/src/GitTool.ts +99 -5
- package/src/Perimeter.ts +85 -1
- package/src/Roster.ts +824 -0
- package/src/RosterSeats.ts +306 -0
- package/src/Stories.ts +588 -88
- package/src/StoryPlan.ts +73 -0
- package/src/TransientRetry.ts +97 -8
package/src/StoryPlan.ts
CHANGED
|
@@ -216,6 +216,79 @@ export const dependentsOf = (plan: StoryPlan, id: string): ReadonlyArray<string>
|
|
|
216
216
|
.filter((candidate) => candidate !== id && blocked.has(candidate))
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/** Every story `id` waits for, directly or through another dependency. */
|
|
220
|
+
export const dependenciesOf = (plan: StoryPlan, id: string): ReadonlyArray<string> => {
|
|
221
|
+
const needed = new Set<string>()
|
|
222
|
+
const pending = [...(plan.story(id)?.dependsOn ?? [])]
|
|
223
|
+
while (pending.length > 0) {
|
|
224
|
+
const next = pending.pop()
|
|
225
|
+
if (next === undefined || needed.has(next)) {
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
needed.add(next)
|
|
229
|
+
pending.push(...(plan.story(next)?.dependsOn ?? []))
|
|
230
|
+
}
|
|
231
|
+
return plan.stories.map((story) => story.id).filter((candidate) => needed.has(candidate))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** How `other` stands to `story` in the plan's order. */
|
|
235
|
+
export type StoryRelation = "self" | "dependency" | "dependent" | "parallel"
|
|
236
|
+
|
|
237
|
+
export const relationOf = (plan: StoryPlan, story: Story, other: Story): StoryRelation =>
|
|
238
|
+
other.id === story.id
|
|
239
|
+
? "self"
|
|
240
|
+
: dependenciesOf(plan, story.id).includes(other.id)
|
|
241
|
+
? "dependency"
|
|
242
|
+
: dependentsOf(plan, story.id).includes(other.id)
|
|
243
|
+
? "dependent"
|
|
244
|
+
: "parallel"
|
|
245
|
+
|
|
246
|
+
/** The story that owns `path`, if any (owned sets are disjoint). */
|
|
247
|
+
export const ownerOf = (plan: StoryPlan, path: string): Story | undefined =>
|
|
248
|
+
plan.stories.find((story) => story.owned.some((prefix) => pathWithin(path, prefix)))
|
|
249
|
+
|
|
250
|
+
const fileLike = /\.[A-Za-z0-9]{1,5}$/
|
|
251
|
+
|
|
252
|
+
const basename = (path: string): string => normalizePath(path).split("/").at(-1) ?? ""
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The repository paths a piece of prose names: tokens that start under one
|
|
256
|
+
* of the plan's top-level directories (`src/App.tsx`), plus a bare file name
|
|
257
|
+
* that is exactly the file name of an owned path (`App.tsx`), which resolves
|
|
258
|
+
* to that path. Prose paths are guesses, never proofs — callers use them to
|
|
259
|
+
* ask again or to explain, not to fail.
|
|
260
|
+
*/
|
|
261
|
+
export const pathsNamedIn = (plan: StoryPlan, text: string): ReadonlyArray<string> => {
|
|
262
|
+
const known = plan.stories.flatMap((story) => [...story.owned, ...story.sharedReadOnly])
|
|
263
|
+
const roots = new Set(
|
|
264
|
+
known.map((path) => normalizePath(path).split("/")[0] ?? "").filter((root) => root.length > 0)
|
|
265
|
+
)
|
|
266
|
+
const ownedFiles = plan.stories.flatMap((story) =>
|
|
267
|
+
story.owned.filter((path) => fileLike.test(normalizePath(path)))
|
|
268
|
+
)
|
|
269
|
+
const found: Array<string> = []
|
|
270
|
+
for (const raw of text.match(/[A-Za-z0-9_.@/\\-]+/g) ?? []) {
|
|
271
|
+
const token = normalizePath(raw.replace(/[.,:;]+$/, ""))
|
|
272
|
+
if (token.length === 0 || token.includes("://")) {
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
if (token.includes("/")) {
|
|
276
|
+
if (roots.has(token.split("/")[0] ?? "")) {
|
|
277
|
+
found.push(token)
|
|
278
|
+
}
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
if (fileLike.test(token)) {
|
|
282
|
+
for (const path of ownedFiles) {
|
|
283
|
+
if (basename(path) === token) {
|
|
284
|
+
found.push(normalizePath(path))
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return [...new Set(found)]
|
|
290
|
+
}
|
|
291
|
+
|
|
219
292
|
/** Stable over the story entry's content — a changed entry means a fresh branch. */
|
|
220
293
|
export const storyHash = (story: Story): string =>
|
|
221
294
|
stableHash(
|
package/src/TransientRetry.ts
CHANGED
|
@@ -25,7 +25,27 @@ const transientSignals: ReadonlyArray<string> = [
|
|
|
25
25
|
" 500",
|
|
26
26
|
" 502",
|
|
27
27
|
" 503",
|
|
28
|
-
" 504"
|
|
28
|
+
" 504",
|
|
29
|
+
// A local single-model server (LM Studio) under parallel load: a request
|
|
30
|
+
// queued behind another generation is cut off ("terminated"), or a stream
|
|
31
|
+
// dies mid-way. Both recover on a prompt retry.
|
|
32
|
+
"terminated",
|
|
33
|
+
"stream disconnected"
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
// The serving engine itself is down: a local server restarting after a GPU
|
|
37
|
+
// backend crash, or unable to (re)load its model. It comes back, but in
|
|
38
|
+
// minutes, not seconds — a one-second backoff spends every retry while the
|
|
39
|
+
// engine is still recovering, so these get their own, slower budget.
|
|
40
|
+
const outageSignals: ReadonlyArray<string> = [
|
|
41
|
+
"engine is recovering",
|
|
42
|
+
"engine_recovering",
|
|
43
|
+
"backend is unhealthy",
|
|
44
|
+
"runtime_unavailable",
|
|
45
|
+
"server is shutting down",
|
|
46
|
+
"server_shutdown",
|
|
47
|
+
"failed to load model",
|
|
48
|
+
"engine protocol startup was aborted"
|
|
29
49
|
]
|
|
30
50
|
|
|
31
51
|
// An "empty response" is ambiguous: gemini returns one both for a random mid-stream
|
|
@@ -55,6 +75,8 @@ const deterministic4xxSignals: ReadonlyArray<string> = [
|
|
|
55
75
|
// message string when no typed cause survived. Two copies of this list would drift.
|
|
56
76
|
const contextOverflowSignals: ReadonlyArray<string> = [
|
|
57
77
|
"exceeds the maximum number of tokens",
|
|
78
|
+
"exceeds the context window",
|
|
79
|
+
"context_length_exceeded",
|
|
58
80
|
"input token count exceeds",
|
|
59
81
|
"context length exceeded",
|
|
60
82
|
"maximum context length",
|
|
@@ -88,6 +110,12 @@ export const isContextOverflowMessage = (message: string): boolean =>
|
|
|
88
110
|
export const isContextOverflow = (error: LlmError): boolean =>
|
|
89
111
|
error._tag === "ProviderError" && isContextOverflowMessage(error.message)
|
|
90
112
|
|
|
113
|
+
/** The serving engine is down or restarting (see `outageSignals`). */
|
|
114
|
+
export const isOutageMessage = (message: string): boolean => includesSignal(message, outageSignals)
|
|
115
|
+
|
|
116
|
+
export const isOutage = (error: LlmError): boolean =>
|
|
117
|
+
error._tag === "ProviderError" && !isContextOverflow(error) && isOutageMessage(error.message)
|
|
118
|
+
|
|
91
119
|
export const isTransient = (error: LlmError): boolean => {
|
|
92
120
|
switch (error._tag) {
|
|
93
121
|
case "TimeoutError":
|
|
@@ -96,6 +124,7 @@ export const isTransient = (error: LlmError): boolean => {
|
|
|
96
124
|
case "ProviderError":
|
|
97
125
|
return (
|
|
98
126
|
!isFlakyStream(error) &&
|
|
127
|
+
!isOutage(error) &&
|
|
99
128
|
!isDeterministic4xx(error.message) &&
|
|
100
129
|
includesSignal(error.message, transientSignals)
|
|
101
130
|
)
|
|
@@ -127,6 +156,12 @@ export interface TransientRetryOptions {
|
|
|
127
156
|
* time, so this budget is small (default 2) and independent of the others.
|
|
128
157
|
*/
|
|
129
158
|
readonly parseRetries?: number
|
|
159
|
+
/** Retries while the serving engine is down (`isOutage`). Default 5. */
|
|
160
|
+
readonly outageRetries?: number
|
|
161
|
+
/** First outage wait, doubled per attempt up to `outageMaxDelay`. Default 15 seconds. */
|
|
162
|
+
readonly outageDelay?: Duration.Input
|
|
163
|
+
/** Default 2 minutes: five attempts wait about six minutes in all. */
|
|
164
|
+
readonly outageMaxDelay?: Duration.Input
|
|
130
165
|
}
|
|
131
166
|
|
|
132
167
|
interface ResolvedTransientRetryOptions {
|
|
@@ -135,6 +170,9 @@ interface ResolvedTransientRetryOptions {
|
|
|
135
170
|
readonly flakyRetries: number
|
|
136
171
|
readonly flakyDelay: Duration.Duration
|
|
137
172
|
readonly parseRetries: number
|
|
173
|
+
readonly outageRetries: number
|
|
174
|
+
readonly outageDelay: Duration.Duration
|
|
175
|
+
readonly outageMaxDelay: Duration.Duration
|
|
138
176
|
}
|
|
139
177
|
|
|
140
178
|
const resolveOptions = (options: TransientRetryOptions): ResolvedTransientRetryOptions => ({
|
|
@@ -142,9 +180,18 @@ const resolveOptions = (options: TransientRetryOptions): ResolvedTransientRetryO
|
|
|
142
180
|
baseDelay: Duration.fromInputUnsafe(options.baseDelay ?? "1 second"),
|
|
143
181
|
flakyRetries: options.flakyRetries ?? 6,
|
|
144
182
|
flakyDelay: Duration.fromInputUnsafe(options.flakyDelay ?? "1 second"),
|
|
145
|
-
parseRetries: options.parseRetries ?? 2
|
|
183
|
+
parseRetries: options.parseRetries ?? 2,
|
|
184
|
+
outageRetries: options.outageRetries ?? 5,
|
|
185
|
+
outageDelay: Duration.fromInputUnsafe(options.outageDelay ?? "15 seconds"),
|
|
186
|
+
outageMaxDelay: Duration.fromInputUnsafe(options.outageMaxDelay ?? "2 minutes")
|
|
146
187
|
})
|
|
147
188
|
|
|
189
|
+
const outageBackoff = (
|
|
190
|
+
options: ResolvedTransientRetryOptions,
|
|
191
|
+
attempt: number
|
|
192
|
+
): Duration.Duration =>
|
|
193
|
+
Duration.min(Duration.times(options.outageDelay, 2 ** attempt), options.outageMaxDelay)
|
|
194
|
+
|
|
148
195
|
/** A structured call whose text came back but did not decode as the requested JSON. */
|
|
149
196
|
export const isStructuredParseFailure = (error: LlmError): boolean => error._tag === "ParseError"
|
|
150
197
|
|
|
@@ -203,11 +250,30 @@ const retryEffect = <A, R>(
|
|
|
203
250
|
options: ResolvedTransientRetryOptions,
|
|
204
251
|
events: FlowEvents["Service"]
|
|
205
252
|
): Effect.Effect<A, LlmError, R> => {
|
|
206
|
-
const loop = (
|
|
253
|
+
const loop = (
|
|
254
|
+
transientAttempt: number,
|
|
255
|
+
flakyAttempt: number,
|
|
256
|
+
outageAttempt = 0
|
|
257
|
+
): Effect.Effect<A, LlmError, R> =>
|
|
207
258
|
Effect.catchIf(
|
|
208
259
|
effect,
|
|
209
260
|
() => true,
|
|
210
261
|
(error) => {
|
|
262
|
+
if (isOutage(error) && outageAttempt < options.outageRetries) {
|
|
263
|
+
return waitForRetry(
|
|
264
|
+
events,
|
|
265
|
+
`serving engine down (${what}) — waiting for it to recover`,
|
|
266
|
+
outageAttempt,
|
|
267
|
+
options.outageRetries,
|
|
268
|
+
error,
|
|
269
|
+
outageBackoff(options, outageAttempt)
|
|
270
|
+
).pipe(
|
|
271
|
+
Effect.andThen(
|
|
272
|
+
Effect.suspend(() => loop(transientAttempt, flakyAttempt, outageAttempt + 1))
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
211
277
|
if (isFlakyStream(error) && flakyAttempt < options.flakyRetries) {
|
|
212
278
|
return waitForRetry(
|
|
213
279
|
events,
|
|
@@ -216,7 +282,11 @@ const retryEffect = <A, R>(
|
|
|
216
282
|
options.flakyRetries,
|
|
217
283
|
error,
|
|
218
284
|
options.flakyDelay
|
|
219
|
-
).pipe(
|
|
285
|
+
).pipe(
|
|
286
|
+
Effect.andThen(
|
|
287
|
+
Effect.suspend(() => loop(transientAttempt, flakyAttempt + 1, outageAttempt))
|
|
288
|
+
)
|
|
289
|
+
)
|
|
220
290
|
}
|
|
221
291
|
|
|
222
292
|
if (isTransient(error) && transientAttempt < options.maxRetries) {
|
|
@@ -227,7 +297,11 @@ const retryEffect = <A, R>(
|
|
|
227
297
|
options.maxRetries,
|
|
228
298
|
error,
|
|
229
299
|
transientDelay(error, backoff(options.baseDelay, transientAttempt))
|
|
230
|
-
).pipe(
|
|
300
|
+
).pipe(
|
|
301
|
+
Effect.andThen(
|
|
302
|
+
Effect.suspend(() => loop(transientAttempt + 1, flakyAttempt, outageAttempt))
|
|
303
|
+
)
|
|
304
|
+
)
|
|
231
305
|
}
|
|
232
306
|
|
|
233
307
|
return Effect.fail(error)
|
|
@@ -251,12 +325,27 @@ const retryStream = <R>(
|
|
|
251
325
|
|
|
252
326
|
const loop = (
|
|
253
327
|
transientAttempt: number,
|
|
254
|
-
flakyAttempt: number
|
|
328
|
+
flakyAttempt: number,
|
|
329
|
+
outageAttempt = 0
|
|
255
330
|
): Stream.Stream<LlmChunk, LlmError, R> =>
|
|
256
331
|
Stream.catchIf(
|
|
257
332
|
stream,
|
|
258
333
|
() => true,
|
|
259
334
|
(error) => {
|
|
335
|
+
if (isOutage(error) && outageAttempt < options.outageRetries) {
|
|
336
|
+
return waitThenRetry(
|
|
337
|
+
waitForRetry(
|
|
338
|
+
events,
|
|
339
|
+
`serving engine down (${what}) — waiting for it to recover`,
|
|
340
|
+
outageAttempt,
|
|
341
|
+
options.outageRetries,
|
|
342
|
+
error,
|
|
343
|
+
outageBackoff(options, outageAttempt)
|
|
344
|
+
),
|
|
345
|
+
() => loop(transientAttempt, flakyAttempt, outageAttempt + 1)
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
260
349
|
if (isFlakyStream(error) && flakyAttempt < options.flakyRetries) {
|
|
261
350
|
return waitThenRetry(
|
|
262
351
|
waitForRetry(
|
|
@@ -267,7 +356,7 @@ const retryStream = <R>(
|
|
|
267
356
|
error,
|
|
268
357
|
options.flakyDelay
|
|
269
358
|
),
|
|
270
|
-
() => loop(transientAttempt, flakyAttempt + 1)
|
|
359
|
+
() => loop(transientAttempt, flakyAttempt + 1, outageAttempt)
|
|
271
360
|
)
|
|
272
361
|
}
|
|
273
362
|
|
|
@@ -281,7 +370,7 @@ const retryStream = <R>(
|
|
|
281
370
|
error,
|
|
282
371
|
transientDelay(error, backoff(options.baseDelay, transientAttempt))
|
|
283
372
|
),
|
|
284
|
-
() => loop(transientAttempt + 1, flakyAttempt)
|
|
373
|
+
() => loop(transientAttempt + 1, flakyAttempt, outageAttempt)
|
|
285
374
|
)
|
|
286
375
|
}
|
|
287
376
|
|