@dieulc/workflow 0.3.1 → 0.4.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/README.md +118 -1
- package/commit-policy.ts +358 -0
- package/index.ts +432 -9
- package/package.json +4 -2
- package/permissions.ts +98 -0
- package/review-providers.ts +406 -0
- package/review-runtime.ts +108 -5
- package/review-ui.ts +1 -0
- package/review.ts +219 -4
package/review-runtime.ts
CHANGED
|
@@ -22,13 +22,17 @@
|
|
|
22
22
|
* instructions — the reviewer only supplies judgement.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import * as path from "node:path";
|
|
26
|
+
|
|
25
27
|
import {
|
|
26
28
|
DEFAULT_REVIEW_MODE_CONFIG,
|
|
27
29
|
createReviewRoundGate,
|
|
28
30
|
createReviewSession,
|
|
29
31
|
createReviewState,
|
|
30
32
|
planHash,
|
|
33
|
+
readProviderErrorEvent,
|
|
31
34
|
reviewNeedsVerification,
|
|
35
|
+
truncateErrorText,
|
|
32
36
|
type CreateReviewSessionOptions,
|
|
33
37
|
type CreateReviewSessionResult,
|
|
34
38
|
type ReviewChildSession,
|
|
@@ -44,6 +48,7 @@ import {
|
|
|
44
48
|
type ReviewVerdict,
|
|
45
49
|
ReviewTranscript as Transcript,
|
|
46
50
|
} from "./review.ts";
|
|
51
|
+
import { appendProviderAuthHint } from "./review-providers.ts";
|
|
47
52
|
import { createReviewTools } from "./review-tools.ts";
|
|
48
53
|
import { renderReviewContextBlock, type ReviewFinding } from "./utils.ts";
|
|
49
54
|
|
|
@@ -71,6 +76,15 @@ export interface ReviewRuntimeDeps {
|
|
|
71
76
|
| undefined;
|
|
72
77
|
/** Resolve a model from the parent's registry. */
|
|
73
78
|
findModel: (provider: string, id: string) => unknown;
|
|
79
|
+
/**
|
|
80
|
+
* Parent-side provider-registration surface for the reviewer runtime, found
|
|
81
|
+
* with `findParentProviderSurface(ctx)` at session-creation time. Providers
|
|
82
|
+
* the parent knows that the isolated child does not compose yet are seeded
|
|
83
|
+
* onto it before `createAgentSession`. Optional: when absent (or when the
|
|
84
|
+
* probe finds no surface) nothing is seeded and the provider-auth preflight
|
|
85
|
+
* reports the generic escape hatch instead.
|
|
86
|
+
*/
|
|
87
|
+
parentProviders?: () => unknown;
|
|
74
88
|
/** Plan path Plan Mode chose (absolute or cwd-relative), if known. */
|
|
75
89
|
planPath: () => string | undefined;
|
|
76
90
|
/** Current plan text on disk, if it exists. */
|
|
@@ -279,6 +293,29 @@ export function createReviewRuntime(deps: ReviewRuntimeDeps): ReviewRuntime {
|
|
|
279
293
|
let abortController: AbortController | undefined;
|
|
280
294
|
/** True while `runRound` is in flight (gates prompt routing). */
|
|
281
295
|
let roundActive = false;
|
|
296
|
+
/**
|
|
297
|
+
* The reviewer's most recent provider failure, captured live from child
|
|
298
|
+
* events (`auto_retry_start`, failed `auto_retry_end`, an `agent_end` that
|
|
299
|
+
* stopped on error). Latest wins. Cleared at the start of every pass so a
|
|
300
|
+
* stale error can never mislabel a later clean settle (see runRound).
|
|
301
|
+
*/
|
|
302
|
+
let lastProviderError:
|
|
303
|
+
| { message: string; retries: number; at: number }
|
|
304
|
+
| undefined;
|
|
305
|
+
|
|
306
|
+
/** Record one provider failure. The message arrives pre-decoded, non-empty. */
|
|
307
|
+
const noteProviderError = (message: string, retries: number): void => {
|
|
308
|
+
lastProviderError = {
|
|
309
|
+
message,
|
|
310
|
+
retries: Math.max(0, Math.trunc(retries)),
|
|
311
|
+
at: Date.now(),
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/** Drop the captured error when a new pass starts. */
|
|
316
|
+
const clearProviderError = (): void => {
|
|
317
|
+
lastProviderError = undefined;
|
|
318
|
+
};
|
|
282
319
|
|
|
283
320
|
const config = (): ReviewModeConfig => {
|
|
284
321
|
try {
|
|
@@ -594,6 +631,11 @@ export function createReviewRuntime(deps: ReviewRuntimeDeps): ReviewRuntime {
|
|
|
594
631
|
// Reuse the already-resolved module instead of importing twice.
|
|
595
632
|
loadSdk: async () => sdk,
|
|
596
633
|
};
|
|
634
|
+
// Probed lazily, at creation time: the parent context is live here.
|
|
635
|
+
const parentSurface = deps.parentProviders?.();
|
|
636
|
+
if (parentSurface !== undefined) {
|
|
637
|
+
createOptions.parentProviders = parentSurface;
|
|
638
|
+
}
|
|
597
639
|
|
|
598
640
|
const created: CreateReviewSessionResult =
|
|
599
641
|
await createReviewSession(createOptions);
|
|
@@ -633,6 +675,29 @@ export function createReviewRuntime(deps: ReviewRuntimeDeps): ReviewRuntime {
|
|
|
633
675
|
}
|
|
634
676
|
}
|
|
635
677
|
const type = readEventType(event);
|
|
678
|
+
// Capture provider failures as they happen, so a pass that ends
|
|
679
|
+
// without a submission can be diagnosed with the reviewer's real
|
|
680
|
+
// API error instead of a generic "no plan" message (see runRound).
|
|
681
|
+
try {
|
|
682
|
+
const failure = readProviderErrorEvent(event);
|
|
683
|
+
if (failure.kind === "retry") {
|
|
684
|
+
noteProviderError(failure.message, failure.retries);
|
|
685
|
+
} else if (failure.kind === "exhaust") {
|
|
686
|
+
noteProviderError(
|
|
687
|
+
failure.message ||
|
|
688
|
+
lastProviderError?.message ||
|
|
689
|
+
"provider error (no detail)",
|
|
690
|
+
failure.retries || lastProviderError?.retries || 0,
|
|
691
|
+
);
|
|
692
|
+
} else if (failure.kind === "stop") {
|
|
693
|
+
noteProviderError(
|
|
694
|
+
failure.message,
|
|
695
|
+
lastProviderError?.retries ?? 0,
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
} catch {
|
|
699
|
+
/* error capture is diagnostics only: never break the review */
|
|
700
|
+
}
|
|
636
701
|
if (type === "agent_settled") {
|
|
637
702
|
noteSettled();
|
|
638
703
|
} else if (type === "agent_end") {
|
|
@@ -738,6 +803,7 @@ export function createReviewRuntime(deps: ReviewRuntimeDeps): ReviewRuntime {
|
|
|
738
803
|
// ── Pass 1 ──────────────────────────────────────────────────────
|
|
739
804
|
await settleInFlight(deadline, rc);
|
|
740
805
|
gate.reset();
|
|
806
|
+
clearProviderError();
|
|
741
807
|
const pass1Prompt = buildPass1Prompt({
|
|
742
808
|
planPath: input.planPath,
|
|
743
809
|
planText: originalPlan,
|
|
@@ -755,7 +821,16 @@ export function createReviewRuntime(deps: ReviewRuntimeDeps): ReviewRuntime {
|
|
|
755
821
|
|
|
756
822
|
if (p1.stop === "error") {
|
|
757
823
|
await abortChild();
|
|
758
|
-
|
|
824
|
+
// Safety net for any provider-source gap the preflight could not see:
|
|
825
|
+
// the bare "No API key found for <provider>." gains the actionable
|
|
826
|
+
// hint ONLY when the failing provider is the reviewer's own.
|
|
827
|
+
return fail(
|
|
828
|
+
`could not prompt the reviewer: ${appendProviderAuthHint(
|
|
829
|
+
errorText(p1.error),
|
|
830
|
+
deps.reviewerModel()?.provider,
|
|
831
|
+
path.join(deps.agentDir, "models.json"),
|
|
832
|
+
)}`,
|
|
833
|
+
);
|
|
759
834
|
}
|
|
760
835
|
if (p1.stop === "aborted" || rc.signal.aborted) {
|
|
761
836
|
await abortChild();
|
|
@@ -773,10 +848,23 @@ export function createReviewRuntime(deps: ReviewRuntimeDeps): ReviewRuntime {
|
|
|
773
848
|
// The run is over (or out of time) and nothing reached the plan file.
|
|
774
849
|
// Fail now — waiting longer cannot produce a submission.
|
|
775
850
|
await abortChild();
|
|
851
|
+
if (p1.stop === "timeout") {
|
|
852
|
+
return fail(
|
|
853
|
+
`the reviewer did not finish within ${Math.round((limitMs ?? 0) / 1000)}s and submitted no plan`,
|
|
854
|
+
originalPlan,
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
// A provider failure settles the run without a submission — name the
|
|
858
|
+
// real cause (with the provider's own message) instead of blaming the
|
|
859
|
+
// write path. The slot only holds errors captured during THIS pass.
|
|
860
|
+
if (lastProviderError) {
|
|
861
|
+
return fail(
|
|
862
|
+
`the reviewer's provider failed after ${lastProviderError.retries} retry attempt(s): ${truncateErrorText(lastProviderError.message)}`,
|
|
863
|
+
originalPlan,
|
|
864
|
+
);
|
|
865
|
+
}
|
|
776
866
|
return fail(
|
|
777
|
-
|
|
778
|
-
? `the reviewer did not finish within ${Math.round((limitMs ?? 0) / 1000)}s and submitted no plan`
|
|
779
|
-
: "the reviewer finished without submitting a plan — it has no write path unless review_submit_plan succeeds",
|
|
867
|
+
"the reviewer finished without submitting a plan — it has no write path unless review_submit_plan succeeds",
|
|
780
868
|
originalPlan,
|
|
781
869
|
);
|
|
782
870
|
}
|
|
@@ -802,6 +890,7 @@ export function createReviewRuntime(deps: ReviewRuntimeDeps): ReviewRuntime {
|
|
|
802
890
|
setPhase("verifying");
|
|
803
891
|
await settleInFlight(deadline, rc);
|
|
804
892
|
gate.reset();
|
|
893
|
+
clearProviderError();
|
|
805
894
|
const pass2Prompt = buildPass2Prompt({
|
|
806
895
|
planPath: input.planPath,
|
|
807
896
|
originalPlan,
|
|
@@ -811,14 +900,28 @@ export function createReviewRuntime(deps: ReviewRuntimeDeps): ReviewRuntime {
|
|
|
811
900
|
const p2 = await runPass(2, deadline, rc, () =>
|
|
812
901
|
child.followUp(pass2Prompt),
|
|
813
902
|
);
|
|
903
|
+
// Name the provider when the verification pass died on one; pass 2
|
|
904
|
+
// stays best-effort either way — pass 1's plan is never lost to it.
|
|
905
|
+
const p2Provider = lastProviderError
|
|
906
|
+
? `provider error after ${lastProviderError.retries} retry attempt(s): ${truncateErrorText(lastProviderError.message)}`
|
|
907
|
+
: undefined;
|
|
814
908
|
if (p2.error !== undefined) {
|
|
815
909
|
// Verification is an enhancement: never lose pass 1's plan over it.
|
|
816
910
|
deps.log(
|
|
817
|
-
|
|
911
|
+
p2Provider
|
|
912
|
+
? `Review Mode verification pass failed (${p2Provider}; send error: ${errorText(p2.error)}) — keeping pass 1's plan.`
|
|
913
|
+
: `Review Mode verification pass failed (${errorText(p2.error)}) — keeping pass 1's plan.`,
|
|
818
914
|
"warning",
|
|
819
915
|
);
|
|
820
916
|
} else if (p2.submitted) {
|
|
821
917
|
finalPlan = p2.submitted.planText;
|
|
918
|
+
} else if (p2Provider) {
|
|
919
|
+
// The run settled without a submission AND a provider error was
|
|
920
|
+
// seen this pass: say why, then keep going with pass 1's rewrite.
|
|
921
|
+
deps.log(
|
|
922
|
+
`Review Mode verification pass ended without a submission (${p2Provider}) — keeping pass 1's plan.`,
|
|
923
|
+
"warning",
|
|
924
|
+
);
|
|
822
925
|
}
|
|
823
926
|
if (p2.stop === "timeout") await abortChild();
|
|
824
927
|
}
|
package/review-ui.ts
CHANGED
package/review.ts
CHANGED
|
@@ -36,6 +36,13 @@ import {
|
|
|
36
36
|
type ReviewFinding,
|
|
37
37
|
type ReviewTier,
|
|
38
38
|
} from "./utils.ts";
|
|
39
|
+
import {
|
|
40
|
+
checkReviewerProviderAuth,
|
|
41
|
+
isParentProviderSurface,
|
|
42
|
+
seedChildRuntimeProviders,
|
|
43
|
+
unconfiguredReviewerProviderMessage,
|
|
44
|
+
type ProviderSeedResult,
|
|
45
|
+
} from "./review-providers.ts";
|
|
39
46
|
|
|
40
47
|
// Re-exported so `review.ts` is the single import surface for Review Mode.
|
|
41
48
|
export type { ReviewDisposition, ReviewFinding, ReviewTier };
|
|
@@ -567,7 +574,8 @@ export type ReviewLineKind =
|
|
|
567
574
|
| "tool"
|
|
568
575
|
| "finding"
|
|
569
576
|
| "notice"
|
|
570
|
-
| "status"
|
|
577
|
+
| "status"
|
|
578
|
+
| "error";
|
|
571
579
|
|
|
572
580
|
/** One line in the reviewer pane. Kept small: the pane renders these verbatim. */
|
|
573
581
|
export interface ReviewTranscriptLine {
|
|
@@ -1099,6 +1107,103 @@ function truncateLabel(s: string, max: number): string {
|
|
|
1099
1107
|
return s.length <= max ? s : `${s.slice(0, max - 1).trimEnd()}…`;
|
|
1100
1108
|
}
|
|
1101
1109
|
|
|
1110
|
+
/** Longest provider-error excerpt shown inline in a transcript line. */
|
|
1111
|
+
export const MAX_ERROR_TEXT = 160;
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* One-line excerpt of a provider error message for `notify` and the reviewer
|
|
1115
|
+
* pane. Provider errors are often multi-line JSON blobs; they must become a
|
|
1116
|
+
* single line to survive a transcript entry and a `ui.notify` banner.
|
|
1117
|
+
* Pure and total: empty/undefined input yields "", never throws.
|
|
1118
|
+
*/
|
|
1119
|
+
export function truncateErrorText(input: unknown, max: number = MAX_ERROR_TEXT): string {
|
|
1120
|
+
const raw = typeof input === "string" ? input : "";
|
|
1121
|
+
const flat = raw.replace(/\s+/g, " ").trim();
|
|
1122
|
+
if (!flat) return "";
|
|
1123
|
+
const cap = Math.max(1, Math.trunc(max));
|
|
1124
|
+
return flat.length <= cap ? flat : `${flat.slice(0, cap - 1).trimEnd()}…`;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Extract the provider error message when a run stopped on an error: scan an
|
|
1129
|
+
* `agent_end` event's messages from the end for the last assistant message
|
|
1130
|
+
* with `stopReason === "error"`. Structural on purpose — SDK message shapes
|
|
1131
|
+
* are not imported here. Returns undefined for clean runs and junk input.
|
|
1132
|
+
*/
|
|
1133
|
+
export function lastAssistantErrorStop(messages: unknown): string | undefined {
|
|
1134
|
+
if (!Array.isArray(messages)) return undefined;
|
|
1135
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1136
|
+
const rec = messages[i] as Record<string, unknown> | null | undefined;
|
|
1137
|
+
if (!rec || typeof rec !== "object") continue;
|
|
1138
|
+
if (rec.role !== "assistant") continue;
|
|
1139
|
+
// Last assistant message reached: a non-error stop means a clean run.
|
|
1140
|
+
if (rec.stopReason !== "error") return undefined;
|
|
1141
|
+
const err = typeof rec.errorMessage === "string" ? rec.errorMessage : "";
|
|
1142
|
+
return err || "unknown provider error";
|
|
1143
|
+
}
|
|
1144
|
+
return undefined;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/**
|
|
1148
|
+
* What a child-session event says about provider health, decoded at the
|
|
1149
|
+
* boundary so callers branch on typed values instead of SDK event shapes:
|
|
1150
|
+
*
|
|
1151
|
+
* - `retry` → the SDK scheduled another attempt (`auto_retry_start`) and
|
|
1152
|
+
* reported the provider's error message.
|
|
1153
|
+
* - `exhaust` → the retry budget ran out (`auto_retry_end`, `success: false`);
|
|
1154
|
+
* the run is about to settle on an error. `message` may be empty when the
|
|
1155
|
+
* event carries no `finalError` — callers then keep their earlier message.
|
|
1156
|
+
* - `stop` → the run itself ended with `stopReason === "error"` on its last
|
|
1157
|
+
* assistant message (auto-retry disabled, or a non-retryable failure).
|
|
1158
|
+
* - `none` → anything else, including a retry that recovered.
|
|
1159
|
+
*/
|
|
1160
|
+
export type ReviewProviderFailure =
|
|
1161
|
+
| { kind: "retry"; message: string; retries: number }
|
|
1162
|
+
| { kind: "exhaust"; message: string; retries: number }
|
|
1163
|
+
| { kind: "stop"; message: string }
|
|
1164
|
+
| { kind: "none" };
|
|
1165
|
+
|
|
1166
|
+
/** Structural decode of one provider-failure event. Never throws. */
|
|
1167
|
+
export function readProviderErrorEvent(
|
|
1168
|
+
event: unknown,
|
|
1169
|
+
): ReviewProviderFailure {
|
|
1170
|
+
const e = eventRecord(event);
|
|
1171
|
+
if (!e) return { kind: "none" };
|
|
1172
|
+
const type = typeof e.type === "string" ? e.type : "";
|
|
1173
|
+
if (type === "auto_retry_start") {
|
|
1174
|
+
const message =
|
|
1175
|
+
typeof e.errorMessage === "string" ? e.errorMessage.trim() : "";
|
|
1176
|
+
if (!message) return { kind: "none" };
|
|
1177
|
+
return {
|
|
1178
|
+
kind: "retry",
|
|
1179
|
+
message,
|
|
1180
|
+
retries: readRetryAttempt(e.attempt),
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
if (type === "auto_retry_end") {
|
|
1184
|
+
if (e.success !== false) return { kind: "none" };
|
|
1185
|
+
const message = typeof e.finalError === "string" ? e.finalError.trim() : "";
|
|
1186
|
+
return {
|
|
1187
|
+
kind: "exhaust",
|
|
1188
|
+
message,
|
|
1189
|
+
retries: readRetryAttempt(e.attempt),
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
if (type === "agent_end") {
|
|
1193
|
+
const message = lastAssistantErrorStop(e.messages);
|
|
1194
|
+
if (message === undefined) return { kind: "none" };
|
|
1195
|
+
return { kind: "stop", message };
|
|
1196
|
+
}
|
|
1197
|
+
return { kind: "none" };
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/** Non-negative attempt count from a retry event; 0 when absent or junk. */
|
|
1201
|
+
function readRetryAttempt(value: unknown): number {
|
|
1202
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
1203
|
+
? Math.trunc(value)
|
|
1204
|
+
: 0;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1102
1207
|
/**
|
|
1103
1208
|
* Human-readable header for a tool call: the tool name plus a short, safe
|
|
1104
1209
|
* excerpt of its key argument (`▸ read src/app.ts`, `▸ review_bash: git log -5`).
|
|
@@ -1170,6 +1275,12 @@ export function formatActionLabel(toolName: unknown, args: unknown): string {
|
|
|
1170
1275
|
* `toolLabels` is owned by the stateful caller (`ReviewTranscript`): the end
|
|
1171
1276
|
* event carries no args, so the start event registers `toolCallId → label` and
|
|
1172
1277
|
* the end event consumes it. Pure otherwise; junk input never throws.
|
|
1278
|
+
*
|
|
1279
|
+
* Provider failures are surfaced with their real (truncated) text: the
|
|
1280
|
+
* `auto_retry_start` notice carries the provider's message, and a failed
|
|
1281
|
+
* `auto_retry_end` or an `agent_end` that stopped on `stopReason === "error"`
|
|
1282
|
+
* appends an `error` line — a dead provider is then visible in the pane during
|
|
1283
|
+
* the review instead of resurfacing later as a mysterious "no plan" failure.
|
|
1173
1284
|
*/
|
|
1174
1285
|
export function translateReviewEvent(
|
|
1175
1286
|
event: unknown,
|
|
@@ -1207,15 +1318,70 @@ export function translateReviewEvent(
|
|
|
1207
1318
|
if (type === "agent_start") {
|
|
1208
1319
|
return [{ op: "new", kind: "status", text: "reviewer started" }];
|
|
1209
1320
|
}
|
|
1321
|
+
return providerErrorOps(e, type);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* Provider-error events → transcript lines, split out of
|
|
1326
|
+
* `translateReviewEvent` to keep the dispatcher flat:
|
|
1327
|
+
*
|
|
1328
|
+
* - `agent_end` → status line, plus an `error` line when the run
|
|
1329
|
+
* stopped on `stopReason === "error"` (auto-retry disabled, or the first
|
|
1330
|
+
* failure was not retryable).
|
|
1331
|
+
* - `auto_retry_start` → notice carrying the provider's message (truncated).
|
|
1332
|
+
* - `auto_retry_end` → `error` line when the retry budget is exhausted
|
|
1333
|
+
* (`success: false`); a recovered retry (`success: true`) stays silent.
|
|
1334
|
+
*
|
|
1335
|
+
* Unknown types and junk fields degrade to empty ops; never throws.
|
|
1336
|
+
*/
|
|
1337
|
+
function providerErrorOps(
|
|
1338
|
+
e: Record<string, unknown>,
|
|
1339
|
+
type: string,
|
|
1340
|
+
): ReviewTranscriptOp[] {
|
|
1210
1341
|
if (type === "agent_end") {
|
|
1211
|
-
|
|
1342
|
+
const ops: ReviewTranscriptOp[] = [
|
|
1343
|
+
{ op: "new", kind: "status", text: "reviewer finished this run" },
|
|
1344
|
+
];
|
|
1345
|
+
const failed = lastAssistantErrorStop(e.messages);
|
|
1346
|
+
if (failed) {
|
|
1347
|
+
ops.push({
|
|
1348
|
+
op: "new",
|
|
1349
|
+
kind: "error",
|
|
1350
|
+
text: `reviewer run ended on a provider error: ${truncateErrorText(failed)}`,
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
return ops;
|
|
1212
1354
|
}
|
|
1213
1355
|
if (type === "auto_retry_start") {
|
|
1356
|
+
const msg = typeof e.errorMessage === "string" ? e.errorMessage : "";
|
|
1214
1357
|
return [
|
|
1215
|
-
{
|
|
1358
|
+
{
|
|
1359
|
+
op: "new",
|
|
1360
|
+
kind: "notice",
|
|
1361
|
+
text: msg
|
|
1362
|
+
? `retrying after a provider error: ${truncateErrorText(msg)}`
|
|
1363
|
+
: "retrying after a provider error",
|
|
1364
|
+
},
|
|
1216
1365
|
];
|
|
1217
1366
|
}
|
|
1218
|
-
|
|
1367
|
+
if (type === "auto_retry_end") {
|
|
1368
|
+
// `success: false` ends the retry loop with the provider still failing —
|
|
1369
|
+
// the run is about to settle on an error stop. `success: true` means a
|
|
1370
|
+
// retry recovered; the earlier notice already covers that case.
|
|
1371
|
+
if (e.success === false) {
|
|
1372
|
+
const detail = truncateErrorText(e.finalError);
|
|
1373
|
+
return [
|
|
1374
|
+
{
|
|
1375
|
+
op: "new",
|
|
1376
|
+
kind: "error",
|
|
1377
|
+
text: detail
|
|
1378
|
+
? `reviewer retry budget exhausted: ${detail}`
|
|
1379
|
+
: "reviewer retry budget exhausted",
|
|
1380
|
+
},
|
|
1381
|
+
];
|
|
1382
|
+
}
|
|
1383
|
+
return [];
|
|
1384
|
+
}
|
|
1219
1385
|
return [];
|
|
1220
1386
|
}
|
|
1221
1387
|
|
|
@@ -1443,6 +1609,23 @@ export interface CreateReviewSessionOptions {
|
|
|
1443
1609
|
reviewPrompt: string;
|
|
1444
1610
|
/** Reviewer-only tools, built by the caller. */
|
|
1445
1611
|
customTools: unknown[];
|
|
1612
|
+
/**
|
|
1613
|
+
* Parent-side provider-registration surface (structural, feature-detected),
|
|
1614
|
+
* found with `findParentProviderSurface(ctx)` — the session's model registry
|
|
1615
|
+
* facade or its underlying model runtime.
|
|
1616
|
+
*
|
|
1617
|
+
* The reviewer session is deliberately created without ambient extensions,
|
|
1618
|
+
* so a provider registered only in the parent session (e.g. by a provider
|
|
1619
|
+
* extension) does not exist in the reviewer's fresh runtime. When this
|
|
1620
|
+
* surface is given, every provider the parent knows that the child does not
|
|
1621
|
+
* yet compose is registered onto the child BEFORE `createAgentSession` —
|
|
1622
|
+
* natively when the parent has a provider object, otherwise by config.
|
|
1623
|
+
* Absent or surface-less ⇒ nothing is seeded and the provider-auth preflight
|
|
1624
|
+
* (below) reports the generic escape hatch instead. Providers the child
|
|
1625
|
+
* already composes are skipped, so this is a no-op for every provider the
|
|
1626
|
+
* runtime builds in by construction.
|
|
1627
|
+
*/
|
|
1628
|
+
parentProviders?: unknown;
|
|
1446
1629
|
/** Injectable for tests; defaults to a real dynamic import. */
|
|
1447
1630
|
loadSdk?: ReviewSdkLoader;
|
|
1448
1631
|
}
|
|
@@ -1453,6 +1636,8 @@ export interface CreateReviewSessionResult {
|
|
|
1453
1636
|
modelLabel: string;
|
|
1454
1637
|
/** Tool names the child actually has enabled, when the SDK exposes them. */
|
|
1455
1638
|
activeTools?: string[];
|
|
1639
|
+
/** What provider seeding did before the session was created, when probed. */
|
|
1640
|
+
providerSeed?: ProviderSeedResult;
|
|
1456
1641
|
error?: string;
|
|
1457
1642
|
}
|
|
1458
1643
|
|
|
@@ -1601,6 +1786,36 @@ export async function createReviewSession(
|
|
|
1601
1786
|
};
|
|
1602
1787
|
}
|
|
1603
1788
|
|
|
1789
|
+
// Seed providers the child does not compose yet (e.g. providers registered
|
|
1790
|
+
// only in the parent session) BEFORE the session exists. Present-probe:
|
|
1791
|
+
// only the difference is registered, native-first, per-provider
|
|
1792
|
+
// fault-isolated — never fatal, never layer-aware.
|
|
1793
|
+
const providerSeed = seedChildRuntimeProviders(
|
|
1794
|
+
modelRuntime,
|
|
1795
|
+
options.parentProviders,
|
|
1796
|
+
);
|
|
1797
|
+
|
|
1798
|
+
// Provider-agnostic auth preflight, mirroring the child session's own
|
|
1799
|
+
// prompt gate: fail fast here — with the seed report and the generic
|
|
1800
|
+
// escape hatch — instead of mid-review with a bare "No API key found".
|
|
1801
|
+
const auth = await checkReviewerProviderAuth(
|
|
1802
|
+
modelRuntime,
|
|
1803
|
+
options.modelRef.provider,
|
|
1804
|
+
);
|
|
1805
|
+
if (!auth.configured) {
|
|
1806
|
+
return {
|
|
1807
|
+
ok: false,
|
|
1808
|
+
modelLabel,
|
|
1809
|
+
providerSeed,
|
|
1810
|
+
error: unconfiguredReviewerProviderMessage({
|
|
1811
|
+
providerId: options.modelRef.provider,
|
|
1812
|
+
seed: providerSeed,
|
|
1813
|
+
parentSurfaceFound: isParentProviderSurface(options.parentProviders),
|
|
1814
|
+
modelsJsonPath: path.join(options.agentDir, "models.json"),
|
|
1815
|
+
}),
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1604
1819
|
// No ambient extensions, no skills/prompts/themes: the reviewer gets exactly
|
|
1605
1820
|
// the system prompt below and nothing from the user's other packages.
|
|
1606
1821
|
// Project context files (AGENTS.md etc.) DO load — they are the framework
|