@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 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 | 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_CALLS_WAIT_MS` | Max wait time for a free Gemini slot | `2000` | No |
384
- | `MAX_CONCURRENT_CALLS_POLL_MS` | Poll interval while waiting for a free slot | `25` | No |
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
 
@@ -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 = new Array(files.length);
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
- const path = resolveChangedPath(file);
45
- if (path) {
46
- paths.add(path);
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 { added, deleted, paths, summaries };
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. */
@@ -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;
@@ -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>;
@@ -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, ['status', 'statusCode', 'code']);
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, ['code', 'status', 'statusText']);
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.prompt,
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 waitForConcurrencySlot(limit, requestSignal) {
470
- if (activeCalls < limit) {
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
- slotWaiters.push(waiter);
519
+ waiters.push(waiter);
490
520
  const deadlineTimer = setTimeout(() => {
491
521
  if (settled)
492
522
  return;
493
523
  settled = true;
494
- const idx = slotWaiters.indexOf(waiter);
524
+ const idx = waiters.indexOf(waiter);
495
525
  if (idx !== -1) {
496
- slotWaiters.splice(idx, 1);
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 = slotWaiters.indexOf(waiter);
538
+ const idx = waiters.indexOf(waiter);
509
539
  if (idx !== -1) {
510
- slotWaiters.splice(idx, 1);
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 = maxConcurrentCallsConfig.get();
526
- await waitForConcurrencySlot(limit, request.signal);
527
- activeCalls += 1;
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 }, () => runWithRetries(request, model, timeoutMs, maxRetries, onLog));
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
- activeCalls -= 1;
533
- tryWakeNextWaiter();
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;
@@ -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 || /validation/i.test(message)) {
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: { attempt, error: errorMessage },
390
+ details: {
391
+ attempt,
392
+ error: schemaRetryPrompt.summarizedError,
393
+ originalChars: errorMessage.length,
394
+ },
347
395
  });
348
- retryPrompt = `${prompt}\n\nCRITICAL: The previous response failed schema validation. Error: ${errorMessage}`;
396
+ retryPrompt = schemaRetryPrompt.prompt;
349
397
  }
350
398
  }
351
399
  if (!parsed) {
@@ -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, parseDiffFiles, } from '../lib/diff-parser.js';
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 = parseDiffFiles(diff);
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 diff = ctx.diffSlot?.diff ?? '';
22
- const { files, added, deleted } = computeDiffStatsFromFiles(parseDiffFiles(diff));
23
- return { diff, files, added, deleted };
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, parseDiffFiles, } from '../lib/diff-parser.js';
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 = parseDiffFiles(diff);
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, parseDiffFiles, } from '../lib/diff-parser.js';
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 = parseDiffFiles(diff);
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, parseDiffFiles, } from '../lib/diff-parser.js';
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 = parseDiffFiles(diff);
53
+ const files = ctx.diffSlot?.parsedFiles ?? [];
51
54
  const paths = extractChangedPathsFromFiles(files);
52
55
  return {
53
56
  systemInstruction: SYSTEM_INSTRUCTION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j0hanz/code-review-analyst-mcp",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "mcpName": "io.github.j0hanz/code-review-analyst",
5
5
  "description": "Gemini-powered MCP server for code review analysis.",
6
6
  "type": "module",