@llm4ts/flow 2.4.2 → 2.6.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/Classified.d.ts +2 -0
- package/dist/Classified.d.ts.map +1 -1
- package/dist/Classified.js +4 -0
- package/dist/Classified.js.map +1 -1
- package/dist/CostReport.d.ts +110 -0
- package/dist/CostReport.d.ts.map +1 -0
- package/dist/CostReport.js +313 -0
- package/dist/CostReport.js.map +1 -0
- package/dist/EstimatedUsage.d.ts.map +1 -1
- package/dist/EstimatedUsage.js +4 -0
- package/dist/EstimatedUsage.js.map +1 -1
- package/dist/Flow.d.ts +14 -0
- package/dist/Flow.d.ts.map +1 -1
- package/dist/Flow.js +45 -2
- package/dist/Flow.js.map +1 -1
- package/dist/FlowContext.d.ts +7 -0
- package/dist/FlowContext.d.ts.map +1 -1
- package/dist/FlowContext.js.map +1 -1
- package/dist/FlowEvents.d.ts +44 -1
- package/dist/FlowEvents.d.ts.map +1 -1
- package/dist/FlowEvents.js +35 -0
- package/dist/FlowEvents.js.map +1 -1
- package/dist/GitTool.d.ts +7 -0
- package/dist/GitTool.d.ts.map +1 -1
- package/dist/GitTool.js +15 -1
- package/dist/GitTool.js.map +1 -1
- package/dist/Judgment.d.ts +88 -0
- package/dist/Judgment.d.ts.map +1 -0
- package/dist/Judgment.js +281 -0
- package/dist/Judgment.js.map +1 -0
- package/dist/JudgmentDataset.d.ts +79 -0
- package/dist/JudgmentDataset.d.ts.map +1 -0
- package/dist/JudgmentDataset.js +164 -0
- package/dist/JudgmentDataset.js.map +1 -0
- package/dist/JudgmentEval.d.ts +143 -0
- package/dist/JudgmentEval.d.ts.map +1 -0
- package/dist/JudgmentEval.js +242 -0
- package/dist/JudgmentEval.js.map +1 -0
- package/dist/JudgmentLog.d.ts +46 -0
- package/dist/JudgmentLog.d.ts.map +1 -0
- package/dist/JudgmentLog.js +62 -0
- package/dist/JudgmentLog.js.map +1 -0
- package/dist/JudgmentTypes.d.ts +13 -0
- package/dist/JudgmentTypes.d.ts.map +1 -0
- package/dist/JudgmentTypes.js +11 -0
- package/dist/JudgmentTypes.js.map +1 -0
- package/dist/ProgramJudge.d.ts +15 -2
- package/dist/ProgramJudge.d.ts.map +1 -1
- package/dist/ProgramJudge.js +58 -3
- package/dist/ProgramJudge.js.map +1 -1
- package/dist/Replay.d.ts.map +1 -1
- package/dist/Replay.js +9 -4
- package/dist/Replay.js.map +1 -1
- package/dist/Review.d.ts +35 -0
- package/dist/Review.d.ts.map +1 -1
- package/dist/Review.js +92 -13
- package/dist/Review.js.map +1 -1
- package/dist/Reviewer.d.ts +8 -0
- package/dist/Reviewer.d.ts.map +1 -1
- package/dist/Reviewer.js +12 -1
- package/dist/Reviewer.js.map +1 -1
- package/dist/TransientRetry.d.ts.map +1 -1
- package/dist/TransientRetry.js +1 -0
- package/dist/TransientRetry.js.map +1 -1
- package/package.json +8 -3
- package/src/Classified.ts +5 -0
- package/src/CostReport.ts +402 -0
- package/src/EstimatedUsage.ts +15 -0
- package/src/Flow.ts +77 -2
- package/src/FlowContext.ts +7 -0
- package/src/FlowEvents.ts +42 -0
- package/src/GitTool.ts +19 -1
- package/src/Judgment.ts +418 -0
- package/src/JudgmentDataset.ts +254 -0
- package/src/JudgmentEval.ts +306 -0
- package/src/JudgmentLog.ts +96 -0
- package/src/JudgmentTypes.ts +15 -0
- package/src/ProgramJudge.ts +89 -4
- package/src/Replay.ts +23 -17
- package/src/Review.ts +163 -13
- package/src/Reviewer.ts +15 -1
- package/src/TransientRetry.ts +1 -0
package/src/Review.ts
CHANGED
|
@@ -8,9 +8,24 @@ import { Capabilities } from "@llm4ts/core/Capability"
|
|
|
8
8
|
import { guarded } from "./CapabilityGuard.ts"
|
|
9
9
|
import type { Chat } from "./Chat.ts"
|
|
10
10
|
import { FlowLlmError, ProcessError, describeFlowError, type FlowError } from "./FlowError.ts"
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Info,
|
|
13
|
+
JudgmentObserved,
|
|
14
|
+
publishJudgmentObserved,
|
|
15
|
+
type FlowEventsShape
|
|
16
|
+
} from "./FlowEvents.ts"
|
|
12
17
|
import { Reviewer } from "./Reviewer.ts"
|
|
13
18
|
import { publishUsage } from "./Usage.ts"
|
|
19
|
+
import type { JudgmentShape } from "@llm4ts/core/judgment/Judgment"
|
|
20
|
+
import { truth, type JudgmentResult, type TruthAnswer } from "@llm4ts/core/judgment/Schemas"
|
|
21
|
+
import {
|
|
22
|
+
certaintyOf,
|
|
23
|
+
decide,
|
|
24
|
+
defaultJudgmentPolicy,
|
|
25
|
+
type Decision,
|
|
26
|
+
type JudgmentMode,
|
|
27
|
+
type JudgmentPolicy
|
|
28
|
+
} from "./Judgment.ts"
|
|
14
29
|
|
|
15
30
|
export const Severity = Schema.Literals(["Critical", "Warning", "Info"])
|
|
16
31
|
export type Severity = typeof Severity.Type
|
|
@@ -67,8 +82,8 @@ export const reviewJsonSchema: JsonSchema = {
|
|
|
67
82
|
required: ["issues", "summary"]
|
|
68
83
|
}
|
|
69
84
|
|
|
70
|
-
const reviewer = (name: string, systemPrompt: string, files = ".*"): Reviewer =>
|
|
71
|
-
Reviewer.make({ name, systemPrompt, files })
|
|
85
|
+
const reviewer = (name: string, systemPrompt: string, screen: string, files = ".*"): Reviewer =>
|
|
86
|
+
Reviewer.make({ name, systemPrompt, files, screen })
|
|
72
87
|
|
|
73
88
|
export const correctnessReviewer = reviewer(
|
|
74
89
|
"code-functionality",
|
|
@@ -76,7 +91,8 @@ export const correctnessReviewer = reviewer(
|
|
|
76
91
|
"Review for functional correctness. Check that the change implements the task's intent,",
|
|
77
92
|
"handles obvious edge cases, and does not regress existing behavior. Report only concrete",
|
|
78
93
|
"logic errors, wrong conditions, mishandled error paths, and missing cases. Ignore style."
|
|
79
|
-
].join("\n")
|
|
94
|
+
].join("\n"),
|
|
95
|
+
"The diff plausibly contains a logic error, a wrong condition, a mishandled error path, or a missing case."
|
|
80
96
|
)
|
|
81
97
|
|
|
82
98
|
export const readabilityReviewer = reviewer(
|
|
@@ -84,7 +100,8 @@ export const readabilityReviewer = reviewer(
|
|
|
84
100
|
[
|
|
85
101
|
"Review for readability and clarity. Check names, focused functions, understandable control",
|
|
86
102
|
"flow, and useful comments. Report only concrete, actionable readability problems."
|
|
87
|
-
].join("\n")
|
|
103
|
+
].join("\n"),
|
|
104
|
+
"The diff plausibly introduces an unclear name, a tangled function, or confusing control flow."
|
|
88
105
|
)
|
|
89
106
|
|
|
90
107
|
export const testReviewer = reviewer(
|
|
@@ -92,27 +109,32 @@ export const testReviewer = reviewer(
|
|
|
92
109
|
[
|
|
93
110
|
"Review test coverage and quality. Check that new behavior and important error paths are",
|
|
94
111
|
"covered by tests that assert real outcomes and would fail on regression. Report concrete gaps."
|
|
95
|
-
].join("\n")
|
|
112
|
+
].join("\n"),
|
|
113
|
+
"The diff changes behavior that is not covered by a test in the same diff."
|
|
96
114
|
)
|
|
97
115
|
|
|
98
116
|
export const structureReviewer = reviewer(
|
|
99
117
|
"code-structure",
|
|
100
|
-
"Review module boundaries, dependency direction, cohesion, and unnecessary coupling. Report concrete structural problems."
|
|
118
|
+
"Review module boundaries, dependency direction, cohesion, and unnecessary coupling. Report concrete structural problems.",
|
|
119
|
+
"The diff plausibly crosses a module boundary, reverses a dependency direction, or adds coupling."
|
|
101
120
|
)
|
|
102
121
|
|
|
103
122
|
export const performanceReviewer = reviewer(
|
|
104
123
|
"performance",
|
|
105
|
-
"Review for material performance regressions, unbounded work, avoidable repeated I/O, and resource leaks."
|
|
124
|
+
"Review for material performance regressions, unbounded work, avoidable repeated I/O, and resource leaks.",
|
|
125
|
+
"The diff plausibly changes performance characteristics: unbounded work, repeated I/O, or a resource leak."
|
|
106
126
|
)
|
|
107
127
|
|
|
108
128
|
export const securityReviewer = reviewer(
|
|
109
129
|
"security",
|
|
110
|
-
"Review trust boundaries, input handling, secrets, authorization, injection risks, and unsafe defaults."
|
|
130
|
+
"Review trust boundaries, input handling, secrets, authorization, injection risks, and unsafe defaults.",
|
|
131
|
+
"The diff plausibly touches a trust boundary, secret, authorization check, input parser, or unsafe default."
|
|
111
132
|
)
|
|
112
133
|
|
|
113
134
|
export const effectReviewer = reviewer(
|
|
114
135
|
"effect-ts",
|
|
115
136
|
"Review Effect usage: typed errors, scoped resources, service boundaries, interruption, concurrency, and runtime ownership.",
|
|
137
|
+
"The diff plausibly misuses Effect: an untyped error, an unscoped resource, a leaked service, or unmanaged concurrency.",
|
|
116
138
|
".*\\.(ts|tsx|mts|cts)$"
|
|
117
139
|
)
|
|
118
140
|
|
|
@@ -277,9 +299,103 @@ export interface ReviewAndFixOptions {
|
|
|
277
299
|
readonly lint?: Effect.Effect<ReviewResult, FlowError>
|
|
278
300
|
readonly parallelism?: number
|
|
279
301
|
readonly format?: Effect.Effect<void, FlowError>
|
|
302
|
+
/**
|
|
303
|
+
* A judgment pre-screen (ADR 0017): one Truth question per lens over the
|
|
304
|
+
* diff. Observes by default, keeping every selected lens; only explicit
|
|
305
|
+
* act mode skips lenses. Omit to disable the judgment entirely.
|
|
306
|
+
*/
|
|
307
|
+
readonly prescreen?: ReviewPrescreen
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export interface ReviewPrescreen {
|
|
311
|
+
readonly judgment: JudgmentShape
|
|
312
|
+
readonly policy?: JudgmentPolicy
|
|
313
|
+
readonly mode?: JudgmentMode
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface ReviewPrescreenResult {
|
|
317
|
+
readonly reviewers: ReadonlyArray<Reviewer>
|
|
318
|
+
readonly observations: ReadonlyArray<{
|
|
319
|
+
readonly key: string
|
|
320
|
+
readonly answer: TruthAnswer
|
|
321
|
+
readonly question: ReturnType<typeof truth>
|
|
322
|
+
readonly state: { readonly diff: string }
|
|
323
|
+
readonly judgmentIdentity: string
|
|
324
|
+
readonly decision: Decision
|
|
325
|
+
}>
|
|
280
326
|
}
|
|
281
327
|
|
|
282
|
-
|
|
328
|
+
/**
|
|
329
|
+
* Selected lenses and their answers for publication after review. In act mode
|
|
330
|
+
* a lens is skipped only when its screen
|
|
331
|
+
* answered with `act` certainty that the diff has nothing for it; doubt,
|
|
332
|
+
* failure, and escalation all run the lens. Never skip on doubt.
|
|
333
|
+
*/
|
|
334
|
+
export const prescreenReviewers = Effect.fn("@llm4ts/flow/Review.prescreen")(function* (
|
|
335
|
+
prescreen: ReviewPrescreen,
|
|
336
|
+
events: FlowEventsShape,
|
|
337
|
+
diff: string,
|
|
338
|
+
lenses: ReadonlyArray<Reviewer>
|
|
339
|
+
): Effect.fn.Return<ReviewPrescreenResult, FlowError> {
|
|
340
|
+
if (lenses.length === 0) {
|
|
341
|
+
return { reviewers: lenses, observations: [] }
|
|
342
|
+
}
|
|
343
|
+
const policy = prescreen.policy ?? defaultJudgmentPolicy
|
|
344
|
+
const state = { diff }
|
|
345
|
+
const questions = Object.fromEntries(
|
|
346
|
+
lenses.map((lens) => [lens.name, truth(lens.screeningStatement)])
|
|
347
|
+
)
|
|
348
|
+
const result = yield* prescreen.judgment.judge({ state, questions }).pipe(
|
|
349
|
+
Effect.catch((error) =>
|
|
350
|
+
events
|
|
351
|
+
.publish(
|
|
352
|
+
Info.make({
|
|
353
|
+
message: `review pre-screen unavailable (${error.message}); running every lens`
|
|
354
|
+
})
|
|
355
|
+
)
|
|
356
|
+
.pipe(Effect.as<JudgmentResult | undefined>(undefined))
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
if (result === undefined) {
|
|
360
|
+
return { reviewers: lenses, observations: [] }
|
|
361
|
+
}
|
|
362
|
+
const observations = lenses.flatMap((lens) => {
|
|
363
|
+
const answer = result.answers[lens.name]
|
|
364
|
+
return answer?.type === "truth"
|
|
365
|
+
? [
|
|
366
|
+
{
|
|
367
|
+
key: lens.name,
|
|
368
|
+
answer,
|
|
369
|
+
decision: decide(answer, policy),
|
|
370
|
+
state,
|
|
371
|
+
question: questions[lens.name],
|
|
372
|
+
judgmentIdentity: prescreen.judgment.identity
|
|
373
|
+
}
|
|
374
|
+
]
|
|
375
|
+
: []
|
|
376
|
+
})
|
|
377
|
+
const kept =
|
|
378
|
+
prescreen.mode !== "act"
|
|
379
|
+
? lenses
|
|
380
|
+
: lenses.filter(
|
|
381
|
+
(lens) =>
|
|
382
|
+
!observations.some(
|
|
383
|
+
({ key, answer, decision }) =>
|
|
384
|
+
key === lens.name && answer.truth < 0.5 && decision === "act"
|
|
385
|
+
)
|
|
386
|
+
)
|
|
387
|
+
const skipped = lenses.filter((lens) => !kept.includes(lens))
|
|
388
|
+
if (skipped.length > 0) {
|
|
389
|
+
yield* events.publish(
|
|
390
|
+
Info.make({
|
|
391
|
+
message: `review pre-screen skipped ${skipped.map((lens) => lens.name).join(", ")}`
|
|
392
|
+
})
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
return { reviewers: kept, observations }
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
export const reviewWith = (
|
|
283
399
|
service: LlmServiceShape,
|
|
284
400
|
events: FlowEventsShape,
|
|
285
401
|
lens: Reviewer,
|
|
@@ -339,15 +455,49 @@ export const reviewAndFixLoop = Effect.fn("@llm4ts/flow/Review.reviewAndFixLoop"
|
|
|
339
455
|
}
|
|
340
456
|
const diff = yield* options.currentDiff
|
|
341
457
|
const files = yield* changedFiles
|
|
342
|
-
const
|
|
458
|
+
const selected = yield* selector.select(options.reviewers, files, round, previous)
|
|
459
|
+
const screened =
|
|
460
|
+
options.prescreen === undefined
|
|
461
|
+
? { reviewers: selected, observations: [] }
|
|
462
|
+
: yield* prescreenReviewers(options.prescreen, options.events, diff, selected)
|
|
463
|
+
const chosen = screened.reviewers
|
|
343
464
|
const run = (lens: Reviewer) =>
|
|
344
|
-
reviewWith(options.reviewerService, options.events, lens, options.taskTitle, diff)
|
|
465
|
+
reviewWith(options.reviewerService, options.events, lens, options.taskTitle, diff).pipe(
|
|
466
|
+
Effect.map((result) => ({ lens, result }))
|
|
467
|
+
)
|
|
345
468
|
const parallelism = options.parallelism ?? 0
|
|
346
469
|
const results =
|
|
347
470
|
parallelism > 0
|
|
348
471
|
? yield* Effect.forEach(chosen, run, { concurrency: parallelism })
|
|
349
472
|
: yield* Effect.forEach(chosen, run, { concurrency: "unbounded" })
|
|
350
|
-
|
|
473
|
+
const mode = options.prescreen?.mode ?? "observe"
|
|
474
|
+
if (mode !== "act") {
|
|
475
|
+
for (const { lens, result } of results) {
|
|
476
|
+
const observation = screened.observations.find(({ key }) => key === lens.name)
|
|
477
|
+
if (observation === undefined) continue
|
|
478
|
+
const { key, answer, decision, state, question, judgmentIdentity } = observation
|
|
479
|
+
const counts = { Critical: 0, Warning: 0, Info: 0 }
|
|
480
|
+
for (const issue of result.issues) counts[issue.severity] += 1
|
|
481
|
+
yield* publishJudgmentObserved(
|
|
482
|
+
options.events,
|
|
483
|
+
JudgmentObserved.make({
|
|
484
|
+
consumer: "review-prescreen",
|
|
485
|
+
state,
|
|
486
|
+
question,
|
|
487
|
+
answer,
|
|
488
|
+
judgmentIdentity,
|
|
489
|
+
key,
|
|
490
|
+
decision,
|
|
491
|
+
certainty: certaintyOf(answer),
|
|
492
|
+
support: answer.support,
|
|
493
|
+
origin: answer.origin,
|
|
494
|
+
outcome: { _tag: "ReviewPrescreen", lens: lens.name, issues: counts },
|
|
495
|
+
mode
|
|
496
|
+
})
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return mergeReviewResults(results.map(({ result }) => result))
|
|
351
501
|
})
|
|
352
502
|
|
|
353
503
|
const loop = (
|
package/src/Reviewer.ts
CHANGED
|
@@ -3,8 +3,22 @@ import * as Schema from "effect/Schema"
|
|
|
3
3
|
export class Reviewer extends Schema.Class<Reviewer>("Reviewer")({
|
|
4
4
|
name: Schema.String,
|
|
5
5
|
systemPrompt: Schema.String,
|
|
6
|
-
files: Schema.optionalKey(Schema.String)
|
|
6
|
+
files: Schema.optionalKey(Schema.String),
|
|
7
|
+
/**
|
|
8
|
+
* The yes/no statement a judgment pre-screen evaluates against a diff to
|
|
9
|
+
* decide whether this lens is worth a full pass (ADR 0017). Absent, one is
|
|
10
|
+
* derived from the system prompt.
|
|
11
|
+
*/
|
|
12
|
+
screen: Schema.optionalKey(Schema.String)
|
|
7
13
|
}) {
|
|
14
|
+
/** The screening statement, explicit or derived. */
|
|
15
|
+
get screeningStatement(): string {
|
|
16
|
+
return (
|
|
17
|
+
this.screen ??
|
|
18
|
+
`A reviewer following these instructions would report at least one concrete issue in the diff: ${this.systemPrompt}`
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
matches(changedFiles: ReadonlyArray<string>): boolean {
|
|
9
23
|
if (this.files === undefined || changedFiles.length === 0) {
|
|
10
24
|
return true
|