@llm4ts/flow 2.31.0 → 2.32.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 +5 -2
- package/dist/Chat.d.ts.map +1 -1
- package/dist/Chat.js +7 -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 +16 -1
- package/dist/Flow.d.ts.map +1 -1
- package/dist/Flow.js +29 -3
- package/dist/Flow.js.map +1 -1
- package/dist/FlowError.d.ts +15 -2
- package/dist/FlowError.d.ts.map +1 -1
- package/dist/FlowError.js +15 -0
- package/dist/FlowError.js.map +1 -1
- package/dist/FlowEvents.d.ts +18 -5
- package/dist/FlowEvents.d.ts.map +1 -1
- package/dist/FlowEvents.js +30 -9
- package/dist/FlowEvents.js.map +1 -1
- package/dist/Gates.d.ts +1 -1
- package/dist/Gates.d.ts.map +1 -1
- package/dist/Judgment.d.ts +2 -2
- package/dist/Judgment.d.ts.map +1 -1
- package/dist/Landing.d.ts +1 -1
- package/dist/Landing.d.ts.map +1 -1
- package/dist/NodePreflight.d.ts +1 -1
- package/dist/NodePreflight.d.ts.map +1 -1
- package/dist/Pack.d.ts +9 -1
- package/dist/Pack.d.ts.map +1 -1
- package/dist/Pack.js +26 -2
- package/dist/Pack.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/ProgramJudge.js +6 -3
- package/dist/ProgramJudge.js.map +1 -1
- package/dist/Replay.d.ts +2 -2
- package/dist/Replay.d.ts.map +1 -1
- package/dist/Retro.d.ts +51 -4
- package/dist/Retro.d.ts.map +1 -1
- package/dist/Retro.js +196 -11
- package/dist/Retro.js.map +1 -1
- package/dist/Review.d.ts +38 -6
- package/dist/Review.d.ts.map +1 -1
- package/dist/Review.js +147 -8
- package/dist/Review.js.map +1 -1
- package/dist/ReviewCache.d.ts +1 -1
- package/dist/ReviewCache.d.ts.map +1 -1
- package/dist/Reviewer.d.ts +2 -0
- package/dist/Reviewer.d.ts.map +1 -1
- package/dist/Reviewer.js +3 -1
- package/dist/Reviewer.js.map +1 -1
- package/dist/Roster.d.ts +19 -10
- package/dist/Roster.d.ts.map +1 -1
- package/dist/Roster.js +51 -17
- package/dist/Roster.js.map +1 -1
- package/dist/RosterSeats.d.ts +5 -4
- package/dist/RosterSeats.d.ts.map +1 -1
- package/dist/RosterSeats.js +7 -4
- package/dist/RosterSeats.js.map +1 -1
- package/dist/Stall.d.ts +29 -0
- package/dist/Stall.d.ts.map +1 -0
- package/dist/Stall.js +49 -0
- package/dist/Stall.js.map +1 -0
- package/dist/Stories.d.ts +8 -1
- package/dist/Stories.d.ts.map +1 -1
- package/dist/Stories.js +7 -1
- package/dist/Stories.js.map +1 -1
- package/package.json +3 -2
- package/src/Chat.ts +15 -7
- package/src/Flow.ts +48 -3
- package/src/FlowError.ts +16 -0
- package/src/FlowEvents.ts +34 -11
- package/src/Pack.ts +34 -1
- package/src/ProgramJudge.ts +3 -0
- package/src/Retro.ts +229 -4
- package/src/Review.ts +202 -8
- package/src/Reviewer.ts +3 -1
- package/src/Roster.ts +86 -39
- package/src/RosterSeats.ts +11 -6
- package/src/Stall.ts +85 -0
- package/src/Stories.ts +14 -0
package/src/Roster.ts
CHANGED
|
@@ -395,6 +395,8 @@ export interface Lease {
|
|
|
395
395
|
readonly release: Effect.Effect<void>
|
|
396
396
|
/** Took no slot: the context's own coder executor serves the call (see `LeaseOptions.borrow`). */
|
|
397
397
|
readonly borrowed: boolean
|
|
398
|
+
/** Set when the executor is the context's own coder (not independent), and why; see `ExecutorLeased.because`. */
|
|
399
|
+
readonly because?: "nobody" | "held" | "out"
|
|
398
400
|
}
|
|
399
401
|
|
|
400
402
|
export interface LeaseOptions {
|
|
@@ -405,15 +407,16 @@ export interface LeaseOptions {
|
|
|
405
407
|
/** Who is asking, for the events (a story's worktree). */
|
|
406
408
|
readonly label?: string
|
|
407
409
|
/**
|
|
408
|
-
* The executor holding this context's coder,
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
410
|
+
* The executor holding this context's coder, taken when independence
|
|
411
|
+
* cannot be had now: no executor outside `avoid` can ever take the role,
|
|
412
|
+
* every one that could is held by a coder, or every one is out of the
|
|
413
|
+
* round. A coder's slot frees only when its story ends (which may itself
|
|
414
|
+
* wait on a call like this one), and an exclusion may last hours; a
|
|
415
|
+
* reasoning call only ever waits for another call to end. The own
|
|
416
|
+
* executor is taken on a free slot of its own when it has one, else
|
|
417
|
+
* without a slot (its coder is idle while the context makes this call).
|
|
418
|
+
* Out of the round itself, it is still taken when nobody can ever serve
|
|
419
|
+
* (the call fails on it, as it would on anyone), and waited past otherwise.
|
|
417
420
|
*/
|
|
418
421
|
readonly borrow?: ExecutorSpec
|
|
419
422
|
}
|
|
@@ -445,6 +448,15 @@ export interface RosterShape {
|
|
|
445
448
|
* for `role` would then wait for a story to end, not for a call.
|
|
446
449
|
*/
|
|
447
450
|
readonly coderHeld: (role: Role, avoid?: ReadonlyArray<string>) => Effect.Effect<boolean>
|
|
451
|
+
/**
|
|
452
|
+
* Why a lease for `role` outside `avoid` cannot be served by a call
|
|
453
|
+
* ending: `held` (every executor in the round is held by coders), `out`
|
|
454
|
+
* (none is in the round), or `undefined` when one has a slot a call frees.
|
|
455
|
+
*/
|
|
456
|
+
readonly independenceBlocked: (
|
|
457
|
+
role: Role,
|
|
458
|
+
avoid?: ReadonlyArray<string>
|
|
459
|
+
) => Effect.Effect<"held" | "out" | undefined>
|
|
448
460
|
/** Free slots for `role` now, across executors in the round. */
|
|
449
461
|
readonly available: (role: Role) => Effect.Effect<number>
|
|
450
462
|
/** Configured slots for `role`. */
|
|
@@ -574,11 +586,13 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
574
586
|
const leaseOf = (
|
|
575
587
|
spec: ExecutorSpec,
|
|
576
588
|
role: Role,
|
|
577
|
-
label: string | undefined
|
|
589
|
+
label: string | undefined,
|
|
590
|
+
because?: "nobody" | "held" | "out"
|
|
578
591
|
): Effect.Effect<Lease, never, Scope.Scope> =>
|
|
579
592
|
Effect.gen(function* () {
|
|
580
593
|
const labelled = label === undefined ? {} : { label }
|
|
581
|
-
|
|
594
|
+
const own = because === undefined ? {} : { because }
|
|
595
|
+
yield* events.publish(ExecutorLeased.make({ executor: spec.id, role, ...labelled, ...own }))
|
|
582
596
|
const done = yield* Ref.make(false)
|
|
583
597
|
const free = Effect.flatMap(Ref.getAndSet(done, true), (was) =>
|
|
584
598
|
was
|
|
@@ -589,7 +603,7 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
589
603
|
)
|
|
590
604
|
)
|
|
591
605
|
yield* Effect.addFinalizer(() => free)
|
|
592
|
-
return { executor: spec, role, release: free, borrowed: false }
|
|
606
|
+
return { executor: spec, role, release: free, borrowed: false, ...own }
|
|
593
607
|
})
|
|
594
608
|
|
|
595
609
|
/** A lease over the context's own coder executor: no slot taken, none freed. */
|
|
@@ -597,7 +611,7 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
597
611
|
spec: ExecutorSpec,
|
|
598
612
|
role: Role,
|
|
599
613
|
label: string | undefined,
|
|
600
|
-
because: "nobody" | "held"
|
|
614
|
+
because: "nobody" | "held" | "out"
|
|
601
615
|
): Effect.Effect<Lease, never, Scope.Scope> =>
|
|
602
616
|
Effect.gen(function* () {
|
|
603
617
|
const labelled = label === undefined ? {} : { label }
|
|
@@ -611,7 +625,7 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
611
625
|
: events.publish(ExecutorReleased.make({ executor: spec.id, role, ...labelled }))
|
|
612
626
|
)
|
|
613
627
|
yield* Effect.addFinalizer(() => free)
|
|
614
|
-
return { executor: spec, role, release: free, borrowed: true }
|
|
628
|
+
return { executor: spec, role, release: free, borrowed: true, because }
|
|
615
629
|
})
|
|
616
630
|
|
|
617
631
|
/** One attempt: expire, pick, take the slot — all under the lock. */
|
|
@@ -660,18 +674,26 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
660
674
|
)
|
|
661
675
|
)
|
|
662
676
|
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
(spec) => (usage.get(spec.id) ?? { busy: 0, busyCoders: 0 }).busyCoders >= slotsOf(spec)
|
|
671
|
-
)
|
|
672
|
-
)
|
|
673
|
-
})
|
|
677
|
+
const blockedOutside = (role: Role, avoid: ReadonlyArray<string>): "held" | "out" | undefined => {
|
|
678
|
+
const inRound = eligible(role, avoid)
|
|
679
|
+
if (inRound.length === 0) {
|
|
680
|
+
return "out"
|
|
681
|
+
}
|
|
682
|
+
return inRound.every(
|
|
683
|
+
(spec) => (usage.get(spec.id) ?? { busy: 0, busyCoders: 0 }).busyCoders >= slotsOf(spec)
|
|
674
684
|
)
|
|
685
|
+
? "held"
|
|
686
|
+
: undefined
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const independenceBlocked = (
|
|
690
|
+
role: Role,
|
|
691
|
+
avoid: ReadonlyArray<string> = []
|
|
692
|
+
): Effect.Effect<"held" | "out" | undefined> =>
|
|
693
|
+
lock.withPermit(Effect.sync(() => blockedOutside(role, avoid)))
|
|
694
|
+
|
|
695
|
+
const coderHeld = (role: Role, avoid: ReadonlyArray<string> = []): Effect.Effect<boolean> =>
|
|
696
|
+
Effect.map(independenceBlocked(role, avoid), (blocked) => blocked === "held")
|
|
675
697
|
|
|
676
698
|
/** Probes every `health` exclusion whose next probe is due; a live one comes back. */
|
|
677
699
|
const probeDue: Effect.Effect<void> = Effect.gen(function* () {
|
|
@@ -771,13 +793,37 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
771
793
|
leaseOptions.borrow !== undefined && hasRole(leaseOptions.borrow, role)
|
|
772
794
|
? leaseOptions.borrow
|
|
773
795
|
: undefined
|
|
774
|
-
/**
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
796
|
+
/**
|
|
797
|
+
* Independence cannot be had now: the context's own executor, when in
|
|
798
|
+
* the round, takes the call — on a free slot of its own, else without
|
|
799
|
+
* one (its coder is idle while the context makes this call).
|
|
800
|
+
*/
|
|
801
|
+
const takeOwn = (
|
|
802
|
+
because: "nobody" | "held" | "out"
|
|
803
|
+
): Effect.Effect<Lease | undefined, never, Scope.Scope> =>
|
|
804
|
+
Effect.gen(function* () {
|
|
805
|
+
if (borrow === undefined) {
|
|
806
|
+
return undefined
|
|
807
|
+
}
|
|
808
|
+
// Out of the round itself, the own executor is still taken when
|
|
809
|
+
// nobody else can ever serve (the call fails on it, as on anyone).
|
|
810
|
+
const inRound = yield* lock.withPermit(Effect.sync(() => !exclusions.has(borrow.id)))
|
|
811
|
+
if (!inRound && because !== "nobody") {
|
|
812
|
+
return undefined
|
|
813
|
+
}
|
|
814
|
+
const own = yield* attempt(role, {
|
|
815
|
+
...leaseOptions,
|
|
816
|
+
avoid: avoid.filter((id) => id !== borrow.id),
|
|
817
|
+
prefer: borrow.id
|
|
818
|
+
})
|
|
819
|
+
yield* waited
|
|
820
|
+
return own !== undefined && own.id === borrow.id
|
|
821
|
+
? yield* leaseOf(own, role, leaseOptions.label, because)
|
|
822
|
+
: own !== undefined
|
|
823
|
+
? // Someone came back between the check and the pick: independent after all.
|
|
824
|
+
yield* leaseOf(own, role, leaseOptions.label)
|
|
825
|
+
: yield* borrowedLease(borrow, role, leaseOptions.label, because)
|
|
826
|
+
})
|
|
781
827
|
let announced = false
|
|
782
828
|
const asked = yield* Clock.currentTimeMillis
|
|
783
829
|
// A lease that had to wait says how long, for the story it is for.
|
|
@@ -800,9 +846,9 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
800
846
|
return yield* leaseOf(spec, role, leaseOptions.label)
|
|
801
847
|
}
|
|
802
848
|
if (!(yield* canEverServe(role, avoid))) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
return
|
|
849
|
+
const own = yield* takeOwn("nobody")
|
|
850
|
+
if (own !== undefined) {
|
|
851
|
+
return own
|
|
806
852
|
}
|
|
807
853
|
const reasons = yield* lock.withPermit(
|
|
808
854
|
Effect.sync(() =>
|
|
@@ -819,11 +865,11 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
819
865
|
reasons: reasons.length === 0 ? ["no executor in the roster takes this role"] : reasons
|
|
820
866
|
})
|
|
821
867
|
}
|
|
822
|
-
|
|
823
|
-
|
|
868
|
+
const blocked = yield* independenceBlocked(role, avoid)
|
|
869
|
+
if (blocked !== undefined) {
|
|
870
|
+
const own = yield* takeOwn(blocked)
|
|
824
871
|
if (own !== undefined) {
|
|
825
|
-
|
|
826
|
-
return yield* borrowedLease(own, role, leaseOptions.label, "held")
|
|
872
|
+
return own
|
|
827
873
|
}
|
|
828
874
|
}
|
|
829
875
|
if (!announced) {
|
|
@@ -956,6 +1002,7 @@ export const makeRoster = Effect.fn("@llm4ts/flow/Roster.make")(function* (
|
|
|
956
1002
|
tryLease,
|
|
957
1003
|
canEverServe,
|
|
958
1004
|
coderHeld,
|
|
1005
|
+
independenceBlocked,
|
|
959
1006
|
available,
|
|
960
1007
|
slots,
|
|
961
1008
|
report,
|
package/src/RosterSeats.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { describeFlowError, type FlowError } from "./FlowError.ts"
|
|
|
15
15
|
import { ExecutorHandedOver, type FlowEventsShape } from "./FlowEvents.ts"
|
|
16
16
|
import {
|
|
17
17
|
describeExclusion,
|
|
18
|
+
hasRole,
|
|
18
19
|
type ExecutorSpec,
|
|
19
20
|
type Lease,
|
|
20
21
|
type Role,
|
|
@@ -104,9 +105,10 @@ export interface RosterSeatOptions {
|
|
|
104
105
|
/** Executors this seat must not use: the context's coder, for independence. */
|
|
105
106
|
readonly avoid?: Effect.Effect<ReadonlyArray<string>>
|
|
106
107
|
/**
|
|
107
|
-
* The context's coder executor,
|
|
108
|
-
*
|
|
109
|
-
*
|
|
108
|
+
* The context's coder executor, taken when independence cannot be had —
|
|
109
|
+
* nobody outside `avoid` can ever serve the role, every executor that
|
|
110
|
+
* could is held by a coder, or out of the round — on a free slot of its
|
|
111
|
+
* own, or without one (`LeaseOptions.borrow`).
|
|
110
112
|
*/
|
|
111
113
|
readonly borrow?: Effect.Effect<ExecutorSpec | undefined>
|
|
112
114
|
/** Who is asking, for the events. */
|
|
@@ -150,8 +152,9 @@ export const rosterSeat = (
|
|
|
150
152
|
|
|
151
153
|
/**
|
|
152
154
|
* After a failure that took `executor` out of the round, whether another
|
|
153
|
-
* executor can still take the call
|
|
154
|
-
*
|
|
155
|
+
* executor can still take the call — an independent one, or the context's
|
|
156
|
+
* own: the call moves once, never twice — a second failure is the call's
|
|
157
|
+
* own, not the executor's.
|
|
155
158
|
*/
|
|
156
159
|
const retryElsewhere = (
|
|
157
160
|
executor: ExecutorSpec,
|
|
@@ -164,7 +167,9 @@ export const rosterSeat = (
|
|
|
164
167
|
return false
|
|
165
168
|
}
|
|
166
169
|
const avoid = options.avoid === undefined ? [] : yield* options.avoid
|
|
167
|
-
const
|
|
170
|
+
const borrow = options.borrow === undefined ? undefined : yield* options.borrow
|
|
171
|
+
const own = borrow !== undefined && borrow.id !== executor.id && hasRole(borrow, role)
|
|
172
|
+
const other = own || (yield* roster.canEverServe(role, [...avoid, executor.id]))
|
|
168
173
|
if (other) {
|
|
169
174
|
yield* options.events.publish(
|
|
170
175
|
ExecutorHandedOver.make({
|
package/src/Stall.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Stopping conditions the model does not control (ADR 0027 decision 11).
|
|
2
|
+
// Pure helpers over a chat's chunk stream; `Chat` applies them when asked.
|
|
3
|
+
import * as Duration from "effect/Duration"
|
|
4
|
+
import * as Effect from "effect/Effect"
|
|
5
|
+
import * as Ref from "effect/Ref"
|
|
6
|
+
import * as Stream from "effect/Stream"
|
|
7
|
+
import type { LlmError } from "@llm4ts/core/Errors"
|
|
8
|
+
import type { LlmChunk } from "@llm4ts/core/Models"
|
|
9
|
+
import { Stalled } from "./FlowError.ts"
|
|
10
|
+
|
|
11
|
+
export interface StallOptions {
|
|
12
|
+
/** The same tool call with the same arguments this many times in a row ends the turn. Default 5. */
|
|
13
|
+
readonly repeats?: number
|
|
14
|
+
/** No chunk for this long ends the turn; absent means never (a slow seat looks like silence). */
|
|
15
|
+
readonly silence?: Duration.Duration
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const defaultStallRepeats = 5
|
|
19
|
+
|
|
20
|
+
/** The identity of a tool call chunk: name and arguments; `undefined` for anything else. */
|
|
21
|
+
export const toolCallKey = (chunk: LlmChunk): string | undefined =>
|
|
22
|
+
chunk.metadata.event === "tool_use"
|
|
23
|
+
? `${chunk.metadata.tool_name ?? chunk.metadata.toolName ?? ""}|${chunk.metadata.tool_input ?? chunk.metadata.toolInput ?? ""}`
|
|
24
|
+
: undefined
|
|
25
|
+
|
|
26
|
+
export interface RepeatState {
|
|
27
|
+
readonly key: string | undefined
|
|
28
|
+
readonly count: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const noRepeats: RepeatState = { key: undefined, count: 0 }
|
|
32
|
+
|
|
33
|
+
/** The repeat state after one chunk: a different call resets, the same one counts. */
|
|
34
|
+
export const nextRepeat = (state: RepeatState, chunk: LlmChunk): RepeatState => {
|
|
35
|
+
const key = toolCallKey(chunk)
|
|
36
|
+
if (key === undefined) {
|
|
37
|
+
return state
|
|
38
|
+
}
|
|
39
|
+
return key === state.key ? { key, count: state.count + 1 } : { key, count: 1 }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The stream, ended with `Stalled` when one tool call repeats `repeats`
|
|
44
|
+
* times in a row (the state is shared across a chat's turns through `state`)
|
|
45
|
+
* or when no chunk arrives within `silence`.
|
|
46
|
+
*/
|
|
47
|
+
export const stallGuard = <R>(
|
|
48
|
+
stream: Stream.Stream<LlmChunk, LlmError, R>,
|
|
49
|
+
options: StallOptions,
|
|
50
|
+
state: Ref.Ref<RepeatState>
|
|
51
|
+
): Stream.Stream<LlmChunk, LlmError | Stalled, R> => {
|
|
52
|
+
const limit = Math.max(2, options.repeats ?? defaultStallRepeats)
|
|
53
|
+
const counted = Stream.mapEffect(stream, (chunk) =>
|
|
54
|
+
Effect.flatMap(
|
|
55
|
+
Ref.modify(state, (current) => {
|
|
56
|
+
const next = nextRepeat(current, chunk)
|
|
57
|
+
return [next, next] as const
|
|
58
|
+
}),
|
|
59
|
+
(next) =>
|
|
60
|
+
next.count >= limit
|
|
61
|
+
? Effect.fail(
|
|
62
|
+
Stalled.make({
|
|
63
|
+
signal: "repeated-tool-call",
|
|
64
|
+
detail: `the same tool call ${next.count} times in a row: ${(next.key ?? "").slice(0, 200)}`
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
: Effect.succeed(chunk)
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
const silence = options.silence
|
|
71
|
+
return silence === undefined
|
|
72
|
+
? counted
|
|
73
|
+
: counted.pipe(
|
|
74
|
+
Stream.timeoutOrElse({
|
|
75
|
+
duration: silence,
|
|
76
|
+
orElse: () =>
|
|
77
|
+
Stream.fail(
|
|
78
|
+
Stalled.make({
|
|
79
|
+
signal: "silence",
|
|
80
|
+
detail: `no output for ${Duration.format(silence)} (LLM4TS_STALL_MINUTES)`
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
}
|
package/src/Stories.ts
CHANGED
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
ReviewIssue,
|
|
73
73
|
ReviewResult,
|
|
74
74
|
applyTriage,
|
|
75
|
+
loadRepoReviewRules,
|
|
75
76
|
type FixPromptOptions,
|
|
76
77
|
type GateTriageOptions
|
|
77
78
|
} from "./Review.ts"
|
|
@@ -85,6 +86,7 @@ import { cachedValue, fingerprintOf } from "./ReviewCache.ts"
|
|
|
85
86
|
import { withContract, type ContractProfile } from "./AutonomyContract.ts"
|
|
86
87
|
import { fabricatedStatusIssues, unverifiedClaims } from "./Evidence.ts"
|
|
87
88
|
import type { Trailer } from "./CarriedNotes.ts"
|
|
89
|
+
import type { StallOptions } from "./Stall.ts"
|
|
88
90
|
import type { Reviewer } from "./Reviewer.ts"
|
|
89
91
|
import {
|
|
90
92
|
dependentsOf,
|
|
@@ -659,6 +661,12 @@ export interface StoriesOptions {
|
|
|
659
661
|
readonly fix?: FixPromptOptions
|
|
660
662
|
/** Test-file and marker patterns for the oracle guard; default rules when absent (ADR 0027). */
|
|
661
663
|
readonly oracleRules?: OracleRules
|
|
664
|
+
/** Independent votes of the adversarial lens per review round (ADR 0027 decision 7). Default 1. */
|
|
665
|
+
readonly votes?: number
|
|
666
|
+
/** Who applies review findings: the implementer's chat (default) or a separate fixer (ADR 0027 decision 8). */
|
|
667
|
+
readonly fixer?: "coder" | "separate"
|
|
668
|
+
/** End a coder turn that repeats one tool call or goes silent (ADR 0027 decision 11). */
|
|
669
|
+
readonly stall?: StallOptions
|
|
662
670
|
/**
|
|
663
671
|
* The autonomy contract profile for the coder's system prompt (ADR 0027
|
|
664
672
|
* decision 5). Default: the roster executor's `contract`, else `full`.
|
|
@@ -1447,6 +1455,12 @@ export const implementStoriesFlow = Effect.fn("@llm4ts/flow/Stories.implement")(
|
|
|
1447
1455
|
result
|
|
1448
1456
|
),
|
|
1449
1457
|
onTaskReply: (task, _reply, trailer, startedAt) => checkEvidence(task, trailer, startedAt),
|
|
1458
|
+
// The repository's own review rules ride along as one extra lens
|
|
1459
|
+
// (ADR 0027 decision 9), read from the epic checkout.
|
|
1460
|
+
repoRules: loadRepoReviewRules(files, context.workDir),
|
|
1461
|
+
...(options.votes === undefined ? {} : { votes: options.votes }),
|
|
1462
|
+
...(options.fixer === undefined ? {} : { fixer: options.fixer }),
|
|
1463
|
+
...(options.stall === undefined ? {} : { stall: options.stall }),
|
|
1450
1464
|
...(options.reviewers === undefined ? {} : { reviewers: options.reviewers }),
|
|
1451
1465
|
...(options.maxRounds === undefined ? {} : { maxRounds: options.maxRounds })
|
|
1452
1466
|
})
|