@j0hanz/code-review-analyst-mcp 1.5.0 → 1.5.1
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 +15 -11
- package/dist/lib/diff-parser.js +33 -13
- package/dist/lib/diff-store.d.ts +2 -0
- package/dist/lib/gemini.d.ts +4 -0
- package/dist/lib/gemini.js +283 -18
- package/dist/lib/tool-contracts.d.ts +9 -0
- package/dist/lib/tool-contracts.js +7 -0
- package/dist/lib/tool-factory.d.ts +5 -0
- package/dist/lib/tool-factory.js +51 -3
- package/dist/lib/types.d.ts +6 -0
- package/dist/resources/server-config.js +15 -0
- package/dist/tools/analyze-complexity.js +3 -0
- package/dist/tools/analyze-pr-impact.js +5 -2
- package/dist/tools/detect-api-breaking.js +3 -0
- package/dist/tools/generate-diff.js +1 -1
- package/dist/tools/generate-review-summary.js +13 -4
- package/dist/tools/generate-test-plan.js +5 -2
- package/dist/tools/inspect-code-quality.js +5 -2
- package/dist/tools/suggest-search-replace.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -371,17 +371,21 @@ Create a test plan covering the changes in the diff using the Flash model with t
|
|
|
371
371
|
|
|
372
372
|
### Environment Variables
|
|
373
373
|
|
|
374
|
-
| Variable
|
|
375
|
-
|
|
|
376
|
-
| `GEMINI_API_KEY`
|
|
377
|
-
| `GOOGLE_API_KEY`
|
|
378
|
-
| `GEMINI_MODEL`
|
|
379
|
-
| `GEMINI_HARM_BLOCK_THRESHOLD`
|
|
380
|
-
| `MAX_DIFF_CHARS`
|
|
381
|
-
| `MAX_CONTEXT_CHARS`
|
|
382
|
-
| `MAX_CONCURRENT_CALLS`
|
|
383
|
-
| `
|
|
384
|
-
| `
|
|
374
|
+
| Variable | Description | Default | Required |
|
|
375
|
+
| ------------------------------- | ---------------------------------------------------- | ------------ | -------- |
|
|
376
|
+
| `GEMINI_API_KEY` | Gemini API key | — | Yes |
|
|
377
|
+
| `GOOGLE_API_KEY` | Alternative API key (if `GEMINI_API_KEY` not set) | — | No |
|
|
378
|
+
| `GEMINI_MODEL` | Override default model selection | — | No |
|
|
379
|
+
| `GEMINI_HARM_BLOCK_THRESHOLD` | Safety threshold (BLOCK_NONE, BLOCK_ONLY_HIGH, etc.) | `BLOCK_NONE` | No |
|
|
380
|
+
| `MAX_DIFF_CHARS` | Max chars for diff input | `120000` | No |
|
|
381
|
+
| `MAX_CONTEXT_CHARS` | Max combined context for inspection | `500000` | No |
|
|
382
|
+
| `MAX_CONCURRENT_CALLS` | Max concurrent Gemini requests | `10` | No |
|
|
383
|
+
| `MAX_CONCURRENT_BATCH_CALLS` | Max concurrent inline batch requests | `2` | No |
|
|
384
|
+
| `MAX_CONCURRENT_CALLS_WAIT_MS` | Max wait time for a free Gemini slot | `2000` | No |
|
|
385
|
+
| `MAX_SCHEMA_RETRY_ERROR_CHARS` | Max chars from schema error injected into retry text | `1500` | No |
|
|
386
|
+
| `GEMINI_BATCH_MODE` | Request mode for Gemini calls (`off`, `inline`) | `off` | No |
|
|
387
|
+
| `GEMINI_BATCH_POLL_INTERVAL_MS` | Poll interval for batch job status | `2000` | No |
|
|
388
|
+
| `GEMINI_BATCH_TIMEOUT_MS` | Max wait for batch completion | `120000` | No |
|
|
385
389
|
|
|
386
390
|
### Models
|
|
387
391
|
|
package/dist/lib/diff-parser.js
CHANGED
|
@@ -32,24 +32,35 @@ function sortPaths(paths) {
|
|
|
32
32
|
}
|
|
33
33
|
return Array.from(paths).sort(PATH_SORTER);
|
|
34
34
|
}
|
|
35
|
-
function buildDiffComputation(files) {
|
|
35
|
+
function buildDiffComputation(files, options) {
|
|
36
36
|
let added = 0;
|
|
37
37
|
let deleted = 0;
|
|
38
|
-
const paths = new Set();
|
|
39
|
-
const summaries =
|
|
38
|
+
const paths = options.needPaths ? new Set() : undefined;
|
|
39
|
+
const summaries = options.needSummaries
|
|
40
|
+
? new Array(files.length)
|
|
41
|
+
: undefined;
|
|
40
42
|
let index = 0;
|
|
41
43
|
for (const file of files) {
|
|
42
44
|
added += file.additions;
|
|
43
45
|
deleted += file.deletions;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
paths
|
|
46
|
+
if (options.needPaths || options.needSummaries) {
|
|
47
|
+
const path = resolveChangedPath(file);
|
|
48
|
+
if (paths && path) {
|
|
49
|
+
paths.add(path);
|
|
50
|
+
}
|
|
51
|
+
if (summaries) {
|
|
52
|
+
summaries[index] =
|
|
53
|
+
`${path ?? UNKNOWN_PATH} (+${file.additions} -${file.deletions})`;
|
|
54
|
+
}
|
|
47
55
|
}
|
|
48
|
-
summaries[index] =
|
|
49
|
-
`${path ?? UNKNOWN_PATH} (+${file.additions} -${file.deletions})`;
|
|
50
56
|
index += 1;
|
|
51
57
|
}
|
|
52
|
-
return {
|
|
58
|
+
return {
|
|
59
|
+
added,
|
|
60
|
+
deleted,
|
|
61
|
+
paths: paths ?? new Set(),
|
|
62
|
+
summaries: summaries ?? [],
|
|
63
|
+
};
|
|
53
64
|
}
|
|
54
65
|
function buildStats(filesCount, added, deleted) {
|
|
55
66
|
return { files: filesCount, added, deleted };
|
|
@@ -61,7 +72,10 @@ export function computeDiffStatsAndSummaryFromFiles(files) {
|
|
|
61
72
|
summary: NO_FILES_CHANGED,
|
|
62
73
|
};
|
|
63
74
|
}
|
|
64
|
-
const computed = buildDiffComputation(files
|
|
75
|
+
const computed = buildDiffComputation(files, {
|
|
76
|
+
needPaths: false,
|
|
77
|
+
needSummaries: true,
|
|
78
|
+
});
|
|
65
79
|
const stats = buildStats(files.length, computed.added, computed.deleted);
|
|
66
80
|
return {
|
|
67
81
|
stats,
|
|
@@ -75,7 +89,10 @@ export function computeDiffStatsAndPathsFromFiles(files) {
|
|
|
75
89
|
paths: EMPTY_PATHS,
|
|
76
90
|
};
|
|
77
91
|
}
|
|
78
|
-
const computed = buildDiffComputation(files
|
|
92
|
+
const computed = buildDiffComputation(files, {
|
|
93
|
+
needPaths: true,
|
|
94
|
+
needSummaries: false,
|
|
95
|
+
});
|
|
79
96
|
return {
|
|
80
97
|
stats: buildStats(files.length, computed.added, computed.deleted),
|
|
81
98
|
paths: sortPaths(computed.paths),
|
|
@@ -86,7 +103,7 @@ export function extractChangedPathsFromFiles(files) {
|
|
|
86
103
|
if (files.length === 0) {
|
|
87
104
|
return EMPTY_PATHS;
|
|
88
105
|
}
|
|
89
|
-
return sortPaths(buildDiffComputation(files).paths);
|
|
106
|
+
return sortPaths(buildDiffComputation(files, { needPaths: true, needSummaries: false }).paths);
|
|
90
107
|
}
|
|
91
108
|
/** Extract all unique changed file paths (renamed: returns new path). */
|
|
92
109
|
export function extractChangedPaths(diff) {
|
|
@@ -96,7 +113,10 @@ export function computeDiffStatsFromFiles(files) {
|
|
|
96
113
|
if (files.length === 0) {
|
|
97
114
|
return EMPTY_STATS;
|
|
98
115
|
}
|
|
99
|
-
const computed = buildDiffComputation(files
|
|
116
|
+
const computed = buildDiffComputation(files, {
|
|
117
|
+
needPaths: false,
|
|
118
|
+
needSummaries: false,
|
|
119
|
+
});
|
|
100
120
|
return buildStats(files.length, computed.added, computed.deleted);
|
|
101
121
|
}
|
|
102
122
|
/** Count changed files, added lines, and deleted lines. */
|
package/dist/lib/diff-store.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { ParsedFile } from './diff-parser.js';
|
|
2
3
|
import { createErrorToolResponse } from './tool-response.js';
|
|
3
4
|
export declare const DIFF_RESOURCE_URI = "diff://current";
|
|
4
5
|
export interface DiffStats {
|
|
@@ -8,6 +9,7 @@ export interface DiffStats {
|
|
|
8
9
|
}
|
|
9
10
|
export interface DiffSlot {
|
|
10
11
|
diff: string;
|
|
12
|
+
parsedFiles: readonly ParsedFile[];
|
|
11
13
|
stats: DiffStats;
|
|
12
14
|
generatedAt: string;
|
|
13
15
|
mode: string;
|
package/dist/lib/gemini.d.ts
CHANGED
|
@@ -4,4 +4,8 @@ import type { GeminiStructuredRequest } from './types.js';
|
|
|
4
4
|
export declare const geminiEvents: EventEmitter<[never]>;
|
|
5
5
|
export declare function getCurrentRequestId(): string;
|
|
6
6
|
export declare function setClientForTesting(client: GoogleGenAI): void;
|
|
7
|
+
export declare function getGeminiQueueSnapshot(): {
|
|
8
|
+
activeCalls: number;
|
|
9
|
+
waitingCalls: number;
|
|
10
|
+
};
|
|
7
11
|
export declare function generateStructuredJson(request: GeminiStructuredRequest): Promise<unknown>;
|
package/dist/lib/gemini.js
CHANGED
|
@@ -13,6 +13,7 @@ const DEFAULT_MODEL = 'gemini-3-flash-preview';
|
|
|
13
13
|
const GEMINI_MODEL_ENV_VAR = 'GEMINI_MODEL';
|
|
14
14
|
const GEMINI_HARM_BLOCK_THRESHOLD_ENV_VAR = 'GEMINI_HARM_BLOCK_THRESHOLD';
|
|
15
15
|
const GEMINI_INCLUDE_THOUGHTS_ENV_VAR = 'GEMINI_INCLUDE_THOUGHTS';
|
|
16
|
+
const GEMINI_BATCH_MODE_ENV_VAR = 'GEMINI_BATCH_MODE';
|
|
16
17
|
const GEMINI_API_KEY_ENV_VAR = 'GEMINI_API_KEY';
|
|
17
18
|
const GOOGLE_API_KEY_ENV_VAR = 'GOOGLE_API_KEY';
|
|
18
19
|
function getDefaultModel() {
|
|
@@ -30,14 +31,20 @@ const RETRY_DELAY_MAX_MS = 5_000;
|
|
|
30
31
|
const RETRY_JITTER_RATIO = 0.2;
|
|
31
32
|
const DEFAULT_SAFETY_THRESHOLD = HarmBlockThreshold.BLOCK_NONE;
|
|
32
33
|
const DEFAULT_INCLUDE_THOUGHTS = false;
|
|
34
|
+
const DEFAULT_BATCH_MODE = 'off';
|
|
33
35
|
const UNKNOWN_REQUEST_CONTEXT_VALUE = 'unknown';
|
|
34
36
|
const RETRYABLE_NUMERIC_CODES = new Set([429, 500, 502, 503, 504]);
|
|
35
37
|
const DIGITS_ONLY_PATTERN = /^\d+$/;
|
|
36
38
|
const SLEEP_UNREF_OPTIONS = { ref: false };
|
|
37
39
|
const maxConcurrentCallsConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS', 10);
|
|
40
|
+
const maxConcurrentBatchCallsConfig = createCachedEnvInt('MAX_CONCURRENT_BATCH_CALLS', 2);
|
|
38
41
|
const concurrencyWaitMsConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS_WAIT_MS', 2_000);
|
|
42
|
+
const batchPollIntervalMsConfig = createCachedEnvInt('GEMINI_BATCH_POLL_INTERVAL_MS', 2_000);
|
|
43
|
+
const batchTimeoutMsConfig = createCachedEnvInt('GEMINI_BATCH_TIMEOUT_MS', 120_000);
|
|
39
44
|
let activeCalls = 0;
|
|
45
|
+
let activeBatchCalls = 0;
|
|
40
46
|
const slotWaiters = [];
|
|
47
|
+
const batchSlotWaiters = [];
|
|
41
48
|
const RETRYABLE_TRANSIENT_CODES = new Set([
|
|
42
49
|
'RESOURCE_EXHAUSTED',
|
|
43
50
|
'UNAVAILABLE',
|
|
@@ -149,6 +156,25 @@ function getDefaultIncludeThoughts() {
|
|
|
149
156
|
cachedIncludeThoughts = parseBooleanEnv(value) ?? DEFAULT_INCLUDE_THOUGHTS;
|
|
150
157
|
return cachedIncludeThoughts;
|
|
151
158
|
}
|
|
159
|
+
function getDefaultBatchMode() {
|
|
160
|
+
const value = process.env[GEMINI_BATCH_MODE_ENV_VAR]?.trim().toLowerCase();
|
|
161
|
+
if (value === 'inline') {
|
|
162
|
+
return 'inline';
|
|
163
|
+
}
|
|
164
|
+
return DEFAULT_BATCH_MODE;
|
|
165
|
+
}
|
|
166
|
+
function applyResponseKeyOrdering(responseSchema, responseKeyOrdering) {
|
|
167
|
+
if (!responseKeyOrdering || responseKeyOrdering.length === 0) {
|
|
168
|
+
return responseSchema;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
...responseSchema,
|
|
172
|
+
propertyOrdering: [...responseKeyOrdering],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function getPromptWithFunctionCallingContext(request) {
|
|
176
|
+
return request.prompt;
|
|
177
|
+
}
|
|
152
178
|
function getSafetySettings(threshold) {
|
|
153
179
|
const cached = safetySettingsCache.get(threshold);
|
|
154
180
|
if (cached) {
|
|
@@ -281,19 +307,21 @@ function findFirstStringCode(record, keys) {
|
|
|
281
307
|
}
|
|
282
308
|
return undefined;
|
|
283
309
|
}
|
|
310
|
+
const NUMERIC_ERROR_KEYS = ['status', 'statusCode', 'code'];
|
|
284
311
|
function getNumericErrorCode(error) {
|
|
285
312
|
const record = getNestedError(error);
|
|
286
313
|
if (!record) {
|
|
287
314
|
return undefined;
|
|
288
315
|
}
|
|
289
|
-
return findFirstNumericCode(record,
|
|
316
|
+
return findFirstNumericCode(record, NUMERIC_ERROR_KEYS);
|
|
290
317
|
}
|
|
318
|
+
const TRANSIENT_ERROR_KEYS = ['code', 'status', 'statusText'];
|
|
291
319
|
function getTransientErrorCode(error) {
|
|
292
320
|
const record = getNestedError(error);
|
|
293
321
|
if (!record) {
|
|
294
322
|
return undefined;
|
|
295
323
|
}
|
|
296
|
-
return findFirstStringCode(record,
|
|
324
|
+
return findFirstStringCode(record, TRANSIENT_ERROR_KEYS);
|
|
297
325
|
}
|
|
298
326
|
function shouldRetry(error) {
|
|
299
327
|
const numericCode = getNumericErrorCode(error);
|
|
@@ -322,7 +350,7 @@ function buildGenerationConfig(request, abortSignal) {
|
|
|
322
350
|
temperature: request.temperature ?? 1.0,
|
|
323
351
|
maxOutputTokens: request.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
|
|
324
352
|
responseMimeType: 'application/json',
|
|
325
|
-
responseSchema: request.responseSchema,
|
|
353
|
+
responseSchema: applyResponseKeyOrdering(request.responseSchema, request.responseKeyOrdering),
|
|
326
354
|
safetySettings: getSafetySettings(getSafetyThreshold()),
|
|
327
355
|
topP: 0.95,
|
|
328
356
|
topK: 40,
|
|
@@ -366,12 +394,12 @@ async function generateContentWithTimeout(request, model, timeoutMs) {
|
|
|
366
394
|
try {
|
|
367
395
|
return await getClient().models.generateContent({
|
|
368
396
|
model,
|
|
369
|
-
contents: request
|
|
397
|
+
contents: getPromptWithFunctionCallingContext(request),
|
|
370
398
|
config: buildGenerationConfig(request, signal),
|
|
371
399
|
});
|
|
372
400
|
}
|
|
373
401
|
catch (error) {
|
|
374
|
-
if (request.signal?.aborted) {
|
|
402
|
+
if (request.signal?.aborted === true) {
|
|
375
403
|
throw new Error('Gemini request was cancelled.');
|
|
376
404
|
}
|
|
377
405
|
if (controller.signal.aborted) {
|
|
@@ -466,8 +494,9 @@ function tryWakeNextWaiter() {
|
|
|
466
494
|
next();
|
|
467
495
|
}
|
|
468
496
|
}
|
|
469
|
-
async function
|
|
470
|
-
if (
|
|
497
|
+
async function waitForSlot(limit, getActiveCount, acquireSlot, waiters, requestSignal) {
|
|
498
|
+
if (waiters.length === 0 && getActiveCount() < limit) {
|
|
499
|
+
acquireSlot();
|
|
471
500
|
return;
|
|
472
501
|
}
|
|
473
502
|
if (requestSignal?.aborted) {
|
|
@@ -484,16 +513,17 @@ async function waitForConcurrencySlot(limit, requestSignal) {
|
|
|
484
513
|
if (requestSignal) {
|
|
485
514
|
requestSignal.removeEventListener('abort', onAbort);
|
|
486
515
|
}
|
|
516
|
+
acquireSlot();
|
|
487
517
|
resolve();
|
|
488
518
|
};
|
|
489
|
-
|
|
519
|
+
waiters.push(waiter);
|
|
490
520
|
const deadlineTimer = setTimeout(() => {
|
|
491
521
|
if (settled)
|
|
492
522
|
return;
|
|
493
523
|
settled = true;
|
|
494
|
-
const idx =
|
|
524
|
+
const idx = waiters.indexOf(waiter);
|
|
495
525
|
if (idx !== -1) {
|
|
496
|
-
|
|
526
|
+
waiters.splice(idx, 1);
|
|
497
527
|
}
|
|
498
528
|
if (requestSignal) {
|
|
499
529
|
requestSignal.removeEventListener('abort', onAbort);
|
|
@@ -505,9 +535,9 @@ async function waitForConcurrencySlot(limit, requestSignal) {
|
|
|
505
535
|
if (settled)
|
|
506
536
|
return;
|
|
507
537
|
settled = true;
|
|
508
|
-
const idx =
|
|
538
|
+
const idx = waiters.indexOf(waiter);
|
|
509
539
|
if (idx !== -1) {
|
|
510
|
-
|
|
540
|
+
waiters.splice(idx, 1);
|
|
511
541
|
}
|
|
512
542
|
clearTimeout(deadlineTimer);
|
|
513
543
|
reject(new Error('Gemini request was cancelled.'));
|
|
@@ -517,19 +547,254 @@ async function waitForConcurrencySlot(limit, requestSignal) {
|
|
|
517
547
|
}
|
|
518
548
|
});
|
|
519
549
|
}
|
|
550
|
+
async function waitForConcurrencySlot(limit, requestSignal) {
|
|
551
|
+
return waitForSlot(limit, () => activeCalls, () => {
|
|
552
|
+
activeCalls += 1;
|
|
553
|
+
}, slotWaiters, requestSignal);
|
|
554
|
+
}
|
|
555
|
+
function tryWakeNextBatchWaiter() {
|
|
556
|
+
const next = batchSlotWaiters.shift();
|
|
557
|
+
if (next !== undefined) {
|
|
558
|
+
next();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async function waitForBatchConcurrencySlot(limit, requestSignal) {
|
|
562
|
+
return waitForSlot(limit, () => activeBatchCalls, () => {
|
|
563
|
+
activeBatchCalls += 1;
|
|
564
|
+
}, batchSlotWaiters, requestSignal);
|
|
565
|
+
}
|
|
566
|
+
function getBatchState(payload) {
|
|
567
|
+
const record = asRecord(payload);
|
|
568
|
+
if (!record) {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
const directState = toUpperStringCode(record.state);
|
|
572
|
+
if (directState) {
|
|
573
|
+
return directState;
|
|
574
|
+
}
|
|
575
|
+
const metadata = asRecord(record.metadata);
|
|
576
|
+
if (!metadata) {
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
return toUpperStringCode(metadata.state);
|
|
580
|
+
}
|
|
581
|
+
function extractBatchResponseText(payload) {
|
|
582
|
+
const record = asRecord(payload);
|
|
583
|
+
if (!record) {
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
const inlineResponse = asRecord(record.inlineResponse);
|
|
587
|
+
const inlineText = typeof inlineResponse?.text === 'string' ? inlineResponse.text : undefined;
|
|
588
|
+
if (inlineText) {
|
|
589
|
+
return inlineText;
|
|
590
|
+
}
|
|
591
|
+
const response = asRecord(record.response);
|
|
592
|
+
if (!response) {
|
|
593
|
+
return undefined;
|
|
594
|
+
}
|
|
595
|
+
const responseText = typeof response.text === 'string' ? response.text : undefined;
|
|
596
|
+
if (responseText) {
|
|
597
|
+
return responseText;
|
|
598
|
+
}
|
|
599
|
+
const { inlineResponses } = response;
|
|
600
|
+
if (!Array.isArray(inlineResponses) || inlineResponses.length === 0) {
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
603
|
+
const firstInline = asRecord(inlineResponses[0]);
|
|
604
|
+
return typeof firstInline?.text === 'string' ? firstInline.text : undefined;
|
|
605
|
+
}
|
|
606
|
+
function extractBatchErrorDetail(payload) {
|
|
607
|
+
const record = asRecord(payload);
|
|
608
|
+
if (!record) {
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
const directError = asRecord(record.error);
|
|
612
|
+
const directMessage = typeof directError?.message === 'string' ? directError.message : undefined;
|
|
613
|
+
if (directMessage) {
|
|
614
|
+
return directMessage;
|
|
615
|
+
}
|
|
616
|
+
const metadata = asRecord(record.metadata);
|
|
617
|
+
const metadataError = asRecord(metadata?.error);
|
|
618
|
+
const metadataMessage = typeof metadataError?.message === 'string'
|
|
619
|
+
? metadataError.message
|
|
620
|
+
: undefined;
|
|
621
|
+
if (metadataMessage) {
|
|
622
|
+
return metadataMessage;
|
|
623
|
+
}
|
|
624
|
+
const response = asRecord(record.response);
|
|
625
|
+
const responseError = asRecord(response?.error);
|
|
626
|
+
return typeof responseError?.message === 'string'
|
|
627
|
+
? responseError.message
|
|
628
|
+
: undefined;
|
|
629
|
+
}
|
|
630
|
+
function getBatchSuccessResponseText(polled) {
|
|
631
|
+
const responseText = extractBatchResponseText(polled);
|
|
632
|
+
if (!responseText) {
|
|
633
|
+
const errorDetail = extractBatchErrorDetail(polled);
|
|
634
|
+
throw new Error(errorDetail
|
|
635
|
+
? `Gemini batch request succeeded but returned no response text: ${errorDetail}`
|
|
636
|
+
: 'Gemini batch request succeeded but returned no response text.');
|
|
637
|
+
}
|
|
638
|
+
return responseText;
|
|
639
|
+
}
|
|
640
|
+
function handleBatchTerminalState(state, payload) {
|
|
641
|
+
if (state === 'JOB_STATE_FAILED' || state === 'JOB_STATE_CANCELLED') {
|
|
642
|
+
const errorDetail = extractBatchErrorDetail(payload);
|
|
643
|
+
throw new Error(errorDetail
|
|
644
|
+
? `Gemini batch request ended with state ${state}: ${errorDetail}`
|
|
645
|
+
: `Gemini batch request ended with state ${state}.`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async function pollBatchStatusWithRetries(batches, batchName, onLog, requestSignal) {
|
|
649
|
+
const maxPollRetries = 2;
|
|
650
|
+
for (let attempt = 0; attempt <= maxPollRetries; attempt += 1) {
|
|
651
|
+
try {
|
|
652
|
+
return await batches.get({ name: batchName });
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
if (!canRetryAttempt(attempt, maxPollRetries, error)) {
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
658
|
+
await waitBeforeRetry(attempt, error, onLog, requestSignal);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
throw new Error('Batch polling retries exhausted unexpectedly.');
|
|
662
|
+
}
|
|
663
|
+
async function cancelBatchIfNeeded(request, batches, batchName, onLog, completed, timedOut) {
|
|
664
|
+
const aborted = request.signal?.aborted === true;
|
|
665
|
+
if (completed || (!aborted && !timedOut) || !batchName) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (batches.cancel === undefined) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
await batches.cancel({ name: batchName });
|
|
673
|
+
await emitGeminiLog(onLog, 'info', {
|
|
674
|
+
event: 'gemini_batch_cancelled',
|
|
675
|
+
details: {
|
|
676
|
+
batchName,
|
|
677
|
+
reason: timedOut ? 'timeout' : 'aborted',
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
await emitGeminiLog(onLog, 'warning', {
|
|
683
|
+
event: 'gemini_batch_cancel_failed',
|
|
684
|
+
details: {
|
|
685
|
+
batchName,
|
|
686
|
+
reason: timedOut ? 'timeout' : 'aborted',
|
|
687
|
+
error: getErrorMessage(error),
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async function runInlineBatchWithPolling(request, model, onLog) {
|
|
693
|
+
const client = getClient();
|
|
694
|
+
const { batches } = client;
|
|
695
|
+
if (batches === undefined) {
|
|
696
|
+
throw new Error('Batch mode requires SDK batch support, but batches API is unavailable.');
|
|
697
|
+
}
|
|
698
|
+
let batchName;
|
|
699
|
+
let completed = false;
|
|
700
|
+
let timedOut = false;
|
|
701
|
+
try {
|
|
702
|
+
const createPayload = {
|
|
703
|
+
model,
|
|
704
|
+
src: [
|
|
705
|
+
{
|
|
706
|
+
contents: [{ role: 'user', parts: [{ text: request.prompt }] }],
|
|
707
|
+
config: buildGenerationConfig(request, new AbortController().signal),
|
|
708
|
+
},
|
|
709
|
+
],
|
|
710
|
+
};
|
|
711
|
+
const createdJob = await batches.create(createPayload);
|
|
712
|
+
const createdRecord = asRecord(createdJob);
|
|
713
|
+
batchName =
|
|
714
|
+
typeof createdRecord?.name === 'string' ? createdRecord.name : undefined;
|
|
715
|
+
if (!batchName) {
|
|
716
|
+
throw new Error('Batch mode failed to return a job name.');
|
|
717
|
+
}
|
|
718
|
+
const pollStart = performance.now();
|
|
719
|
+
const timeoutMs = batchTimeoutMsConfig.get();
|
|
720
|
+
const pollIntervalMs = batchPollIntervalMsConfig.get();
|
|
721
|
+
await emitGeminiLog(onLog, 'info', {
|
|
722
|
+
event: 'gemini_batch_created',
|
|
723
|
+
details: { batchName },
|
|
724
|
+
});
|
|
725
|
+
for (;;) {
|
|
726
|
+
if (request.signal?.aborted === true) {
|
|
727
|
+
throw new Error('Gemini request was cancelled.');
|
|
728
|
+
}
|
|
729
|
+
const elapsedMs = Math.round(performance.now() - pollStart);
|
|
730
|
+
if (elapsedMs > timeoutMs) {
|
|
731
|
+
timedOut = true;
|
|
732
|
+
throw new Error(`Gemini batch request timed out after ${formatNumber(timeoutMs)}ms.`);
|
|
733
|
+
}
|
|
734
|
+
const polled = await pollBatchStatusWithRetries(batches, batchName, onLog, request.signal);
|
|
735
|
+
const state = getBatchState(polled);
|
|
736
|
+
if (state === 'JOB_STATE_SUCCEEDED') {
|
|
737
|
+
const responseText = getBatchSuccessResponseText(polled);
|
|
738
|
+
completed = true;
|
|
739
|
+
return parseStructuredResponse(responseText);
|
|
740
|
+
}
|
|
741
|
+
handleBatchTerminalState(state, polled);
|
|
742
|
+
await sleep(pollIntervalMs, undefined, request.signal
|
|
743
|
+
? { ...SLEEP_UNREF_OPTIONS, signal: request.signal }
|
|
744
|
+
: SLEEP_UNREF_OPTIONS);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
finally {
|
|
748
|
+
await cancelBatchIfNeeded(request, batches, batchName, onLog, completed, timedOut);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
export function getGeminiQueueSnapshot() {
|
|
752
|
+
return {
|
|
753
|
+
activeCalls,
|
|
754
|
+
waitingCalls: slotWaiters.length,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
520
757
|
export async function generateStructuredJson(request) {
|
|
521
758
|
const model = request.model ?? getDefaultModel();
|
|
522
759
|
const timeoutMs = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
523
760
|
const maxRetries = request.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
761
|
+
const batchMode = request.batchMode ?? getDefaultBatchMode();
|
|
524
762
|
const { onLog } = request;
|
|
525
|
-
const limit =
|
|
526
|
-
|
|
527
|
-
|
|
763
|
+
const limit = batchMode === 'inline'
|
|
764
|
+
? maxConcurrentBatchCallsConfig.get()
|
|
765
|
+
: maxConcurrentCallsConfig.get();
|
|
766
|
+
const queueWaitStartedAt = performance.now();
|
|
767
|
+
if (batchMode === 'inline') {
|
|
768
|
+
await waitForBatchConcurrencySlot(limit, request.signal);
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
await waitForConcurrencySlot(limit, request.signal);
|
|
772
|
+
}
|
|
773
|
+
const queueWaitMs = Math.round(performance.now() - queueWaitStartedAt);
|
|
774
|
+
await safeCallOnLog(onLog, 'info', {
|
|
775
|
+
event: 'gemini_queue_acquired',
|
|
776
|
+
queueWaitMs,
|
|
777
|
+
waitingCalls: batchMode === 'inline' ? batchSlotWaiters.length : slotWaiters.length,
|
|
778
|
+
activeCalls,
|
|
779
|
+
activeBatchCalls,
|
|
780
|
+
mode: batchMode,
|
|
781
|
+
});
|
|
528
782
|
try {
|
|
529
|
-
return await geminiContext.run({ requestId: nextRequestId(), model }, () =>
|
|
783
|
+
return await geminiContext.run({ requestId: nextRequestId(), model }, () => {
|
|
784
|
+
if (batchMode === 'inline') {
|
|
785
|
+
return runInlineBatchWithPolling(request, model, onLog);
|
|
786
|
+
}
|
|
787
|
+
return runWithRetries(request, model, timeoutMs, maxRetries, onLog);
|
|
788
|
+
});
|
|
530
789
|
}
|
|
531
790
|
finally {
|
|
532
|
-
|
|
533
|
-
|
|
791
|
+
if (batchMode === 'inline') {
|
|
792
|
+
activeBatchCalls -= 1;
|
|
793
|
+
tryWakeNextBatchWaiter();
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
activeCalls -= 1;
|
|
797
|
+
tryWakeNextWaiter();
|
|
798
|
+
}
|
|
534
799
|
}
|
|
535
800
|
}
|
|
@@ -23,6 +23,8 @@ export interface ToolContract {
|
|
|
23
23
|
* Omit to use the global default (0.2).
|
|
24
24
|
*/
|
|
25
25
|
temperature?: number;
|
|
26
|
+
/** Enables deterministic JSON guidance and schema key ordering. */
|
|
27
|
+
deterministicJson?: boolean;
|
|
26
28
|
params: readonly ToolParameterContract[];
|
|
27
29
|
outputShape: string;
|
|
28
30
|
gotchas: readonly string[];
|
|
@@ -53,6 +55,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
53
55
|
readonly thinkingLevel: "minimal";
|
|
54
56
|
readonly maxOutputTokens: 4096;
|
|
55
57
|
readonly temperature: 1;
|
|
58
|
+
readonly deterministicJson: true;
|
|
56
59
|
readonly params: readonly [{
|
|
57
60
|
readonly name: "repository";
|
|
58
61
|
readonly type: "string";
|
|
@@ -77,6 +80,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
77
80
|
readonly thinkingLevel: "minimal";
|
|
78
81
|
readonly maxOutputTokens: 4096;
|
|
79
82
|
readonly temperature: 1;
|
|
83
|
+
readonly deterministicJson: true;
|
|
80
84
|
readonly params: readonly [{
|
|
81
85
|
readonly name: "repository";
|
|
82
86
|
readonly type: "string";
|
|
@@ -101,6 +105,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
101
105
|
readonly thinkingLevel: "high";
|
|
102
106
|
readonly maxOutputTokens: 12288;
|
|
103
107
|
readonly temperature: 1;
|
|
108
|
+
readonly deterministicJson: true;
|
|
104
109
|
readonly params: readonly [{
|
|
105
110
|
readonly name: "repository";
|
|
106
111
|
readonly type: "string";
|
|
@@ -144,6 +149,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
144
149
|
readonly thinkingLevel: "high";
|
|
145
150
|
readonly maxOutputTokens: 8192;
|
|
146
151
|
readonly temperature: 1;
|
|
152
|
+
readonly deterministicJson: true;
|
|
147
153
|
readonly params: readonly [{
|
|
148
154
|
readonly name: "findingTitle";
|
|
149
155
|
readonly type: "string";
|
|
@@ -169,6 +175,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
169
175
|
readonly thinkingLevel: "medium";
|
|
170
176
|
readonly maxOutputTokens: 8192;
|
|
171
177
|
readonly temperature: 1;
|
|
178
|
+
readonly deterministicJson: true;
|
|
172
179
|
readonly params: readonly [{
|
|
173
180
|
readonly name: "repository";
|
|
174
181
|
readonly type: "string";
|
|
@@ -205,6 +212,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
205
212
|
readonly thinkingLevel: "medium";
|
|
206
213
|
readonly maxOutputTokens: 2048;
|
|
207
214
|
readonly temperature: 1;
|
|
215
|
+
readonly deterministicJson: true;
|
|
208
216
|
readonly params: readonly [{
|
|
209
217
|
readonly name: "language";
|
|
210
218
|
readonly type: "string";
|
|
@@ -223,6 +231,7 @@ export declare const TOOL_CONTRACTS: readonly [{
|
|
|
223
231
|
readonly thinkingLevel: "minimal";
|
|
224
232
|
readonly maxOutputTokens: 4096;
|
|
225
233
|
readonly temperature: 1;
|
|
234
|
+
readonly deterministicJson: true;
|
|
226
235
|
readonly params: readonly [{
|
|
227
236
|
readonly name: "language";
|
|
228
237
|
readonly type: "string";
|
|
@@ -43,6 +43,7 @@ export const TOOL_CONTRACTS = [
|
|
|
43
43
|
thinkingLevel: FLASH_TRIAGE_THINKING_LEVEL,
|
|
44
44
|
maxOutputTokens: FLASH_TRIAGE_MAX_OUTPUT_TOKENS,
|
|
45
45
|
temperature: TRIAGE_TEMPERATURE,
|
|
46
|
+
deterministicJson: true,
|
|
46
47
|
params: [
|
|
47
48
|
{
|
|
48
49
|
name: 'repository',
|
|
@@ -76,6 +77,7 @@ export const TOOL_CONTRACTS = [
|
|
|
76
77
|
thinkingLevel: FLASH_TRIAGE_THINKING_LEVEL,
|
|
77
78
|
maxOutputTokens: FLASH_TRIAGE_MAX_OUTPUT_TOKENS,
|
|
78
79
|
temperature: TRIAGE_TEMPERATURE,
|
|
80
|
+
deterministicJson: true,
|
|
79
81
|
params: [
|
|
80
82
|
{
|
|
81
83
|
name: 'repository',
|
|
@@ -109,6 +111,7 @@ export const TOOL_CONTRACTS = [
|
|
|
109
111
|
thinkingLevel: PRO_THINKING_LEVEL,
|
|
110
112
|
maxOutputTokens: PRO_REVIEW_MAX_OUTPUT_TOKENS,
|
|
111
113
|
temperature: ANALYSIS_TEMPERATURE,
|
|
114
|
+
deterministicJson: true,
|
|
112
115
|
params: [
|
|
113
116
|
{
|
|
114
117
|
name: 'repository',
|
|
@@ -166,6 +169,7 @@ export const TOOL_CONTRACTS = [
|
|
|
166
169
|
thinkingLevel: PRO_THINKING_LEVEL,
|
|
167
170
|
maxOutputTokens: PRO_PATCH_MAX_OUTPUT_TOKENS,
|
|
168
171
|
temperature: PATCH_TEMPERATURE,
|
|
172
|
+
deterministicJson: true,
|
|
169
173
|
params: [
|
|
170
174
|
{
|
|
171
175
|
name: 'findingTitle',
|
|
@@ -201,6 +205,7 @@ export const TOOL_CONTRACTS = [
|
|
|
201
205
|
thinkingLevel: FLASH_THINKING_LEVEL,
|
|
202
206
|
maxOutputTokens: FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS,
|
|
203
207
|
temperature: CREATIVE_TEMPERATURE,
|
|
208
|
+
deterministicJson: true,
|
|
204
209
|
params: [
|
|
205
210
|
{
|
|
206
211
|
name: 'repository',
|
|
@@ -248,6 +253,7 @@ export const TOOL_CONTRACTS = [
|
|
|
248
253
|
thinkingLevel: FLASH_THINKING_LEVEL,
|
|
249
254
|
maxOutputTokens: FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS,
|
|
250
255
|
temperature: ANALYSIS_TEMPERATURE,
|
|
256
|
+
deterministicJson: true,
|
|
251
257
|
params: [
|
|
252
258
|
{
|
|
253
259
|
name: 'language',
|
|
@@ -272,6 +278,7 @@ export const TOOL_CONTRACTS = [
|
|
|
272
278
|
thinkingLevel: FLASH_TRIAGE_THINKING_LEVEL,
|
|
273
279
|
maxOutputTokens: FLASH_API_BREAKING_MAX_OUTPUT_TOKENS,
|
|
274
280
|
temperature: TRIAGE_TEMPERATURE,
|
|
281
|
+
deterministicJson: true,
|
|
275
282
|
params: [
|
|
276
283
|
{
|
|
277
284
|
name: 'language',
|
|
@@ -72,6 +72,10 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
|
|
|
72
72
|
temperature?: number;
|
|
73
73
|
/** Optional opt-in to Gemini thought output. Defaults to false. */
|
|
74
74
|
includeThoughts?: boolean;
|
|
75
|
+
/** Optional deterministic JSON mode for stricter key ordering and repair prompting. */
|
|
76
|
+
deterministicJson?: boolean;
|
|
77
|
+
/** Optional batch execution mode. Defaults to runtime setting. */
|
|
78
|
+
batchMode?: 'off' | 'inline';
|
|
75
79
|
/** Optional formatter for human-readable text output. */
|
|
76
80
|
formatOutput?: (result: TFinal) => string;
|
|
77
81
|
/** Optional context text used in progress messages. */
|
|
@@ -81,6 +85,7 @@ export interface StructuredToolTaskConfig<TInput extends object = Record<string,
|
|
|
81
85
|
/** Builds the system instruction and user prompt from parsed tool input. */
|
|
82
86
|
buildPrompt: (input: TInput, ctx: ToolExecutionContext) => PromptParts;
|
|
83
87
|
}
|
|
88
|
+
export declare function summarizeSchemaValidationErrorForRetry(errorMessage: string): string;
|
|
84
89
|
export declare function wrapToolHandler<TInput, TResult extends CallToolResult>(options: {
|
|
85
90
|
toolName: string;
|
|
86
91
|
progressContext?: (input: TInput) => string;
|
package/dist/lib/tool-factory.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { DefaultOutputSchema } from '../schemas/outputs.js';
|
|
3
3
|
import { getDiff } from './diff-store.js';
|
|
4
|
+
import { createCachedEnvInt } from './env-config.js';
|
|
4
5
|
import { getErrorMessage, RETRYABLE_UPSTREAM_ERROR_PATTERN } from './errors.js';
|
|
5
6
|
import { stripJsonSchemaConstraints } from './gemini-schema.js';
|
|
6
7
|
import { generateStructuredJson, getCurrentRequestId } from './gemini.js';
|
|
@@ -14,6 +15,9 @@ const TIMEOUT_ERROR_PATTERN = /timed out|timeout/i;
|
|
|
14
15
|
const BUDGET_ERROR_PATTERN = /exceeds limit|max allowed size|input too large/i;
|
|
15
16
|
const BUSY_ERROR_PATTERN = /too many concurrent/i;
|
|
16
17
|
const MAX_SCHEMA_RETRIES = 1;
|
|
18
|
+
const DEFAULT_SCHEMA_RETRY_ERROR_CHARS = 1_500;
|
|
19
|
+
const schemaRetryErrorCharsConfig = createCachedEnvInt('MAX_SCHEMA_RETRY_ERROR_CHARS', DEFAULT_SCHEMA_RETRY_ERROR_CHARS);
|
|
20
|
+
const DETERMINISTIC_JSON_RETRY_NOTE = 'Deterministic JSON mode: keep key names exactly as schema-defined and preserve stable field ordering.';
|
|
17
21
|
function createGeminiResponseSchema(config) {
|
|
18
22
|
const sourceSchema = config.geminiSchema ?? config.resultSchema;
|
|
19
23
|
return stripJsonSchemaConstraints(z.toJSONSchema(sourceSchema));
|
|
@@ -21,6 +25,35 @@ function createGeminiResponseSchema(config) {
|
|
|
21
25
|
function parseToolInput(input, fullInputSchema) {
|
|
22
26
|
return fullInputSchema.parse(input);
|
|
23
27
|
}
|
|
28
|
+
function extractResponseKeyOrdering(responseSchema) {
|
|
29
|
+
const schemaType = responseSchema.type;
|
|
30
|
+
if (schemaType !== 'object') {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const { properties } = responseSchema;
|
|
34
|
+
if (typeof properties !== 'object' || properties === null) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return Object.keys(properties);
|
|
38
|
+
}
|
|
39
|
+
export function summarizeSchemaValidationErrorForRetry(errorMessage) {
|
|
40
|
+
const maxChars = Math.max(200, schemaRetryErrorCharsConfig.get());
|
|
41
|
+
const compact = errorMessage.replace(/\s+/g, ' ').trim();
|
|
42
|
+
if (compact.length <= maxChars) {
|
|
43
|
+
return compact;
|
|
44
|
+
}
|
|
45
|
+
return `${compact.slice(0, maxChars - 3)}...`;
|
|
46
|
+
}
|
|
47
|
+
function createSchemaRetryPrompt(prompt, errorMessage, deterministicJson) {
|
|
48
|
+
const summarizedError = summarizeSchemaValidationErrorForRetry(errorMessage);
|
|
49
|
+
const deterministicNote = deterministicJson
|
|
50
|
+
? `\n${DETERMINISTIC_JSON_RETRY_NOTE}`
|
|
51
|
+
: '';
|
|
52
|
+
return {
|
|
53
|
+
summarizedError,
|
|
54
|
+
prompt: `${prompt}\n\nCRITICAL: The previous response failed schema validation. Error: ${summarizedError}${deterministicNote}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
24
57
|
function createGenerationRequest(config, promptParts, responseSchema, onLog, signal) {
|
|
25
58
|
const request = {
|
|
26
59
|
systemInstruction: promptParts.systemInstruction,
|
|
@@ -46,13 +79,23 @@ function createGenerationRequest(config, promptParts, responseSchema, onLog, sig
|
|
|
46
79
|
if (config.includeThoughts !== undefined) {
|
|
47
80
|
request.includeThoughts = config.includeThoughts;
|
|
48
81
|
}
|
|
82
|
+
if (config.deterministicJson) {
|
|
83
|
+
const responseKeyOrdering = extractResponseKeyOrdering(responseSchema);
|
|
84
|
+
if (responseKeyOrdering !== undefined) {
|
|
85
|
+
request.responseKeyOrdering = responseKeyOrdering;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (config.batchMode !== undefined) {
|
|
89
|
+
request.batchMode = config.batchMode;
|
|
90
|
+
}
|
|
49
91
|
if (signal !== undefined) {
|
|
50
92
|
request.signal = signal;
|
|
51
93
|
}
|
|
52
94
|
return request;
|
|
53
95
|
}
|
|
96
|
+
const VALIDATION_ERROR_PATTERN = /validation/i;
|
|
54
97
|
function classifyErrorMeta(error, message) {
|
|
55
|
-
if (error instanceof z.ZodError ||
|
|
98
|
+
if (error instanceof z.ZodError || VALIDATION_ERROR_PATTERN.test(message)) {
|
|
56
99
|
return {
|
|
57
100
|
kind: 'validation',
|
|
58
101
|
retryable: false,
|
|
@@ -341,11 +384,16 @@ export function registerStructuredToolTask(server, config) {
|
|
|
341
384
|
throw error;
|
|
342
385
|
}
|
|
343
386
|
const errorMessage = getErrorMessage(error);
|
|
387
|
+
const schemaRetryPrompt = createSchemaRetryPrompt(prompt, errorMessage, config.deterministicJson === true);
|
|
344
388
|
await onLog('warning', {
|
|
345
389
|
event: 'schema_validation_failed',
|
|
346
|
-
details: {
|
|
390
|
+
details: {
|
|
391
|
+
attempt,
|
|
392
|
+
error: schemaRetryPrompt.summarizedError,
|
|
393
|
+
originalChars: errorMessage.length,
|
|
394
|
+
},
|
|
347
395
|
});
|
|
348
|
-
retryPrompt =
|
|
396
|
+
retryPrompt = schemaRetryPrompt.prompt;
|
|
349
397
|
}
|
|
350
398
|
}
|
|
351
399
|
if (!parsed) {
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export type JsonObject = Record<string, unknown>;
|
|
2
2
|
export type GeminiLogHandler = (level: string, data: unknown) => Promise<void>;
|
|
3
|
+
export interface GeminiFunctionCallingContext {
|
|
4
|
+
readonly modelParts: readonly unknown[];
|
|
5
|
+
}
|
|
3
6
|
export interface GeminiRequestExecutionOptions {
|
|
4
7
|
maxRetries?: number;
|
|
5
8
|
timeoutMs?: number;
|
|
@@ -9,6 +12,9 @@ export interface GeminiRequestExecutionOptions {
|
|
|
9
12
|
includeThoughts?: boolean;
|
|
10
13
|
signal?: AbortSignal;
|
|
11
14
|
onLog?: GeminiLogHandler;
|
|
15
|
+
responseKeyOrdering?: readonly string[];
|
|
16
|
+
functionCallingContext?: GeminiFunctionCallingContext;
|
|
17
|
+
batchMode?: 'off' | 'inline';
|
|
12
18
|
}
|
|
13
19
|
export interface GeminiStructuredRequestOptions extends GeminiRequestExecutionOptions {
|
|
14
20
|
model?: string;
|
|
@@ -8,13 +8,18 @@ const DEFAULT_CONCURRENT_WAIT_MS = 2_000;
|
|
|
8
8
|
const DEFAULT_SAFETY_THRESHOLD = 'BLOCK_NONE';
|
|
9
9
|
const GEMINI_HARM_BLOCK_THRESHOLD_ENV_VAR = 'GEMINI_HARM_BLOCK_THRESHOLD';
|
|
10
10
|
const GEMINI_MODEL_ENV_VAR = 'GEMINI_MODEL';
|
|
11
|
+
const GEMINI_BATCH_MODE_ENV_VAR = 'GEMINI_BATCH_MODE';
|
|
11
12
|
const diffCharsConfig = createCachedEnvInt('MAX_DIFF_CHARS', DEFAULT_MAX_DIFF_CHARS);
|
|
12
13
|
const contextCharsConfig = createCachedEnvInt('MAX_CONTEXT_CHARS', DEFAULT_MAX_CONTEXT_CHARS);
|
|
13
14
|
const concurrentCallsConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS', DEFAULT_MAX_CONCURRENT_CALLS);
|
|
15
|
+
const concurrentBatchCallsConfig = createCachedEnvInt('MAX_CONCURRENT_BATCH_CALLS', 2);
|
|
14
16
|
const concurrentWaitConfig = createCachedEnvInt('MAX_CONCURRENT_CALLS_WAIT_MS', DEFAULT_CONCURRENT_WAIT_MS);
|
|
15
17
|
function getModelOverride() {
|
|
16
18
|
return process.env[GEMINI_MODEL_ENV_VAR] ?? FLASH_MODEL;
|
|
17
19
|
}
|
|
20
|
+
function getBatchMode() {
|
|
21
|
+
return process.env[GEMINI_BATCH_MODE_ENV_VAR] ?? 'off';
|
|
22
|
+
}
|
|
18
23
|
function getSafetyThreshold() {
|
|
19
24
|
return (process.env[GEMINI_HARM_BLOCK_THRESHOLD_ENV_VAR] ?? DEFAULT_SAFETY_THRESHOLD);
|
|
20
25
|
}
|
|
@@ -31,8 +36,10 @@ export function buildServerConfig() {
|
|
|
31
36
|
const maxDiffChars = diffCharsConfig.get();
|
|
32
37
|
const maxContextChars = contextCharsConfig.get();
|
|
33
38
|
const maxConcurrent = concurrentCallsConfig.get();
|
|
39
|
+
const maxConcurrentBatch = concurrentBatchCallsConfig.get();
|
|
34
40
|
const concurrentWaitMs = concurrentWaitConfig.get();
|
|
35
41
|
const defaultModel = getModelOverride();
|
|
42
|
+
const batchMode = getBatchMode();
|
|
36
43
|
const safetyThreshold = getSafetyThreshold();
|
|
37
44
|
const toolRows = getToolContracts()
|
|
38
45
|
.filter((contract) => contract.model !== 'none')
|
|
@@ -49,7 +56,9 @@ export function buildServerConfig() {
|
|
|
49
56
|
| Diff limit | ${formatNumber(maxDiffChars)} chars | \`MAX_DIFF_CHARS\` |
|
|
50
57
|
| Context limit (inspect) | ${formatNumber(maxContextChars)} chars | \`MAX_CONTEXT_CHARS\` |
|
|
51
58
|
| Concurrency limit | ${maxConcurrent} | \`MAX_CONCURRENT_CALLS\` |
|
|
59
|
+
| Batch concurrency limit | ${maxConcurrentBatch} | \`MAX_CONCURRENT_BATCH_CALLS\` |
|
|
52
60
|
| Wait timeout | ${formatNumber(concurrentWaitMs)}ms | \`MAX_CONCURRENT_CALLS_WAIT_MS\` |
|
|
61
|
+
| Batch mode | ${batchMode} | \`GEMINI_BATCH_MODE\` |
|
|
53
62
|
|
|
54
63
|
## Model Assignments
|
|
55
64
|
|
|
@@ -67,5 +76,11 @@ ${toolRows}
|
|
|
67
76
|
## API Keys
|
|
68
77
|
|
|
69
78
|
- Set \`GEMINI_API_KEY\` or \`GOOGLE_API_KEY\` environment variable (required)
|
|
79
|
+
|
|
80
|
+
## Batch Mode
|
|
81
|
+
|
|
82
|
+
- \`GEMINI_BATCH_MODE\`: \`off\` (default) or \`inline\`
|
|
83
|
+
- \`GEMINI_BATCH_POLL_INTERVAL_MS\`: poll cadence for batch status checks
|
|
84
|
+
- \`GEMINI_BATCH_TIMEOUT_MS\`: max wait for batch completion
|
|
70
85
|
`;
|
|
71
86
|
}
|
|
@@ -29,6 +29,9 @@ export function registerAnalyzeComplexityTool(server) {
|
|
|
29
29
|
...(TOOL_CONTRACT.temperature !== undefined
|
|
30
30
|
? { temperature: TOOL_CONTRACT.temperature }
|
|
31
31
|
: undefined),
|
|
32
|
+
...(TOOL_CONTRACT.deterministicJson !== undefined
|
|
33
|
+
? { deterministicJson: TOOL_CONTRACT.deterministicJson }
|
|
34
|
+
: undefined),
|
|
32
35
|
validateInput: (_input, ctx) => {
|
|
33
36
|
const slot = ctx.diffSlot;
|
|
34
37
|
if (!slot)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
-
import { computeDiffStatsAndSummaryFromFiles
|
|
2
|
+
import { computeDiffStatsAndSummaryFromFiles } from '../lib/diff-parser.js';
|
|
3
3
|
import { createNoDiffError } from '../lib/diff-store.js';
|
|
4
4
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
5
5
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
@@ -33,6 +33,9 @@ export function registerAnalyzePrImpactTool(server) {
|
|
|
33
33
|
...(TOOL_CONTRACT.temperature !== undefined
|
|
34
34
|
? { temperature: TOOL_CONTRACT.temperature }
|
|
35
35
|
: undefined),
|
|
36
|
+
...(TOOL_CONTRACT.deterministicJson !== undefined
|
|
37
|
+
? { deterministicJson: TOOL_CONTRACT.deterministicJson }
|
|
38
|
+
: undefined),
|
|
36
39
|
validateInput: (_input, ctx) => {
|
|
37
40
|
const slot = ctx.diffSlot;
|
|
38
41
|
if (!slot)
|
|
@@ -43,7 +46,7 @@ export function registerAnalyzePrImpactTool(server) {
|
|
|
43
46
|
formatOutput: (result) => `Impact Analysis (${result.severity}): ${result.summary}`,
|
|
44
47
|
buildPrompt: (input, ctx) => {
|
|
45
48
|
const diff = ctx.diffSlot?.diff ?? '';
|
|
46
|
-
const files =
|
|
49
|
+
const files = ctx.diffSlot?.parsedFiles ?? [];
|
|
47
50
|
const { stats, summary: fileSummary } = computeDiffStatsAndSummaryFromFiles(files);
|
|
48
51
|
const languageSegment = formatLanguageSegment(input.language);
|
|
49
52
|
return {
|
|
@@ -29,6 +29,9 @@ export function registerDetectApiBreakingTool(server) {
|
|
|
29
29
|
...(TOOL_CONTRACT.temperature !== undefined
|
|
30
30
|
? { temperature: TOOL_CONTRACT.temperature }
|
|
31
31
|
: undefined),
|
|
32
|
+
...(TOOL_CONTRACT.deterministicJson !== undefined
|
|
33
|
+
? { deterministicJson: TOOL_CONTRACT.deterministicJson }
|
|
34
|
+
: undefined),
|
|
32
35
|
validateInput: (_input, ctx) => {
|
|
33
36
|
const slot = ctx.diffSlot;
|
|
34
37
|
if (!slot)
|
|
@@ -59,7 +59,7 @@ export function registerGenerateDiffTool(server) {
|
|
|
59
59
|
const parsedFiles = parseDiffFiles(cleaned);
|
|
60
60
|
const stats = computeDiffStatsFromFiles(parsedFiles);
|
|
61
61
|
const generatedAt = new Date().toISOString();
|
|
62
|
-
storeDiff({ diff: cleaned, stats, generatedAt, mode });
|
|
62
|
+
storeDiff({ diff: cleaned, parsedFiles, stats, generatedAt, mode });
|
|
63
63
|
const summary = `Diff cached at ${DIFF_RESOURCE_URI} — ${stats.files} file(s), +${stats.added} -${stats.deleted}. All review tools are now ready.`;
|
|
64
64
|
return createToolResponse({
|
|
65
65
|
ok: true,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
-
import { computeDiffStatsFromFiles, parseDiffFiles, } from '../lib/diff-parser.js';
|
|
3
2
|
import { createNoDiffError } from '../lib/diff-store.js';
|
|
4
3
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
5
4
|
import { registerStructuredToolTask, } from '../lib/tool-factory.js';
|
|
@@ -18,9 +17,16 @@ function formatLanguageSegment(language) {
|
|
|
18
17
|
return language ? `\nLanguage: ${language}` : '';
|
|
19
18
|
}
|
|
20
19
|
function getDiffStats(ctx) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const slot = ctx.diffSlot;
|
|
21
|
+
if (!slot) {
|
|
22
|
+
return { diff: '', files: 0, added: 0, deleted: 0 };
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
diff: slot.diff,
|
|
26
|
+
files: slot.stats.files,
|
|
27
|
+
added: slot.stats.added,
|
|
28
|
+
deleted: slot.stats.deleted,
|
|
29
|
+
};
|
|
24
30
|
}
|
|
25
31
|
export function registerGenerateReviewSummaryTool(server) {
|
|
26
32
|
registerStructuredToolTask(server, {
|
|
@@ -40,6 +46,9 @@ export function registerGenerateReviewSummaryTool(server) {
|
|
|
40
46
|
...(TOOL_CONTRACT.temperature !== undefined
|
|
41
47
|
? { temperature: TOOL_CONTRACT.temperature }
|
|
42
48
|
: undefined),
|
|
49
|
+
...(TOOL_CONTRACT.deterministicJson !== undefined
|
|
50
|
+
? { deterministicJson: TOOL_CONTRACT.deterministicJson }
|
|
51
|
+
: undefined),
|
|
43
52
|
validateInput: (_input, ctx) => {
|
|
44
53
|
const slot = ctx.diffSlot;
|
|
45
54
|
if (!slot)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
-
import { computeDiffStatsAndPathsFromFiles
|
|
2
|
+
import { computeDiffStatsAndPathsFromFiles } from '../lib/diff-parser.js';
|
|
3
3
|
import { createNoDiffError } from '../lib/diff-store.js';
|
|
4
4
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
5
5
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
@@ -33,6 +33,9 @@ export function registerGenerateTestPlanTool(server) {
|
|
|
33
33
|
...(TOOL_CONTRACT.temperature !== undefined
|
|
34
34
|
? { temperature: TOOL_CONTRACT.temperature }
|
|
35
35
|
: undefined),
|
|
36
|
+
...(TOOL_CONTRACT.deterministicJson !== undefined
|
|
37
|
+
? { deterministicJson: TOOL_CONTRACT.deterministicJson }
|
|
38
|
+
: undefined),
|
|
36
39
|
validateInput: (_input, ctx) => {
|
|
37
40
|
const slot = ctx.diffSlot;
|
|
38
41
|
if (!slot)
|
|
@@ -47,7 +50,7 @@ export function registerGenerateTestPlanTool(server) {
|
|
|
47
50
|
},
|
|
48
51
|
buildPrompt: (input, ctx) => {
|
|
49
52
|
const diff = ctx.diffSlot?.diff ?? '';
|
|
50
|
-
const parsedFiles =
|
|
53
|
+
const parsedFiles = ctx.diffSlot?.parsedFiles ?? [];
|
|
51
54
|
const { stats, paths } = computeDiffStatsAndPathsFromFiles(parsedFiles);
|
|
52
55
|
const languageLine = formatOptionalLine('Language', input.language);
|
|
53
56
|
const frameworkLine = formatOptionalLine('Test Framework', input.testFramework);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { validateContextBudget } from '../lib/context-budget.js';
|
|
2
2
|
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
3
|
-
import { computeDiffStatsAndSummaryFromFiles
|
|
3
|
+
import { computeDiffStatsAndSummaryFromFiles } from '../lib/diff-parser.js';
|
|
4
4
|
import { createNoDiffError } from '../lib/diff-store.js';
|
|
5
5
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
6
6
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
@@ -69,6 +69,9 @@ export function registerInspectCodeQualityTool(server) {
|
|
|
69
69
|
...(TOOL_CONTRACT.temperature !== undefined
|
|
70
70
|
? { temperature: TOOL_CONTRACT.temperature }
|
|
71
71
|
: undefined),
|
|
72
|
+
...(TOOL_CONTRACT.deterministicJson !== undefined
|
|
73
|
+
? { deterministicJson: TOOL_CONTRACT.deterministicJson }
|
|
74
|
+
: undefined),
|
|
72
75
|
progressContext: (input) => {
|
|
73
76
|
const fileCount = input.files?.length;
|
|
74
77
|
return fileCount ? `+${fileCount} files` : '';
|
|
@@ -98,7 +101,7 @@ export function registerInspectCodeQualityTool(server) {
|
|
|
98
101
|
},
|
|
99
102
|
buildPrompt: (input, ctx) => {
|
|
100
103
|
const diff = ctx.diffSlot?.diff ?? '';
|
|
101
|
-
const parsedFiles =
|
|
104
|
+
const parsedFiles = ctx.diffSlot?.parsedFiles ?? [];
|
|
102
105
|
const { summary: fileSummary } = computeDiffStatsAndSummaryFromFiles(parsedFiles);
|
|
103
106
|
const fileContext = formatFileContext(input.files);
|
|
104
107
|
const languageLine = formatOptionalLine('Language', input.language);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { validateDiffBudget } from '../lib/diff-budget.js';
|
|
2
|
-
import { extractChangedPathsFromFiles
|
|
2
|
+
import { extractChangedPathsFromFiles } from '../lib/diff-parser.js';
|
|
3
3
|
import { createNoDiffError } from '../lib/diff-store.js';
|
|
4
4
|
import { requireToolContract } from '../lib/tool-contracts.js';
|
|
5
5
|
import { registerStructuredToolTask } from '../lib/tool-factory.js';
|
|
@@ -33,6 +33,9 @@ export function registerSuggestSearchReplaceTool(server) {
|
|
|
33
33
|
...(TOOL_CONTRACT.temperature !== undefined
|
|
34
34
|
? { temperature: TOOL_CONTRACT.temperature }
|
|
35
35
|
: undefined),
|
|
36
|
+
...(TOOL_CONTRACT.deterministicJson !== undefined
|
|
37
|
+
? { deterministicJson: TOOL_CONTRACT.deterministicJson }
|
|
38
|
+
: undefined),
|
|
36
39
|
validateInput: (_input, ctx) => {
|
|
37
40
|
const slot = ctx.diffSlot;
|
|
38
41
|
if (!slot)
|
|
@@ -47,7 +50,7 @@ export function registerSuggestSearchReplaceTool(server) {
|
|
|
47
50
|
},
|
|
48
51
|
buildPrompt: (input, ctx) => {
|
|
49
52
|
const diff = ctx.diffSlot?.diff ?? '';
|
|
50
|
-
const files =
|
|
53
|
+
const files = ctx.diffSlot?.parsedFiles ?? [];
|
|
51
54
|
const paths = extractChangedPathsFromFiles(files);
|
|
52
55
|
return {
|
|
53
56
|
systemInstruction: SYSTEM_INSTRUCTION,
|