@effect-agent/pr-review 0.1.0-beta.20 → 0.1.0-beta.22
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 +82 -26
- package/dist/action.d.mts +12 -6
- package/dist/action.mjs +17 -27
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{fan-out-CQFA-o0v.d.mts → fan-out-n-00ppWr.d.mts} +308 -223
- package/dist/{github-DnenG3be.mjs → github-BgtP7Rdv.mjs} +652 -196
- package/dist/github-BgtP7Rdv.mjs.map +1 -0
- package/dist/index.d.mts +27 -54
- package/dist/index.mjs +15 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{providers-BE83_Tfo.mjs → providers-BguZK4B_.mjs} +408 -134
- package/dist/providers-BguZK4B_.mjs.map +1 -0
- package/dist/testing.d.mts +10 -47
- package/dist/testing.mjs +29 -55
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/src/action.ts +40 -39
- package/src/index.ts +1 -0
- package/src/internal/anchors.ts +20 -0
- package/src/internal/coverage.ts +628 -134
- package/src/internal/factory.ts +12 -9
- package/src/internal/fan-out-scripted.ts +61 -94
- package/src/internal/fan-out.ts +560 -255
- package/src/internal/profiles.ts +12 -0
- package/src/internal/progress.ts +1 -1
- package/src/internal/render.ts +51 -25
- package/src/internal/review-agent.ts +74 -25
- package/src/internal/review-state.ts +7 -5
- package/src/internal/review-units.ts +326 -32
- package/src/internal/run.ts +48 -41
- package/dist/github-DnenG3be.mjs.map +0 -1
- package/dist/providers-BE83_Tfo.mjs.map +0 -1
package/src/internal/factory.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "effect-agent";
|
|
11
11
|
import { Toolkit, type LanguageModel, type Model, type Tool } from "effect/unstable/ai";
|
|
12
12
|
|
|
13
|
+
import type { ChangedFile } from "./diff.ts";
|
|
13
14
|
import {
|
|
14
15
|
fanOutHandlersLayerFor,
|
|
15
16
|
FanOutCoordinatorToolkitLayer,
|
|
@@ -256,7 +257,7 @@ const make = <
|
|
|
256
257
|
fingerprint: makeFingerprint(signature, options.ignore),
|
|
257
258
|
profileFingerprint: makeProfileFingerprint(profileSignature, options.ignore),
|
|
258
259
|
snapshot: makeReviewSnapshot(options.ignore),
|
|
259
|
-
filterFiles: (files: ReadonlyArray<
|
|
260
|
+
filterFiles: (files: ReadonlyArray<ChangedFile>) => {
|
|
260
261
|
const ignored = compileIgnoreGlobs(options.ignore ?? []);
|
|
261
262
|
return files.filter((file) => !ignored(file.path));
|
|
262
263
|
},
|
|
@@ -280,12 +281,12 @@ export interface PrReviewFanOutOptions<
|
|
|
280
281
|
}
|
|
281
282
|
|
|
282
283
|
/**
|
|
283
|
-
* Build the fan-out reviewer: a coordinator
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* configurable here — the delegation reservation mirrors
|
|
288
|
-
* and letting the two drift apart is a published-API hazard.
|
|
284
|
+
* Build the fan-out reviewer: a coordinator schedules host-planned general
|
|
285
|
+
* and specialist discovery plus independent candidate verification through
|
|
286
|
+
* attached ephemeral children. Host code reconstructs publication only from
|
|
287
|
+
* exactly confirmed candidates. Child and coordinator execution bounds are
|
|
288
|
+
* packaged and not configurable here — the delegation reservation mirrors
|
|
289
|
+
* the child policy, and letting the two drift apart is a published-API hazard.
|
|
289
290
|
*/
|
|
290
291
|
const makeFanOut = <Provider, ModelProvides, ModelRequires>(
|
|
291
292
|
options: PrReviewFanOutOptions<Provider, ModelProvides, ModelRequires>,
|
|
@@ -316,7 +317,9 @@ const makeFanOut = <Provider, ModelProvides, ModelRequires>(
|
|
|
316
317
|
].join(" ");
|
|
317
318
|
const profileSignature = (_mission: ReviewMission): string =>
|
|
318
319
|
[
|
|
319
|
-
|
|
320
|
+
// v3 invalidates continuity produced before complete evidence sharding,
|
|
321
|
+
// universal specialist scrutiny, and request-bound result projection.
|
|
322
|
+
"pr-review-profile-v3-sharded-request-bound-assurance",
|
|
320
323
|
JSON.stringify(guidanceLines),
|
|
321
324
|
JSON.stringify(options.ignore ?? []),
|
|
322
325
|
`maxFindings=${clampMaxFindings(options.maxFindings)}`,
|
|
@@ -358,7 +361,7 @@ const makeFanOut = <Provider, ModelProvides, ModelRequires>(
|
|
|
358
361
|
fingerprint: makeFingerprint(signature, options.ignore),
|
|
359
362
|
profileFingerprint: makeProfileFingerprint(profileSignature, options.ignore),
|
|
360
363
|
snapshot: makeReviewSnapshot(options.ignore),
|
|
361
|
-
filterFiles: (files: ReadonlyArray<
|
|
364
|
+
filterFiles: (files: ReadonlyArray<ChangedFile>) => {
|
|
362
365
|
const ignored = compileIgnoreGlobs(options.ignore ?? []);
|
|
363
366
|
return files.filter((file) => !ignored(file.path));
|
|
364
367
|
},
|
|
@@ -1,55 +1,69 @@
|
|
|
1
1
|
import { Effect, Layer, Ref, Schema, Stream } from "effect";
|
|
2
2
|
import { LanguageModel, Model, type Response } from "effect/unstable/ai";
|
|
3
3
|
|
|
4
|
-
import { FileReviewReport } from "./fan-out.ts";
|
|
4
|
+
import { FileReviewReport, FileReviewRequest } from "./fan-out.ts";
|
|
5
5
|
import { CodeReview } from "./review-agent.ts";
|
|
6
6
|
import { makePromptKeyedModel, scriptedFinalParts, scriptedToolTurn } from "./scripted.ts";
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// Both key every decision on committed history in the prompt (tool-call ids
|
|
12
|
-
// and briefed unit ids), never on call order, so concurrent children and
|
|
13
|
-
// replays stay honest.
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
8
|
+
// Deterministic offline models for the complete fan-out protocol. Decisions
|
|
9
|
+
// key on committed Tool Call IDs or work IDs in each prompt, never call order,
|
|
10
|
+
// so bounded child concurrency cannot make the fixtures flaky.
|
|
15
11
|
|
|
16
12
|
export const OFFLINE_UNITS_CALL_ID = "units-1";
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
export const offlineUnitCallId = (unitId: string): string => `delegate-${unitId}`;
|
|
14
|
+
export const offlineUnitCallId = (workId: string): string => `delegate-${workId}`;
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
export const offlineChildDiffCallId = (unitId: string): string => `fanout-diff-${unitId}`;
|
|
16
|
+
export type OfflineUnitCall = FileReviewRequest;
|
|
23
17
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
};
|
|
29
27
|
|
|
30
|
-
/**
|
|
31
|
-
* Build the offline scripted coordinator model. Turn 1 lists the review
|
|
32
|
-
* units, Turn 2 declares one delegation Tool Call per scripted unit in one
|
|
33
|
-
* batch, Turn 3 returns the scripted merged review JSON. Decisions key on
|
|
34
|
-
* tool-call ids already committed to the prompt.
|
|
35
|
-
*/
|
|
36
28
|
export const makeOfflineFanOutCoordinatorModel = (script: {
|
|
37
|
-
readonly
|
|
29
|
+
readonly discoveryCalls: ReadonlyArray<OfflineUnitCall>;
|
|
30
|
+
readonly verificationCalls: ReadonlyArray<OfflineUnitCall>;
|
|
38
31
|
readonly review: CodeReview;
|
|
39
32
|
}) => {
|
|
40
|
-
const
|
|
33
|
+
const firstDiscovery = scriptedUnitCallId(script.discoveryCalls, 0);
|
|
34
|
+
const firstVerification = scriptedUnitCallId(script.verificationCalls, 0);
|
|
41
35
|
return makePromptKeyedModel("pr-fanout-coordinator-offline", (promptJson) => {
|
|
42
|
-
if (promptJson.includes(
|
|
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
|
+
) {
|
|
43
44
|
return scriptedFinalParts(JSON.stringify(Schema.encodeSync(CodeReview)(script.review)));
|
|
44
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
|
+
}
|
|
45
59
|
if (promptJson.includes(OFFLINE_UNITS_CALL_ID)) {
|
|
46
60
|
return scriptedToolTurn(
|
|
47
|
-
...script.
|
|
48
|
-
(
|
|
61
|
+
...script.discoveryCalls.map(
|
|
62
|
+
(call, index): Response.StreamPartEncoded => ({
|
|
49
63
|
type: "tool-call",
|
|
50
|
-
id:
|
|
64
|
+
id: scriptedUnitCallId(script.discoveryCalls, index),
|
|
51
65
|
name: "delegate_file_review",
|
|
52
|
-
params:
|
|
66
|
+
params: Schema.encodeSync(FileReviewRequest)(call),
|
|
53
67
|
providerExecuted: false,
|
|
54
68
|
}),
|
|
55
69
|
),
|
|
@@ -65,76 +79,19 @@ export const makeOfflineFanOutCoordinatorModel = (script: {
|
|
|
65
79
|
});
|
|
66
80
|
};
|
|
67
81
|
|
|
68
|
-
/** How one scripted child behaves for its briefed unit. */
|
|
69
82
|
export type OfflineUnitOutcome =
|
|
70
|
-
|
|
71
|
-
| { readonly _tag: "
|
|
72
|
-
/** Read one diff, then return non-JSON — the child fails typed (AgentOutputError). */
|
|
73
|
-
| { readonly _tag: "malformed-output" }
|
|
74
|
-
/**
|
|
75
|
-
* Declare more Tool Calls than the child's AgentPolicy allows in one turn —
|
|
76
|
-
* none executes and the child fails typed (AgentPolicyError "tool-calls",
|
|
77
|
-
* the reviewer's deliberate `onExhaustion: "fail"` pin).
|
|
78
|
-
*/
|
|
79
|
-
| { readonly _tag: "budget-runaway"; readonly declaredCalls: number };
|
|
83
|
+
| { readonly _tag: "report"; readonly report: FileReviewReport }
|
|
84
|
+
| { readonly _tag: "malformed-output" };
|
|
80
85
|
|
|
81
86
|
export interface OfflineUnitScript {
|
|
82
|
-
readonly
|
|
83
|
-
/** The one file the scripted child reads the diff of. */
|
|
84
|
-
readonly diffPath: string;
|
|
87
|
+
readonly workId: string;
|
|
85
88
|
readonly outcome: OfflineUnitOutcome;
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
/**
|
|
89
|
-
* Build the offline scripted file-reviewer model shared by every delegated
|
|
90
|
-
* child. Each child Run builds the Model Layer inside its own scope; the
|
|
91
|
-
* script entry is selected by the briefed unitId present in the child's OWN
|
|
92
|
-
* prompt, and the turn is selected by whether that unit's diff Tool Call id
|
|
93
|
-
* is already committed there — content-keyed on both axes, so concurrent
|
|
94
|
-
* children never interfere. First-turn child prompts are recorded for
|
|
95
|
-
* context-isolation assertions.
|
|
96
|
-
*/
|
|
97
91
|
export const makeOfflineFileReviewerModel = (scripts: ReadonlyArray<OfflineUnitScript>) =>
|
|
98
92
|
Effect.gen(function* () {
|
|
99
93
|
const calls = yield* Ref.make(0);
|
|
100
94
|
const prompts = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
101
|
-
const decide = (promptJson: string): ReadonlyArray<Response.StreamPartEncoded> | undefined => {
|
|
102
|
-
const script = scripts.find((candidate) => promptJson.includes(candidate.unitId));
|
|
103
|
-
if (script === undefined) return undefined;
|
|
104
|
-
switch (script.outcome._tag) {
|
|
105
|
-
case "budget-runaway": {
|
|
106
|
-
return scriptedToolTurn(
|
|
107
|
-
...Array.from(
|
|
108
|
-
{ length: script.outcome.declaredCalls },
|
|
109
|
-
(_, index): Response.StreamPartEncoded => ({
|
|
110
|
-
type: "tool-call",
|
|
111
|
-
id: `runaway-${script.unitId}-${index + 1}`,
|
|
112
|
-
name: "read_file_diff",
|
|
113
|
-
params: { path: script.diffPath },
|
|
114
|
-
providerExecuted: false,
|
|
115
|
-
}),
|
|
116
|
-
),
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
case "malformed-output":
|
|
120
|
-
case "findings": {
|
|
121
|
-
if (promptJson.includes(offlineChildDiffCallId(script.unitId))) {
|
|
122
|
-
return scriptedFinalParts(
|
|
123
|
-
script.outcome._tag === "findings"
|
|
124
|
-
? JSON.stringify(Schema.encodeSync(FileReviewReport)(script.outcome.report))
|
|
125
|
-
: "this is not the JSON you are looking for",
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
return scriptedToolTurn({
|
|
129
|
-
type: "tool-call",
|
|
130
|
-
id: offlineChildDiffCallId(script.unitId),
|
|
131
|
-
name: "read_file_diff",
|
|
132
|
-
params: { path: script.diffPath },
|
|
133
|
-
providerExecuted: false,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
95
|
const model = Model.make(
|
|
139
96
|
"scripted",
|
|
140
97
|
"pr-fanout-file-reviewer-offline",
|
|
@@ -148,13 +105,23 @@ export const makeOfflineFileReviewerModel = (scripts: ReadonlyArray<OfflineUnitS
|
|
|
148
105
|
yield* Ref.update(calls, (value) => value + 1);
|
|
149
106
|
const promptJson = JSON.stringify(request.prompt);
|
|
150
107
|
yield* Ref.update(prompts, (previous) => [...previous, promptJson]);
|
|
151
|
-
const
|
|
152
|
-
|
|
108
|
+
const script = scripts.filter((candidate) =>
|
|
109
|
+
promptJson.includes(`for ${candidate.workId} in host-planned unit`),
|
|
110
|
+
);
|
|
111
|
+
if (script.length !== 1) {
|
|
153
112
|
return yield* Effect.die(
|
|
154
|
-
new Error("The child prompt
|
|
113
|
+
new Error("The child prompt must name exactly one scripted work ID"),
|
|
155
114
|
);
|
|
156
115
|
}
|
|
157
|
-
|
|
116
|
+
const [selected] = script;
|
|
117
|
+
if (selected === undefined) return yield* Effect.die("unreachable scripted match");
|
|
118
|
+
return Stream.fromIterable(
|
|
119
|
+
scriptedFinalParts(
|
|
120
|
+
selected.outcome._tag === "report"
|
|
121
|
+
? JSON.stringify(Schema.encodeSync(FileReviewReport)(selected.outcome.report))
|
|
122
|
+
: "this is not valid review JSON",
|
|
123
|
+
),
|
|
124
|
+
);
|
|
158
125
|
}),
|
|
159
126
|
),
|
|
160
127
|
}),
|