@effect-agent/pr-review 0.1.0-beta.21 → 0.1.0-beta.23
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/README.md +63 -46
- package/dist/action.d.mts +13 -13
- package/dist/action.mjs +33 -25
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{fan-out-D5xrmadQ.d.mts → fan-out-BJBTAYuh.d.mts} +278 -303
- package/dist/{github-DSqZp3Ce.mjs → github-BbwYzNrC.mjs} +588 -243
- package/dist/github-BbwYzNrC.mjs.map +1 -0
- package/dist/index.d.mts +72 -116
- package/dist/index.mjs +11 -11
- package/dist/index.mjs.map +1 -1
- package/dist/{providers-CblG1G9b.mjs → providers-NyP-4rS6.mjs} +162 -578
- package/dist/providers-NyP-4rS6.mjs.map +1 -0
- package/dist/testing.d.mts +4 -15
- package/dist/testing.mjs +12 -41
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/src/action.ts +40 -34
- package/src/internal/action-entry.ts +0 -1
- package/src/internal/coverage.ts +147 -532
- package/src/internal/factory.ts +29 -50
- package/src/internal/fan-out-scripted.ts +26 -80
- package/src/internal/fan-out.ts +577 -426
- package/src/internal/profiles.ts +8 -8
- package/src/internal/render.ts +34 -45
- package/src/internal/retirement.ts +3 -1
- package/src/internal/review-agent.ts +6 -1
- package/src/internal/review-state.ts +43 -19
- package/src/internal/review-units.ts +25 -0
- package/src/internal/run.ts +211 -174
- package/src/internal/source.ts +1 -1
- package/dist/github-DSqZp3Ce.mjs.map +0 -1
- package/dist/providers-CblG1G9b.mjs.map +0 -1
package/src/internal/factory.ts
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
import { Effect, Layer } from "effect";
|
|
1
|
+
import { Effect, Layer, Schema } from "effect";
|
|
2
2
|
import {
|
|
3
3
|
Agent,
|
|
4
4
|
AgentPolicy,
|
|
5
5
|
getToolExecutionClass,
|
|
6
6
|
IdGenerator,
|
|
7
|
-
SubagentReservationsMemoryLive,
|
|
8
7
|
type AgentPolicyInput,
|
|
9
8
|
type UsageBudgetLimits,
|
|
10
9
|
} from "effect-agent";
|
|
11
10
|
import { Toolkit, type LanguageModel, type Model, type Tool } from "effect/unstable/ai";
|
|
12
11
|
|
|
13
12
|
import type { ChangedFile } from "./diff.ts";
|
|
14
|
-
import {
|
|
15
|
-
fanOutHandlersLayerFor,
|
|
16
|
-
FanOutCoordinatorToolkitLayer,
|
|
17
|
-
FileReviewToolkitLayer,
|
|
18
|
-
makeFanOutReviewSuite,
|
|
19
|
-
} from "./fan-out.ts";
|
|
13
|
+
import { makeFileReviewerDefinition } from "./fan-out.ts";
|
|
20
14
|
import { computeChangesetFingerprint } from "./fingerprint.ts";
|
|
21
15
|
import { compileIgnoreGlobs, ignoringPullRequestSourceLayer } from "./ignore.ts";
|
|
22
16
|
import {
|
|
@@ -35,6 +29,7 @@ import {
|
|
|
35
29
|
import { buildProfileMission, computeProfileFingerprint } from "./review-state.ts";
|
|
36
30
|
import {
|
|
37
31
|
buildReviewMission,
|
|
32
|
+
executeFanOutReview,
|
|
38
33
|
executeReview,
|
|
39
34
|
fanOutReviewBudgetLimits,
|
|
40
35
|
reviewBudgetLimits,
|
|
@@ -244,8 +239,6 @@ const make = <
|
|
|
244
239
|
signature,
|
|
245
240
|
modelLabel: options.modelLabel,
|
|
246
241
|
runUrl: runOptions.runUrl,
|
|
247
|
-
usageScope: "run",
|
|
248
|
-
reviewShape: "flat",
|
|
249
242
|
}).pipe(Effect.provide(Layer.mergeAll(ReviewToolkitLayer, IdGenerator.layer)), Effect.scoped),
|
|
250
243
|
options.ignore,
|
|
251
244
|
);
|
|
@@ -264,44 +257,39 @@ const make = <
|
|
|
264
257
|
} as const;
|
|
265
258
|
};
|
|
266
259
|
|
|
267
|
-
/** Options accepted by `PrReview.makeFanOut` (the
|
|
260
|
+
/** Options accepted by `PrReview.makeFanOut` (the host-scheduled pipeline). */
|
|
268
261
|
export interface PrReviewFanOutOptions<
|
|
269
262
|
Provider,
|
|
270
263
|
ModelProvides,
|
|
271
264
|
ModelRequires,
|
|
272
265
|
> extends PrReviewSharedOptions {
|
|
273
|
-
/** The Effect AI Model bound to
|
|
266
|
+
/** The Effect AI Model bound to every child review pass. */
|
|
274
267
|
readonly model: Model.Model<Provider, LanguageModel.LanguageModel | ModelProvides, ModelRequires>;
|
|
275
268
|
/**
|
|
276
269
|
* Static guidance injected into every child reviewer's instructions. The
|
|
277
|
-
*
|
|
270
|
+
* pipeline schedules children from the deterministic plan, so
|
|
278
271
|
* mission-dependent guidance cannot exist for children.
|
|
279
272
|
*/
|
|
280
273
|
readonly guidance?: string | ReadonlyArray<string> | undefined;
|
|
281
274
|
}
|
|
282
275
|
|
|
283
276
|
/**
|
|
284
|
-
* Build the fan-out reviewer:
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
277
|
+
* Build the fan-out reviewer: host code schedules host-planned general and
|
|
278
|
+
* specialist discovery plus independent candidate verification directly as
|
|
279
|
+
* bounded child runs — there is no coordinator model and no delegation tool,
|
|
280
|
+
* so review assurance cannot fail on scheduling compliance. Host code
|
|
281
|
+
* composes publication only from exactly confirmed candidates. Child
|
|
282
|
+
* execution bounds are packaged and not configurable here.
|
|
290
283
|
*/
|
|
291
284
|
const makeFanOut = <Provider, ModelProvides, ModelRequires>(
|
|
292
285
|
options: PrReviewFanOutOptions<Provider, ModelProvides, ModelRequires>,
|
|
293
286
|
) => {
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
});
|
|
298
|
-
// Structural bindings for the same reason as in `make` above.
|
|
299
|
-
const binding = Object.freeze({ definition: suite.parent, model: options.model });
|
|
300
|
-
const childBinding = Object.freeze({ definition: suite.child, model: options.model });
|
|
287
|
+
const child = makeFileReviewerDefinition({ guidance: options.guidance });
|
|
288
|
+
// Structural binding for the same reason as in `make` above.
|
|
289
|
+
const childBinding = Object.freeze({ definition: child, model: options.model });
|
|
301
290
|
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
// child guidance, the host knobs, and the model binding descriptor.
|
|
291
|
+
// Everything that shapes this pipeline's output: the child instructions
|
|
292
|
+
// (guidance included) plus the host knobs the children do not carry.
|
|
305
293
|
const guidanceLines =
|
|
306
294
|
options.guidance === undefined
|
|
307
295
|
? []
|
|
@@ -310,31 +298,30 @@ const makeFanOut = <Provider, ModelProvides, ModelRequires>(
|
|
|
310
298
|
: options.guidance;
|
|
311
299
|
const signature = (mission: ReviewMission): string =>
|
|
312
300
|
[
|
|
313
|
-
|
|
301
|
+
"pr-review-fan-out-host-scheduled-v1",
|
|
302
|
+
// Children never see the mission, but the documented skip-unchanged
|
|
303
|
+
// contract includes pull-request framing, so the fingerprint keeps it.
|
|
304
|
+
JSON.stringify(Schema.encodeSync(ReviewMission)(mission)),
|
|
314
305
|
`childGuidance=${JSON.stringify(guidanceLines)}`,
|
|
306
|
+
`maxFindings=${clampMaxFindings(options.maxFindings)}`,
|
|
315
307
|
`applyVerdict=${String(options.applyVerdict ?? false)}`,
|
|
316
308
|
...(options.modelLabel === undefined ? [] : [`model=${options.modelLabel}`]),
|
|
317
309
|
].join(" ");
|
|
318
310
|
const profileSignature = (_mission: ReviewMission): string =>
|
|
319
311
|
[
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
"pr-review-profile-
|
|
312
|
+
// v4 invalidates continuity produced by the coordinator-scheduled
|
|
313
|
+
// pipeline; the host now schedules every pass and retries failures.
|
|
314
|
+
"pr-review-profile-v4-host-scheduled",
|
|
323
315
|
JSON.stringify(guidanceLines),
|
|
324
316
|
JSON.stringify(options.ignore ?? []),
|
|
325
317
|
`maxFindings=${clampMaxFindings(options.maxFindings)}`,
|
|
326
318
|
`applyVerdict=${String(options.applyVerdict ?? false)}`,
|
|
327
319
|
...(options.modelLabel === undefined ? [] : [`model=${options.modelLabel}`]),
|
|
328
|
-
].join("
|
|
329
|
-
const delegationLayer = fanOutHandlersLayerFor(suite.delegation)(childBinding).pipe(
|
|
330
|
-
Layer.provide(
|
|
331
|
-
Layer.mergeAll(FileReviewToolkitLayer, SubagentReservationsMemoryLive, IdGenerator.layer),
|
|
332
|
-
),
|
|
333
|
-
);
|
|
320
|
+
].join(" ");
|
|
334
321
|
|
|
335
322
|
const run = (runOptions: RunReviewOptions = {}) =>
|
|
336
323
|
provideIgnore(
|
|
337
|
-
|
|
324
|
+
executeFanOutReview(childBinding, {
|
|
338
325
|
post: runOptions.post ?? false,
|
|
339
326
|
applyVerdict: options.applyVerdict ?? false,
|
|
340
327
|
limits: options.budget ?? fanOutReviewBudgetLimits,
|
|
@@ -342,20 +329,12 @@ const makeFanOut = <Provider, ModelProvides, ModelRequires>(
|
|
|
342
329
|
signature,
|
|
343
330
|
modelLabel: options.modelLabel,
|
|
344
331
|
runUrl: runOptions.runUrl,
|
|
345
|
-
|
|
346
|
-
reviewShape: "fan-out",
|
|
347
|
-
}).pipe(
|
|
348
|
-
Effect.provide(
|
|
349
|
-
Layer.mergeAll(FanOutCoordinatorToolkitLayer, delegationLayer, IdGenerator.layer),
|
|
350
|
-
),
|
|
351
|
-
Effect.scoped,
|
|
352
|
-
),
|
|
332
|
+
}).pipe(Effect.provide(IdGenerator.layer), Effect.scoped),
|
|
353
333
|
options.ignore,
|
|
354
334
|
);
|
|
355
335
|
|
|
356
336
|
return {
|
|
357
|
-
definition:
|
|
358
|
-
binding,
|
|
337
|
+
definition: child,
|
|
359
338
|
childBinding,
|
|
360
339
|
run,
|
|
361
340
|
fingerprint: makeFingerprint(signature, options.ignore),
|
|
@@ -1,83 +1,14 @@
|
|
|
1
1
|
import { Effect, Layer, Ref, Schema, Stream } from "effect";
|
|
2
|
-
import { LanguageModel, Model
|
|
2
|
+
import { LanguageModel, Model } from "effect/unstable/ai";
|
|
3
3
|
|
|
4
|
-
import { FileReviewReport
|
|
5
|
-
import {
|
|
6
|
-
import { makePromptKeyedModel, scriptedFinalParts, scriptedToolTurn } from "./scripted.ts";
|
|
4
|
+
import { FileReviewReport } from "./fan-out.ts";
|
|
5
|
+
import { scriptedFinalParts } from "./scripted.ts";
|
|
7
6
|
|
|
8
|
-
// Deterministic offline models for the
|
|
9
|
-
// key on
|
|
10
|
-
// so bounded
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export const offlineUnitCallId = (workId: string): string => `delegate-${workId}`;
|
|
15
|
-
|
|
16
|
-
export type OfflineUnitCall = FileReviewRequest;
|
|
17
|
-
|
|
18
|
-
const scriptedUnitCallId = (calls: ReadonlyArray<OfflineUnitCall>, index: number): string => {
|
|
19
|
-
const call = calls[index];
|
|
20
|
-
if (call === undefined) return "delegate-none";
|
|
21
|
-
const occurrence = calls
|
|
22
|
-
.slice(0, index + 1)
|
|
23
|
-
.filter((candidate) => candidate.workId === call.workId).length;
|
|
24
|
-
const base = offlineUnitCallId(call.workId);
|
|
25
|
-
return occurrence === 1 ? base : `${base}-${occurrence}`;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export const makeOfflineFanOutCoordinatorModel = (script: {
|
|
29
|
-
readonly discoveryCalls: ReadonlyArray<OfflineUnitCall>;
|
|
30
|
-
readonly verificationCalls: ReadonlyArray<OfflineUnitCall>;
|
|
31
|
-
readonly review: CodeReview;
|
|
32
|
-
}) => {
|
|
33
|
-
const firstDiscovery = scriptedUnitCallId(script.discoveryCalls, 0);
|
|
34
|
-
const firstVerification = scriptedUnitCallId(script.verificationCalls, 0);
|
|
35
|
-
return makePromptKeyedModel("pr-fanout-coordinator-offline", (promptJson) => {
|
|
36
|
-
if (script.discoveryCalls.length === 0 && promptJson.includes(OFFLINE_UNITS_CALL_ID)) {
|
|
37
|
-
return scriptedFinalParts(JSON.stringify(Schema.encodeSync(CodeReview)(script.review)));
|
|
38
|
-
}
|
|
39
|
-
if (
|
|
40
|
-
script.verificationCalls.length === 0
|
|
41
|
-
? promptJson.includes(firstDiscovery)
|
|
42
|
-
: promptJson.includes(firstVerification)
|
|
43
|
-
) {
|
|
44
|
-
return scriptedFinalParts(JSON.stringify(Schema.encodeSync(CodeReview)(script.review)));
|
|
45
|
-
}
|
|
46
|
-
if (promptJson.includes(firstDiscovery)) {
|
|
47
|
-
return scriptedToolTurn(
|
|
48
|
-
...script.verificationCalls.map(
|
|
49
|
-
(call, index): Response.StreamPartEncoded => ({
|
|
50
|
-
type: "tool-call",
|
|
51
|
-
id: scriptedUnitCallId(script.verificationCalls, index),
|
|
52
|
-
name: "delegate_file_review",
|
|
53
|
-
params: Schema.encodeSync(FileReviewRequest)(call),
|
|
54
|
-
providerExecuted: false,
|
|
55
|
-
}),
|
|
56
|
-
),
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
if (promptJson.includes(OFFLINE_UNITS_CALL_ID)) {
|
|
60
|
-
return scriptedToolTurn(
|
|
61
|
-
...script.discoveryCalls.map(
|
|
62
|
-
(call, index): Response.StreamPartEncoded => ({
|
|
63
|
-
type: "tool-call",
|
|
64
|
-
id: scriptedUnitCallId(script.discoveryCalls, index),
|
|
65
|
-
name: "delegate_file_review",
|
|
66
|
-
params: Schema.encodeSync(FileReviewRequest)(call),
|
|
67
|
-
providerExecuted: false,
|
|
68
|
-
}),
|
|
69
|
-
),
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
return scriptedToolTurn({
|
|
73
|
-
type: "tool-call",
|
|
74
|
-
id: OFFLINE_UNITS_CALL_ID,
|
|
75
|
-
name: "list_review_units",
|
|
76
|
-
params: { scope: "all" },
|
|
77
|
-
providerExecuted: false,
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
};
|
|
7
|
+
// Deterministic offline child models for the host-scheduled fan-out pipeline.
|
|
8
|
+
// Decisions key on the work ID committed into each child's instructions,
|
|
9
|
+
// never call order, so bounded pass concurrency cannot make fixtures flaky.
|
|
10
|
+
// Each work ID carries a SEQUENCE of outcomes consumed per call, so tests can
|
|
11
|
+
// script "fail once, then settle" and pin the pipeline's bounded retry.
|
|
81
12
|
|
|
82
13
|
export type OfflineUnitOutcome =
|
|
83
14
|
| { readonly _tag: "report"; readonly report: FileReviewReport }
|
|
@@ -85,13 +16,15 @@ export type OfflineUnitOutcome =
|
|
|
85
16
|
|
|
86
17
|
export interface OfflineUnitScript {
|
|
87
18
|
readonly workId: string;
|
|
88
|
-
|
|
19
|
+
/** Consumed one per child call for this work ID; the last outcome repeats. */
|
|
20
|
+
readonly outcomes: ReadonlyArray<OfflineUnitOutcome>;
|
|
89
21
|
}
|
|
90
22
|
|
|
91
23
|
export const makeOfflineFileReviewerModel = (scripts: ReadonlyArray<OfflineUnitScript>) =>
|
|
92
24
|
Effect.gen(function* () {
|
|
93
25
|
const calls = yield* Ref.make(0);
|
|
94
26
|
const prompts = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
27
|
+
const callsByWorkId = yield* Ref.make<ReadonlyMap<string, number>>(new Map());
|
|
95
28
|
const model = Model.make(
|
|
96
29
|
"scripted",
|
|
97
30
|
"pr-fanout-file-reviewer-offline",
|
|
@@ -115,10 +48,23 @@ export const makeOfflineFileReviewerModel = (scripts: ReadonlyArray<OfflineUnitS
|
|
|
115
48
|
}
|
|
116
49
|
const [selected] = script;
|
|
117
50
|
if (selected === undefined) return yield* Effect.die("unreachable scripted match");
|
|
51
|
+
const occurrence = yield* Ref.modify(callsByWorkId, (byWorkId) => {
|
|
52
|
+
const count = byWorkId.get(selected.workId) ?? 0;
|
|
53
|
+
const next = new Map(byWorkId);
|
|
54
|
+
next.set(selected.workId, count + 1);
|
|
55
|
+
return [count, next] as const;
|
|
56
|
+
});
|
|
57
|
+
const outcome =
|
|
58
|
+
selected.outcomes[Math.min(occurrence, selected.outcomes.length - 1)];
|
|
59
|
+
if (outcome === undefined) {
|
|
60
|
+
return yield* Effect.die(
|
|
61
|
+
new Error(`Work ID ${selected.workId} scripts no outcomes`),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
118
64
|
return Stream.fromIterable(
|
|
119
65
|
scriptedFinalParts(
|
|
120
|
-
|
|
121
|
-
? JSON.stringify(Schema.encodeSync(FileReviewReport)(
|
|
66
|
+
outcome._tag === "report"
|
|
67
|
+
? JSON.stringify(Schema.encodeSync(FileReviewReport)(outcome.report))
|
|
122
68
|
: "this is not valid review JSON",
|
|
123
69
|
),
|
|
124
70
|
);
|