gentle-pi 3.2.0 → 3.3.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/assets/orchestrator-delegation.md +13 -8
- package/assets/orchestrator.md +2 -2
- package/docs/gentle-shell.md +40 -17
- package/docs/readme-reference.md +41 -7
- package/docs/review-integration.md +25 -11
- package/extensions/gentle-agents.ts +85 -17
- package/extensions/gentle-ai.ts +179 -12
- package/extensions/gentle-shell.ts +408 -38
- package/extensions/gentle-todo.ts +19 -1
- package/lib/agents-view.ts +41 -14
- package/lib/agents-widget.ts +84 -13
- package/lib/command-palette-catalog.ts +1 -0
- package/lib/double-esc-cancel-policy.ts +138 -0
- package/lib/inprocess-reviewer.ts +260 -0
- package/lib/model-routing-authority.ts +1 -1
- package/lib/native-review-cli.ts +23 -0
- package/lib/odd-runtime-delegation-gate.ts +88 -0
- package/lib/review-host-relay.ts +262 -94
- package/lib/review-integration-v2.ts +110 -26
- package/lib/shell-bar.ts +158 -29
- package/lib/shell-card.ts +19 -9
- package/lib/shell-changes-view.ts +43 -5
- package/lib/shell-changes.ts +92 -5
- package/lib/shell-hover.ts +39 -0
- package/lib/shell-prompt.ts +10 -1
- package/lib/shell-sidebar-layout.ts +111 -15
- package/lib/shell-sidebar.ts +16 -0
- package/lib/shell-todo.ts +7 -1
- package/lib/shell-usage-view.ts +98 -10
- package/lib/shell-usage.ts +226 -10
- package/package.json +2 -1
- package/runtime/native-review-cli.mjs +23 -0
- package/runtime/review-integration-v2.mjs +110 -26
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
- package/scripts/mirror-odd-routing.mjs +242 -0
- package/scripts/verify-package-files.mjs +3 -3
- package/tests/agents-grouping.test.ts +75 -18
- package/tests/agents-view.test.ts +28 -18
- package/tests/agents-widget.test.ts +100 -12
- package/tests/command-palette.test.ts +1 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +176 -138
- package/tests/double-esc-cancel-policy.test.ts +194 -0
- package/tests/gentle-agents.test.ts +528 -5
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +69 -5
- package/tests/gentle-shell.test.ts +903 -25
- package/tests/gentle-todo.test.ts +17 -4
- package/tests/inprocess-reviewer.test.ts +368 -0
- package/tests/maintainer/provider-relay.maintest.ts +101 -143
- package/tests/native-review-capability-contract.test.ts +32 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +293 -0
- package/tests/odd-routing-contract.test.ts +57 -0
- package/tests/odd-runtime-delegation-gate.test.ts +212 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/package-manifest.test.ts +6 -6
- package/tests/review-controller-native-routing.test.ts +60 -1
- package/tests/review-host-relay-routing.test.ts +77 -0
- package/tests/review-host-relay.test.ts +285 -239
- package/tests/review-integration-v2-forward.test.ts +61 -0
- package/tests/review-integration-v2.test.ts +116 -1
- package/tests/review-relay-transport-agent.test.ts +83 -0
- package/tests/runtime-harness.mjs +11 -0
- package/tests/session-changes-shell.test.ts +27 -0
- package/tests/session-worktree-registry.test.ts +41 -0
- package/tests/shell-bar.test.ts +224 -6
- package/tests/shell-card.test.ts +5 -3
- package/tests/shell-changes-view.test.ts +47 -0
- package/tests/shell-changes.test.ts +177 -0
- package/tests/shell-hover.test.ts +19 -0
- package/tests/shell-prompt.test.ts +20 -0
- package/tests/shell-sidebar-fullscreen.test.ts +59 -0
- package/tests/shell-sidebar-layout.test.ts +243 -5
- package/tests/shell-sidebar.test.ts +25 -1
- package/tests/shell-todo.test.ts +36 -0
- package/tests/shell-usage-view.test.ts +123 -3
- package/tests/shell-usage.test.ts +254 -6
- package/lib/opaque-pi-reviewer-adapter.ts +0 -284
- package/tests/opaque-pi-reviewer-adapter.test.ts +0 -266
package/lib/review-host-relay.ts
CHANGED
|
@@ -7,14 +7,18 @@
|
|
|
7
7
|
//
|
|
8
8
|
// 1. Run the exact provider-issued capture binding with `--agent pi
|
|
9
9
|
// --materialize` and take stdout as opaque prompt BYTES, verbatim.
|
|
10
|
-
// 2.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
10
|
+
// 2. Run that prompt through one in-process reviewer completion
|
|
11
|
+
// (lib/inprocess-reviewer.ts#runInProcessReviewer): resolve the lens's
|
|
12
|
+
// "provider/id" selection through the live model registry, authenticate
|
|
13
|
+
// through the registry's own resolver, and complete the frozen prompt as
|
|
14
|
+
// a single user message. There is no child process, no extension
|
|
15
|
+
// allowlist, and no ambient default model — a missing registry or a
|
|
16
|
+
// routing entry with no model is a typed refusal before materialize ever
|
|
17
|
+
// runs (gentle-ai#4611; gentle-pi#311 P2).
|
|
18
|
+
// 3. Submit the completion's text untouched through the provider-owned
|
|
19
|
+
// `submission` form carried by the collect input: execute its exact
|
|
20
|
+
// operation and argument tokens with only the tempfile path substituted
|
|
21
|
+
// into the declared {{value}} slot (BOM-less: the buffer is written
|
|
18
22
|
// byte-for-byte). The host never synthesizes or filters the completing
|
|
19
23
|
// form; a materialize slot without a provider submission is a typed
|
|
20
24
|
// contract mismatch, never a rebuilt invocation.
|
|
@@ -25,24 +29,22 @@
|
|
|
25
29
|
// never from transcript inference. The relay never parses or rebuilds
|
|
26
30
|
// binding, evidence, prompt, schema, budgets, or admission.
|
|
27
31
|
|
|
28
|
-
import { spawn } from "node:child_process";
|
|
29
32
|
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
33
|
+
import { spawn } from "node:child_process";
|
|
30
34
|
import { tmpdir } from "node:os";
|
|
31
35
|
import { isAbsolute, join } from "node:path";
|
|
36
|
+
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
32
37
|
import { resolveGentleAiBinary } from "./gentle-ai-binary.ts";
|
|
33
38
|
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
type
|
|
38
|
-
|
|
39
|
+
INPROCESS_REVIEWER_FAILURE,
|
|
40
|
+
runInProcessReviewer,
|
|
41
|
+
type InProcessReviewerFailureCode,
|
|
42
|
+
type InProcessReviewerOutcome,
|
|
43
|
+
type InProcessReviewerRegistry,
|
|
44
|
+
} from "./inprocess-reviewer.ts";
|
|
39
45
|
import { REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION, REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS, type ReviewCaptureSubmissionV1, type ReviewCollectInputV3 } from "./review-integration-v2.ts";
|
|
40
46
|
import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.ts";
|
|
41
47
|
|
|
42
|
-
// Compatibility export for existing relay consumers. The pure adapter owns the
|
|
43
|
-
// fixed Pi process boundary and its locked-down argv.
|
|
44
|
-
export { OPAQUE_PI_REVIEWER_ARGV as REVIEW_HOST_RELAY_PI_ARGV } from "./opaque-pi-reviewer-adapter.ts";
|
|
45
|
-
|
|
46
48
|
export const REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE =
|
|
47
49
|
"provider relay requires a gentle-ai build with the pi host relay surface";
|
|
48
50
|
|
|
@@ -52,14 +54,40 @@ export const REVIEW_HOST_RELAY_FAILURE = {
|
|
|
52
54
|
SUBMISSION_CONTRACT_MISMATCH: "submission-contract-mismatch",
|
|
53
55
|
MATERIALIZE_FAILED: "materialize-failed",
|
|
54
56
|
EMPTY_PROMPT: "empty-prompt",
|
|
57
|
+
// gentle-pi#311 P2: these four names predate the in-process completion,
|
|
58
|
+
// when a killed or crashed child process was the only way a reviewer
|
|
59
|
+
// failed. They stay exactly as they are — including their "pi" wording —
|
|
60
|
+
// because other test lanes (the maintainer provider-relay matrix, the
|
|
61
|
+
// restart-parity harness) construct `ReviewHostRelayError` literals with
|
|
62
|
+
// them directly and are out of this change's scope (gentle-pi#311 P4).
|
|
63
|
+
// PI_TIMED_OUT and PI_FAILED are still reachable from production: a
|
|
64
|
+
// reviewer completion that exceeds its bound or fails for any reason not
|
|
65
|
+
// covered by a more specific REVIEWER_* code below reuses them, since the
|
|
66
|
+
// semantics (a deterministic bound; a generic failure) carried over
|
|
67
|
+
// unchanged. PI_LAUNCH_FAILED and PI_EMPTY_OUTPUT are no longer produced
|
|
68
|
+
// by this module — there is no child to fail to launch, and empty output
|
|
69
|
+
// is now REVIEWER_EMPTY_OUTPUT with different evidence — but the codes
|
|
70
|
+
// stay defined for the lanes above.
|
|
55
71
|
PI_LAUNCH_FAILED: "pi-launch-failed",
|
|
56
72
|
PI_FAILED: "pi-failed",
|
|
57
|
-
// gentle-pi#367: a reviewer killed by the relay bound is not a crash. It
|
|
58
|
-
// is the one failure class that a byte-identical relaunch cannot survive,
|
|
59
|
-
// so it carries its own kind, its own elapsed/limit evidence, and its own
|
|
60
|
-
// continuation instead of hiding inside `pi-failed`.
|
|
61
73
|
PI_TIMED_OUT: "pi-timed-out",
|
|
62
74
|
PI_EMPTY_OUTPUT: "pi-empty-output",
|
|
75
|
+
// gentle-shell#1158 / #1136 (superseded by gentle-pi#311 P2): a caller-owned
|
|
76
|
+
// reviewer selection that cannot possibly complete — no model registry, or
|
|
77
|
+
// a routing entry with no configured model — is a configuration failure,
|
|
78
|
+
// refused typed before anything runs, never a mid-review transport
|
|
79
|
+
// mystery. The in-process path has no ambient default model to fall back
|
|
80
|
+
// to, so a missing selection is refused here rather than launched anyway.
|
|
81
|
+
REVIEWER_CONFIG_INVALID: "reviewer-config-invalid",
|
|
82
|
+
// gentle-pi#311 P2 — in-process completion outcomes with no equivalent
|
|
83
|
+
// above (lib/inprocess-reviewer.ts#INPROCESS_REVIEWER_FAILURE).
|
|
84
|
+
REVIEWER_MODEL_NOT_FOUND: "reviewer-model-not-found",
|
|
85
|
+
REVIEWER_AUTH_UNAVAILABLE: "reviewer-auth-unavailable",
|
|
86
|
+
REVIEWER_THINKING_INVALID: "reviewer-thinking-invalid",
|
|
87
|
+
REVIEWER_TOOL_CALL: "reviewer-tool-call-attempted",
|
|
88
|
+
REVIEWER_EMPTY_OUTPUT: "reviewer-empty-output",
|
|
89
|
+
REVIEWER_OUTPUT_TOO_LARGE: "reviewer-output-too-large",
|
|
90
|
+
REVIEWER_ABORTED: "reviewer-aborted",
|
|
63
91
|
SUBMISSION_REFUSED: "submission-refused",
|
|
64
92
|
} as const;
|
|
65
93
|
export type ReviewHostRelayFailureKind = (typeof REVIEW_HOST_RELAY_FAILURE)[keyof typeof REVIEW_HOST_RELAY_FAILURE];
|
|
@@ -77,10 +105,11 @@ export class ReviewHostRelayError extends Error {
|
|
|
77
105
|
readonly exitCode: number | null;
|
|
78
106
|
readonly stderr: string;
|
|
79
107
|
readonly timedOut: boolean;
|
|
80
|
-
// Wall time the
|
|
81
|
-
// was measured against. Both are null only when
|
|
82
|
-
// Without them a transport failure cannot be told apart
|
|
83
|
-
// is what forced the gentle-pi#367 reporter to measure
|
|
108
|
+
// Wall time the aborted, timed-out, or failed reviewer completion actually
|
|
109
|
+
// consumed, and the bound it was measured against. Both are null only when
|
|
110
|
+
// no completion ran. Without them a transport failure cannot be told apart
|
|
111
|
+
// from a crash, which is what forced the gentle-pi#367 reporter to measure
|
|
112
|
+
// the relay by hand.
|
|
84
113
|
readonly elapsedMs: number | null;
|
|
85
114
|
readonly timeoutMs: number | null;
|
|
86
115
|
// "none" until the submission invocation launches; a launched submission
|
|
@@ -90,7 +119,9 @@ export class ReviewHostRelayError extends Error {
|
|
|
90
119
|
// "none" again: the provider states that the lens slot was not consumed
|
|
91
120
|
// (gentle-pi#522 / #524).
|
|
92
121
|
readonly mutationOutcome: "none" | "unknown";
|
|
93
|
-
|
|
122
|
+
/** What the in-process reviewer outcome carried as structured evidence (e.g. the completion's stopReason on an empty-output refusal). */
|
|
123
|
+
readonly reviewerEvidence: Record<string, unknown> | undefined;
|
|
124
|
+
constructor(kind: ReviewHostRelayFailureKind, stage: ReviewHostRelayStage, message: string, details?: { exitCode?: number | null; stderr?: string; timedOut?: boolean; elapsedMs?: number; timeoutMs?: number; mutationOutcome?: "none" | "unknown"; reviewerEvidence?: Record<string, unknown> }) {
|
|
94
125
|
super(message);
|
|
95
126
|
this.name = "ReviewHostRelayError";
|
|
96
127
|
this.kind = kind;
|
|
@@ -101,6 +132,7 @@ export class ReviewHostRelayError extends Error {
|
|
|
101
132
|
this.elapsedMs = details?.elapsedMs ?? null;
|
|
102
133
|
this.timeoutMs = details?.timeoutMs ?? null;
|
|
103
134
|
this.mutationOutcome = details?.mutationOutcome ?? (stage === "submit" ? "unknown" : "none");
|
|
135
|
+
this.reviewerEvidence = details?.reviewerEvidence;
|
|
104
136
|
}
|
|
105
137
|
}
|
|
106
138
|
|
|
@@ -187,6 +219,16 @@ export interface ReviewHostRelaySlot {
|
|
|
187
219
|
readonly lens?: string;
|
|
188
220
|
readonly order?: string;
|
|
189
221
|
readonly subjectHash?: string;
|
|
222
|
+
/**
|
|
223
|
+
* Overrides the routing config key the reviewer selection resolves
|
|
224
|
+
* through (gentle-pi#311 P3). A lens slot leaves this unset and resolves
|
|
225
|
+
* through `lens` instead; a v9 host-mediated refuter/targeted-validator
|
|
226
|
+
* slot sets it to its fixed `review-refuter` / `review-validator` key,
|
|
227
|
+
* since those roles carry no per-slot lens identity.
|
|
228
|
+
*/
|
|
229
|
+
readonly routingKey?: string;
|
|
230
|
+
/** The provider-declared collect input name (e.g. `provider_refuter`), carried for diagnostics only. */
|
|
231
|
+
readonly name?: string;
|
|
190
232
|
}
|
|
191
233
|
|
|
192
234
|
function argumentValue(input: ReviewCollectInputV3, name: string): string | undefined {
|
|
@@ -248,6 +290,40 @@ export function reviewProviderRoleVectorSlots(inputs: readonly ReviewCollectInpu
|
|
|
248
290
|
}));
|
|
249
291
|
}
|
|
250
292
|
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// Host-mediated provider role slots (gentle-pi#311 P3; provider contract
|
|
295
|
+
// v9) — the same two role capture operations above, but rendered exactly
|
|
296
|
+
// like a lens materialize slot: binding tokens plus `--agent=pi
|
|
297
|
+
// --materialize=true` (never `--execute`) and a provider-owned submission
|
|
298
|
+
// descriptor. These slots run through the SAME relay machinery a lens slot
|
|
299
|
+
// does (`prepareReviewHostRelaySlot` / `submitReviewHostRelayPreparedResult`)
|
|
300
|
+
// — there is no second relay. The only role-specific parts are the fixed
|
|
301
|
+
// `routingKey` (there is no per-slot lens identity to read one from) and the
|
|
302
|
+
// input's own schema, which already names refuter vs targeted-validator in
|
|
303
|
+
// every refusal that carries the request.
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
const REVIEW_HOST_MEDIATED_ROLE_ROUTING_KEY: Record<ReviewProviderRoleVectorSlot["captureOperation"], "review-refuter" | "review-validator"> = {
|
|
307
|
+
[REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_REFUTER]: "review-refuter",
|
|
308
|
+
[REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION]: "review-validator",
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export function isReviewHostMediatedRoleCollectInput(input: ReviewCollectInputV3): boolean {
|
|
312
|
+
return (REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS as readonly string[]).includes(input.captureOperation)
|
|
313
|
+
&& argumentValue(input, "materialize") === "true"
|
|
314
|
+
&& argumentValue(input, "agent") === "pi"
|
|
315
|
+
&& input.submission !== undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function reviewHostMediatedRoleSlots(inputs: readonly ReviewCollectInputV3[]): readonly ReviewHostRelaySlot[] {
|
|
319
|
+
return inputs.filter((input) => isReviewHostMediatedRoleCollectInput(input)).map((input) => ({
|
|
320
|
+
captureArgumentTokens: input.arguments.map((argument) => renderToken(argument)),
|
|
321
|
+
submission: input.submission!,
|
|
322
|
+
routingKey: REVIEW_HOST_MEDIATED_ROLE_ROUTING_KEY[input.captureOperation as ReviewProviderRoleVectorSlot["captureOperation"]],
|
|
323
|
+
name: input.name,
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
|
|
251
327
|
// Resolves the provider-owned submission form into an executable binding.
|
|
252
328
|
// Fails closed with a typed contract-mismatch error whenever the completing
|
|
253
329
|
// form is absent or cannot bind exactly one artifact value; the relay never
|
|
@@ -291,10 +367,27 @@ export interface ReviewHostRelayRequest {
|
|
|
291
367
|
readonly submission?: ReviewCaptureSubmissionV1;
|
|
292
368
|
/** Absolute path; defaults to the verified package-local binary. */
|
|
293
369
|
readonly gentleAiExecutable?: string;
|
|
294
|
-
/** User-owned pi launcher; defaults to `pi` on PATH. */
|
|
295
|
-
readonly piExecutable?: string;
|
|
296
370
|
readonly environment?: NodeJS.ProcessEnv;
|
|
297
371
|
readonly gentleAiTimeoutMs?: number;
|
|
372
|
+
/**
|
|
373
|
+
* The live model registry the reviewer completion resolves its selection
|
|
374
|
+
* and credentials through — structurally, pi's own `ModelRegistry`
|
|
375
|
+
* (`ctx.modelRegistry`). Absent is a typed refusal before materialize ever
|
|
376
|
+
* runs; the relay never falls back to a child process or an ambient
|
|
377
|
+
* default model (gentle-ai#4611; gentle-pi#311 P2).
|
|
378
|
+
*/
|
|
379
|
+
readonly reviewerRegistry?: InProcessReviewerRegistry;
|
|
380
|
+
/**
|
|
381
|
+
* The lens's user-owned "provider/id" selection, read from the agent model
|
|
382
|
+
* routing config's `review-<lens>` entry. The relay never invents one: a
|
|
383
|
+
* routing entry with no configured model is refused typed before
|
|
384
|
+
* materialize, naming {@link ReviewHostRelayRequest.routingKey}.
|
|
385
|
+
*/
|
|
386
|
+
readonly selection?: string;
|
|
387
|
+
/** The routing entry's thinking label, forwarded verbatim to the completion. */
|
|
388
|
+
readonly thinking?: string;
|
|
389
|
+
/** Names the routing config key (e.g. "review-risk") in refusal messages; defaults to a generic label when absent. */
|
|
390
|
+
readonly routingKey?: string;
|
|
298
391
|
/**
|
|
299
392
|
* Overrides the reviewer bound entirely. Production leaves it unset and the
|
|
300
393
|
* relay derives the bound from the materialized prompt bytes and
|
|
@@ -329,7 +422,7 @@ export type ReviewHostRelaySubmissionRunner = (prepared: ReviewHostRelayPrepared
|
|
|
329
422
|
const DEFAULT_GENTLE_AI_TIMEOUT_MS = 120_000;
|
|
330
423
|
|
|
331
424
|
// ---------------------------------------------------------------------------
|
|
332
|
-
// The reviewer
|
|
425
|
+
// The reviewer completion bound (gentle-pi#367).
|
|
333
426
|
//
|
|
334
427
|
// The previous bound was a single hardcoded 600_000 ms reachable only through
|
|
335
428
|
// the test-injectable runner. A field-measured lens legitimately needed 478s
|
|
@@ -351,7 +444,7 @@ const DEFAULT_GENTLE_AI_TIMEOUT_MS = 120_000;
|
|
|
351
444
|
// established numeric-override shape (GENTLE_PI_CANDIDATE_GIT_TIMEOUT_MS,
|
|
352
445
|
// GENTLE_PI_REVIEW_MAX_BUFFER_BYTES): a positive decimal, silently ignored
|
|
353
446
|
// when malformed, and clamped to the same hard ceiling so no configuration can
|
|
354
|
-
// turn a foreground FINALIZE into an unbounded
|
|
447
|
+
// turn a foreground FINALIZE into an unbounded completion.
|
|
355
448
|
// ---------------------------------------------------------------------------
|
|
356
449
|
|
|
357
450
|
export const REVIEW_HOST_RELAY_PI_TIMEOUT_ENV = "GENTLE_PI_REVIEW_RELAY_PI_TIMEOUT_MS";
|
|
@@ -371,11 +464,12 @@ export function resolveReviewHostRelayPiTimeoutMs(promptByteLength: number, envi
|
|
|
371
464
|
return Math.min(scaled, REVIEW_HOST_RELAY_PI_TIMEOUT_MAX_MS);
|
|
372
465
|
}
|
|
373
466
|
|
|
374
|
-
// The reviewer ran out of time; it did not crash. The message
|
|
375
|
-
// measurements and names the two things that can change the
|
|
376
|
-
// the one thing that cannot is relaunching the identical
|
|
467
|
+
// The reviewer completion ran out of time; it did not crash. The message
|
|
468
|
+
// states both measurements and names the two things that can change the
|
|
469
|
+
// outcome, because the one thing that cannot is relaunching the identical
|
|
470
|
+
// slot.
|
|
377
471
|
export function reviewHostRelayPiTimeoutMessage(elapsedMs: number, timeoutMs: number, promptByteLength: number): string {
|
|
378
|
-
return `
|
|
472
|
+
return `the reviewer completion exceeded the relay bound: aborted after ${elapsedMs}ms against a ${timeoutMs}ms limit for a ${promptByteLength}-byte materialized prompt. `
|
|
379
473
|
+ `Relaunching the same slot unchanged reaches the same wall. Raise ${REVIEW_HOST_RELAY_PI_TIMEOUT_ENV} above the reviewer's real wall time (ceiling ${REVIEW_HOST_RELAY_PI_TIMEOUT_MAX_MS}ms) or reduce the candidate scope so the materialized prompt is smaller.`;
|
|
380
474
|
}
|
|
381
475
|
|
|
@@ -436,43 +530,48 @@ function collectGentleAiProcess(
|
|
|
436
530
|
});
|
|
437
531
|
}
|
|
438
532
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
REVIEW_HOST_RELAY_FAILURE.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
533
|
+
// Maps one in-process reviewer refusal onto a typed relay error. TIMED_OUT
|
|
534
|
+
// reuses PI_TIMED_OUT and PROVIDER_FAILED reuses PI_FAILED (identical
|
|
535
|
+
// semantics: a deterministic bound; a generic failure bucket); every other
|
|
536
|
+
// code gets its own REVIEWER_* kind with no prior equivalent.
|
|
537
|
+
function relayReviewerRefusalError(
|
|
538
|
+
outcome: Extract<InProcessReviewerOutcome, { kind: "refused" }>,
|
|
539
|
+
timing: { elapsedMs: number; timeoutMs: number },
|
|
540
|
+
promptByteLength: number,
|
|
541
|
+
): ReviewHostRelayError {
|
|
542
|
+
const details = { ...timing, ...(outcome.evidence === undefined ? {} : { reviewerEvidence: outcome.evidence }) };
|
|
543
|
+
const code: InProcessReviewerFailureCode = outcome.code;
|
|
544
|
+
switch (code) {
|
|
545
|
+
case INPROCESS_REVIEWER_FAILURE.MODEL_NOT_FOUND:
|
|
546
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_MODEL_NOT_FOUND, "pi", outcome.message, details);
|
|
547
|
+
case INPROCESS_REVIEWER_FAILURE.AUTH_UNAVAILABLE:
|
|
548
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_AUTH_UNAVAILABLE, "pi", outcome.message, details);
|
|
549
|
+
case INPROCESS_REVIEWER_FAILURE.THINKING_INVALID:
|
|
550
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_THINKING_INVALID, "pi", outcome.message, details);
|
|
551
|
+
case INPROCESS_REVIEWER_FAILURE.TOOL_CALL_ATTEMPTED:
|
|
552
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_TOOL_CALL, "pi", outcome.message, details);
|
|
553
|
+
case INPROCESS_REVIEWER_FAILURE.EMPTY_OUTPUT:
|
|
554
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_EMPTY_OUTPUT, "pi", outcome.message, details);
|
|
555
|
+
case INPROCESS_REVIEWER_FAILURE.OUTPUT_TOO_LARGE:
|
|
556
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_OUTPUT_TOO_LARGE, "pi", outcome.message, details);
|
|
557
|
+
case INPROCESS_REVIEWER_FAILURE.ABORTED:
|
|
558
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_ABORTED, "pi", outcome.message, details);
|
|
559
|
+
case INPROCESS_REVIEWER_FAILURE.SELECTION_INVALID:
|
|
560
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_CONFIG_INVALID, "pi", outcome.message, details);
|
|
561
|
+
case INPROCESS_REVIEWER_FAILURE.TIMED_OUT:
|
|
562
|
+
return new ReviewHostRelayError(
|
|
563
|
+
REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT,
|
|
564
|
+
"pi",
|
|
565
|
+
reviewHostRelayPiTimeoutMessage(timing.elapsedMs, timing.timeoutMs, promptByteLength),
|
|
566
|
+
{ ...details, timedOut: true },
|
|
567
|
+
);
|
|
568
|
+
case INPROCESS_REVIEWER_FAILURE.PROVIDER_FAILED:
|
|
569
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", outcome.message, details);
|
|
570
|
+
default: {
|
|
571
|
+
const unreachable: never = code;
|
|
572
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", `unrecognized reviewer refusal code ${String(unreachable)}`, details);
|
|
573
|
+
}
|
|
474
574
|
}
|
|
475
|
-
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", "pi subprocess failed", details);
|
|
476
575
|
}
|
|
477
576
|
|
|
478
577
|
function assertTokens(name: string, tokens: readonly string[]): void {
|
|
@@ -482,14 +581,52 @@ function assertTokens(name: string, tokens: readonly string[]): void {
|
|
|
482
581
|
}
|
|
483
582
|
}
|
|
484
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Validates the caller-owned reviewer selection before any process launches: a
|
|
586
|
+
* missing model registry or a routing entry with no configured model is a
|
|
587
|
+
* typed refusal, never a mid-review transport failure and never a fallback to
|
|
588
|
+
* an ambient default model (gentle-ai#4611; gentle-pi#311 P2, superseding
|
|
589
|
+
* gentle-shell#1158 / #1136's child-process launch configuration).
|
|
590
|
+
*/
|
|
591
|
+
function validateReviewerSelectionConfiguration(request: ReviewHostRelayRequest): { reviewerRegistry: InProcessReviewerRegistry; selection: string; thinking?: string; routingKey: string } {
|
|
592
|
+
const routingKey = typeof request.routingKey === "string" && request.routingKey.length > 0 ? request.routingKey : "review capture";
|
|
593
|
+
if (request.reviewerRegistry === undefined) {
|
|
594
|
+
throw new ReviewHostRelayError(
|
|
595
|
+
REVIEW_HOST_RELAY_FAILURE.REVIEWER_CONFIG_INVALID,
|
|
596
|
+
"pi",
|
|
597
|
+
`Pi host relay reviewer launch configuration is invalid: no model registry is available to complete ${routingKey}`,
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
if (typeof request.selection !== "string" || request.selection.length === 0) {
|
|
601
|
+
throw new ReviewHostRelayError(
|
|
602
|
+
REVIEW_HOST_RELAY_FAILURE.REVIEWER_CONFIG_INVALID,
|
|
603
|
+
"pi",
|
|
604
|
+
`Pi host relay reviewer launch configuration is invalid: no model is configured for ${routingKey}; assign it a model in the agent model routing config`,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
reviewerRegistry: request.reviewerRegistry,
|
|
609
|
+
selection: request.selection,
|
|
610
|
+
...(request.thinking === undefined ? {} : { thinking: request.thinking }),
|
|
611
|
+
routingKey,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
485
615
|
function snapshotReviewHostRelayRequest(request: ReviewHostRelayRequest): ReviewHostRelayRequest {
|
|
616
|
+
// Structural malformations (empty/blank tokens, a non-absolute executable)
|
|
617
|
+
// are TypeErrors — a programmer mistake, never a typed relay refusal — and
|
|
618
|
+
// are checked before the business-level reviewer selection below.
|
|
486
619
|
assertTokens("capture", request.captureArgumentTokens);
|
|
620
|
+
const gentleAiExecutable = request.gentleAiExecutable ?? resolveGentleAiBinary();
|
|
621
|
+
if (!isAbsolute(gentleAiExecutable)) throw new TypeError("Pi host relay requires an absolute gentle-ai executable path");
|
|
622
|
+
// Caller-owned reviewer selection is validated before any process launches:
|
|
623
|
+
// a broken configuration is a typed refusal, never a mid-review transport
|
|
624
|
+
// failure (gentle-pi#311 P2).
|
|
625
|
+
const reviewerSelection = validateReviewerSelectionConfiguration(request);
|
|
487
626
|
// The completing form is validated before any process launches: a materialize
|
|
488
627
|
// slot without a provider-owned submission is a typed contract mismatch,
|
|
489
628
|
// never a synthesized invocation.
|
|
490
629
|
resolveReviewHostRelaySubmission(request.submission);
|
|
491
|
-
const gentleAiExecutable = request.gentleAiExecutable ?? resolveGentleAiBinary();
|
|
492
|
-
if (!isAbsolute(gentleAiExecutable)) throw new TypeError("Pi host relay requires an absolute gentle-ai executable path");
|
|
493
630
|
const environment = Object.freeze({ ...(request.environment ?? process.env) }) as NodeJS.ProcessEnv;
|
|
494
631
|
const submission = request.submission === undefined ? undefined : Object.freeze({
|
|
495
632
|
operationToken: request.submission.operationToken,
|
|
@@ -500,6 +637,7 @@ function snapshotReviewHostRelayRequest(request: ReviewHostRelayRequest): Review
|
|
|
500
637
|
...request,
|
|
501
638
|
captureArgumentTokens: Object.freeze([...request.captureArgumentTokens]),
|
|
502
639
|
...(submission === undefined ? {} : { submission }),
|
|
640
|
+
...reviewerSelection,
|
|
503
641
|
gentleAiExecutable,
|
|
504
642
|
environment,
|
|
505
643
|
gentleAiTimeoutMs: request.gentleAiTimeoutMs ?? DEFAULT_GENTLE_AI_TIMEOUT_MS,
|
|
@@ -508,13 +646,14 @@ function snapshotReviewHostRelayRequest(request: ReviewHostRelayRequest): Review
|
|
|
508
646
|
}
|
|
509
647
|
|
|
510
648
|
/**
|
|
511
|
-
* Materializes one provider-bound reviewer prompt and runs
|
|
512
|
-
*
|
|
513
|
-
* finish before the caller performs provider-ordered
|
|
649
|
+
* Materializes one provider-bound reviewer prompt and runs it through one
|
|
650
|
+
* in-process reviewer completion. It does not submit anything, so independent
|
|
651
|
+
* reviewer work can finish before the caller performs provider-ordered
|
|
652
|
+
* admission.
|
|
514
653
|
*/
|
|
515
654
|
export async function prepareReviewHostRelaySlot(
|
|
516
655
|
request: ReviewHostRelayRequest,
|
|
517
|
-
|
|
656
|
+
runReviewer: typeof runInProcessReviewer = runInProcessReviewer,
|
|
518
657
|
): Promise<ReviewHostRelayPreparedResult> {
|
|
519
658
|
// Copy mutable transport configuration before the first async boundary. The
|
|
520
659
|
// supplied AbortSignal intentionally stays live across materialize, reviewer,
|
|
@@ -523,9 +662,14 @@ export async function prepareReviewHostRelaySlot(
|
|
|
523
662
|
|
|
524
663
|
// The provider materializes the opaque prompt and detects whether this relay
|
|
525
664
|
// surface is available. No version sniffing or prompt reconstruction occurs.
|
|
665
|
+
// The materialize subcommand is the provider's own submission operation
|
|
666
|
+
// token (validated present above): "capture-result" for a lens slot,
|
|
667
|
+
// "capture-refuter" or "capture-validation" for a v9 host-mediated role
|
|
668
|
+
// slot — the provider always names the same operation for both the
|
|
669
|
+
// materialize and the submit leg of one slot.
|
|
526
670
|
let materialized: ProcessCapture;
|
|
527
671
|
try {
|
|
528
|
-
materialized = await collectGentleAiProcess(preparedRequest.gentleAiExecutable!, ["review",
|
|
672
|
+
materialized = await collectGentleAiProcess(preparedRequest.gentleAiExecutable!, ["review", preparedRequest.submission!.operationToken, ...preparedRequest.captureArgumentTokens], {
|
|
529
673
|
cwd: preparedRequest.targetCwd!,
|
|
530
674
|
env: { ...preparedRequest.environment!, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT },
|
|
531
675
|
timeoutMs: preparedRequest.gentleAiTimeoutMs!,
|
|
@@ -572,25 +716,43 @@ export async function prepareReviewHostRelaySlot(
|
|
|
572
716
|
// both the user-owned environment override and the scale-derived bound.
|
|
573
717
|
const piTimeoutMs = preparedRequest.piTimeoutMs ?? resolveReviewHostRelayPiTimeoutMs(promptBytes.length, preparedRequest.environment);
|
|
574
718
|
|
|
575
|
-
// The
|
|
576
|
-
//
|
|
577
|
-
|
|
719
|
+
// The completion runs in-process through the live model registry: no
|
|
720
|
+
// child, no extension allowlist, no ambient default model. Elapsed time is
|
|
721
|
+
// measured here (there is no killed process to read it from) so a timed-out
|
|
722
|
+
// or aborted refusal still carries the same elapsed/limit evidence a killed
|
|
723
|
+
// child used to.
|
|
724
|
+
const startedAt = Date.now();
|
|
725
|
+
let outcome: InProcessReviewerOutcome;
|
|
578
726
|
try {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
727
|
+
outcome = await runReviewer(
|
|
728
|
+
{
|
|
729
|
+
selection: preparedRequest.selection!,
|
|
730
|
+
...(preparedRequest.thinking === undefined ? {} : { thinking: preparedRequest.thinking }),
|
|
731
|
+
prompt: promptBytes,
|
|
732
|
+
timeoutMs: piTimeoutMs,
|
|
733
|
+
...(preparedRequest.signal === undefined ? {} : { signal: preparedRequest.signal }),
|
|
734
|
+
routingKey: preparedRequest.routingKey!,
|
|
735
|
+
},
|
|
736
|
+
{ registry: preparedRequest.reviewerRegistry!, complete: completeSimple },
|
|
737
|
+
);
|
|
585
738
|
} catch (error) {
|
|
586
|
-
throw
|
|
739
|
+
throw new ReviewHostRelayError(
|
|
740
|
+
REVIEW_HOST_RELAY_FAILURE.PI_FAILED,
|
|
741
|
+
"pi",
|
|
742
|
+
`the reviewer completion could not run: ${error instanceof Error ? error.message : String(error)}`,
|
|
743
|
+
{ elapsedMs: Date.now() - startedAt, timeoutMs: piTimeoutMs },
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
if (outcome.kind === "refused") {
|
|
747
|
+
throw relayReviewerRefusalError(outcome, { elapsedMs: Date.now() - startedAt, timeoutMs: piTimeoutMs }, promptBytes.length);
|
|
587
748
|
}
|
|
749
|
+
const resultBytes = Buffer.from(outcome.text, "utf8");
|
|
588
750
|
const prepared = Object.freeze({
|
|
589
751
|
request: preparedRequest,
|
|
590
752
|
promptByteLength: promptBytes.length,
|
|
591
|
-
resultByteLength:
|
|
753
|
+
resultByteLength: resultBytes.length,
|
|
592
754
|
});
|
|
593
|
-
preparedResultBytes.set(prepared,
|
|
755
|
+
preparedResultBytes.set(prepared, resultBytes);
|
|
594
756
|
return prepared;
|
|
595
757
|
}
|
|
596
758
|
|
|
@@ -684,9 +846,15 @@ export async function submitReviewHostRelayPreparedResult(prepared: ReviewHostRe
|
|
|
684
846
|
}
|
|
685
847
|
|
|
686
848
|
/**
|
|
687
|
-
* Compatibility one-binding path: materialize →
|
|
688
|
-
* It preserves the established API and its typed
|
|
849
|
+
* Compatibility one-binding path: materialize → in-process reviewer
|
|
850
|
+
* completion → submit. It preserves the established API and its typed
|
|
851
|
+
* failure behavior exactly. `runReviewer` is the same test seam
|
|
852
|
+
* {@link prepareReviewHostRelaySlot} takes, threaded through so a caller never
|
|
853
|
+
* needs to call the two-step path just to inject a fake completion.
|
|
689
854
|
*/
|
|
690
|
-
export async function runReviewHostRelaySlot(
|
|
691
|
-
|
|
855
|
+
export async function runReviewHostRelaySlot(
|
|
856
|
+
request: ReviewHostRelayRequest,
|
|
857
|
+
runReviewer: typeof runInProcessReviewer = runInProcessReviewer,
|
|
858
|
+
): Promise<ReviewHostRelayResult> {
|
|
859
|
+
return await submitReviewHostRelayPreparedResult(await prepareReviewHostRelaySlot(request, runReviewer));
|
|
692
860
|
}
|