@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.
@@ -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<import("./diff.ts").ChangedFile>) => {
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 that delegates bounded per-unit
284
- * file reviews to attached ephemeral children and merges their findings under
285
- * the same output contract and the same fail-closed publication path as the
286
- * flat reviewer. Child and coordinator execution bounds are packaged and not
287
- * configurable here — the delegation reservation mirrors the child policy,
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
- "pr-review-profile-v1-fan-out",
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<import("./diff.ts").ChangedFile>) => {
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
- // Deterministic offline models for the fan-out reviewer: prompt-keyed
10
- // scripted models for BOTH the coordinator and the file-reviewer children.
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
- /** The delegation Tool Call id the scripted coordinator uses for one unit. */
19
- export const offlineUnitCallId = (unitId: string): string => `delegate-${unitId}`;
14
+ export const offlineUnitCallId = (workId: string): string => `delegate-${workId}`;
20
15
 
21
- /** The diff Tool Call id the scripted child uses for one unit. */
22
- export const offlineChildDiffCallId = (unitId: string): string => `fanout-diff-${unitId}`;
16
+ export type OfflineUnitCall = FileReviewRequest;
23
17
 
24
- /** One scripted delegation the offline coordinator declares. */
25
- export interface OfflineUnitCall {
26
- readonly unitId: string;
27
- readonly paths: ReadonlyArray<string>;
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 unitCalls: ReadonlyArray<OfflineUnitCall>;
29
+ readonly discoveryCalls: ReadonlyArray<OfflineUnitCall>;
30
+ readonly verificationCalls: ReadonlyArray<OfflineUnitCall>;
38
31
  readonly review: CodeReview;
39
32
  }) => {
40
- const firstUnitCallId = offlineUnitCallId(script.unitCalls[0]?.unitId ?? "unit-none");
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(firstUnitCallId)) {
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.unitCalls.map(
48
- (unit): Response.StreamPartEncoded => ({
61
+ ...script.discoveryCalls.map(
62
+ (call, index): Response.StreamPartEncoded => ({
49
63
  type: "tool-call",
50
- id: offlineUnitCallId(unit.unitId),
64
+ id: scriptedUnitCallId(script.discoveryCalls, index),
51
65
  name: "delegate_file_review",
52
- params: { unitId: unit.unitId, paths: unit.paths },
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
- /** Read one diff, then return the scripted report. */
71
- | { readonly _tag: "findings"; readonly report: FileReviewReport }
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 unitId: string;
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 parts = decide(promptJson);
152
- if (parts === undefined) {
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 names no scripted review unit"),
113
+ new Error("The child prompt must name exactly one scripted work ID"),
155
114
  );
156
115
  }
157
- return Stream.fromIterable(parts);
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
  }),